diff --git a/Cargo.lock b/Cargo.lock index a23d2e26cf..eae56f4fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11280,6 +11280,7 @@ dependencies = [ "mimetypes", "mysql_queries", "once_cell", + "opaque_cursors", "pager", "password", "primitives", diff --git a/crates/schema/database/mysql_queries/CLAUDE.md b/crates/schema/database/mysql_queries/CLAUDE.md new file mode 100644 index 0000000000..c6e268671e --- /dev/null +++ b/crates/schema/database/mysql_queries/CLAUDE.md @@ -0,0 +1,74 @@ +# mysql_queries + +SQLx-based MySQL queries for the storyteller-web backend and background jobs. + +## Building + +``` +SQLX_OFFLINE=true cargo check -p mysql_queries +``` + +The `SQLX_OFFLINE=true` env var is **required** for compilation. This crate uses the sqlx +offline cache (`.sqlx/` directory at the workspace root) to validate queries at compile time +without a live database connection. + +## Query Style + +### Prefer `sqlx::query!` and `sqlx::query_as!` macros + +Always use the compile-time checked `sqlx::query!` and `sqlx::query_as!` macros over the +runtime `sqlx::query()` and `sqlx::query_as()` functions. The macros validate SQL syntax, +column names, and parameter types against the offline cache at compile time. + +```rust +// PREFERRED: compile-time checked +sqlx::query!( + r#"INSERT INTO my_table SET token = ?, name = ?"#, + token, + name, +) + .execute(args.mysql_executor) + .await?; + +// AVOID: runtime only, no compile-time checking +sqlx::query(r#"INSERT INTO my_table SET token = ?, name = ?"#) + .bind(token) + .bind(name) + .execute(args.mysql_executor) + .await?; +``` + +Use runtime `sqlx::query()` only when: +- The query involves a table not yet in the offline cache and you cannot run `cargo sqlx prepare` +- The query is dynamically constructed (rare) +- SELECT queries for new tables where column type metadata isn't cached yet + +### Offline Cache + +When adding new queries for **existing tables**, the `.sqlx/` cache should already have the +schema metadata. For **new tables**, you may need to: + +1. Run the migration against a real database +2. Run `cargo sqlx prepare` to regenerate the cache +3. Or manually create cache entries for simple INSERT/UPDATE queries (they only need parameter count) + +### Error Types + +New queries should return `Result` instead of the legacy `AnyhowResult`. +The `AnyhowResult` pattern is being phased out. + +### Transaction Support + +Use the executor-generic `Args` pattern for queries that need to work inside transactions: + +```rust +pub struct MyQueryArgs<'a, 'c: 'a, E> + where E: 'a + Executor<'c, Database = MySql> +{ + pub field: &'a str, + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} +``` + +This allows the same query function to accept both `&MySqlPool` and `&mut Transaction`. diff --git a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/list_user_impersonation_requests_for_user.rs b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/list_user_impersonation_requests_for_user.rs new file mode 100644 index 0000000000..dfb23d8634 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/list_user_impersonation_requests_for_user.rs @@ -0,0 +1,116 @@ +use chrono::{DateTime, Utc}; +use sqlx::{MySqlPool, Row}; + +use tokens::tokens::users::UserToken; + +pub struct UserImpersonationRequestListItem { + pub id: u64, + pub impersonator_user_token: UserToken, + pub impersonator_username: String, + pub impersonator_display_name: String, + pub impersonated_user_token: UserToken, + pub impersonated_username: String, + pub impersonated_display_name: String, + pub is_redeemed: bool, + pub is_expired: bool, + pub expires_at: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +pub struct ListUserImpersonationRequestsArgs<'a> { + pub user_token: &'a str, + pub maybe_cursor_id: Option, + pub limit: u32, + pub mysql_pool: &'a MySqlPool, +} + +pub async fn list_user_impersonation_requests_for_user( + args: ListUserImpersonationRequestsArgs<'_>, +) -> Result, sqlx::Error> { + + let limit = args.limit as i64; + + let rows = match args.maybe_cursor_id { + Some(cursor_id) => { + let cursor_id = cursor_id as i64; + sqlx::query( + r#" +SELECT + uir.id, + uir.impersonator_user_token, + impersonator.username as impersonator_username, + impersonator.display_name as impersonator_display_name, + uir.impersonated_user_token, + impersonated.username as impersonated_username, + impersonated.display_name as impersonated_display_name, + uir.is_redeemed, + (uir.expires_at < NOW()) as is_expired, + uir.expires_at, + uir.created_at, + uir.updated_at +FROM user_impersonation_requests uir +JOIN users impersonator ON impersonator.token = uir.impersonator_user_token +JOIN users impersonated ON impersonated.token = uir.impersonated_user_token +WHERE uir.impersonated_user_token = ? + AND uir.id < ? +ORDER BY uir.id DESC +LIMIT ? + "#, + ) + .bind(args.user_token) + .bind(cursor_id) + .bind(limit) + .fetch_all(args.mysql_pool) + .await? + } + None => { + sqlx::query( + r#" +SELECT + uir.id, + uir.impersonator_user_token, + impersonator.username as impersonator_username, + impersonator.display_name as impersonator_display_name, + uir.impersonated_user_token, + impersonated.username as impersonated_username, + impersonated.display_name as impersonated_display_name, + uir.is_redeemed, + (uir.expires_at < NOW()) as is_expired, + uir.expires_at, + uir.created_at, + uir.updated_at +FROM user_impersonation_requests uir +JOIN users impersonator ON impersonator.token = uir.impersonator_user_token +JOIN users impersonated ON impersonated.token = uir.impersonated_user_token +WHERE uir.impersonated_user_token = ? +ORDER BY uir.id DESC +LIMIT ? + "#, + ) + .bind(args.user_token) + .bind(limit) + .fetch_all(args.mysql_pool) + .await? + } + }; + + let items = rows.into_iter().map(|row| { + UserImpersonationRequestListItem { + id: row.get::("id") as u64, + impersonator_user_token: UserToken::new_from_str(row.get("impersonator_user_token")), + impersonator_username: row.get("impersonator_username"), + impersonator_display_name: row.get("impersonator_display_name"), + impersonated_user_token: UserToken::new_from_str(row.get("impersonated_user_token")), + impersonated_username: row.get("impersonated_username"), + impersonated_display_name: row.get("impersonated_display_name"), + is_redeemed: row.get("is_redeemed"), + is_expired: row.get::("is_expired") != 0, + expires_at: row.get("expires_at"), + created_at: row.get("created_at"), + updated_at: row.get("updated_at"), + } + }).collect(); + + Ok(items) +} diff --git a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs index 72dbbdfac9..e1a7bc1475 100644 --- a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs @@ -1,3 +1,4 @@ pub mod insert_user_impersonation_request; +pub mod list_user_impersonation_requests_for_user; pub mod lookup_user_impersonation_request; pub mod mark_impersonation_token_as_redeemed; diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index c3dd17da8a..0ddf9a7e34 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -65,6 +65,7 @@ memory_caching = { path = "../../../lib/caching/memory_caching" } migration = { path = "../../../schema/database/migration" } mimetypes.workspace = true mysql_queries.workspace = true +opaque_cursors.workspace = true pager.workspace = true password = { path = "../../../lib/password" } rootly_client.workspace = true diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 98ae8ea065..d1c6f2e752 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -86,6 +86,7 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_error::Medi use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*; use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*; use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::*; +use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*; use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::*; use crate::http_server::endpoints::prompts::get_prompt_handler::*; use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest; @@ -508,6 +509,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler, crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler, crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler, + crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler, crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler, // Credits crate::http_server::endpoints::credits::get_session_credits_handler::get_session_credits_handler, @@ -887,6 +889,9 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux ListFeaturedWeightsError, ListFeaturedWeightsQueryParams, ListFeaturedWeightsSuccessResponse, + ListImpersonationRequestsPathInfo, + ListImpersonationRequestsQueryParams, + ListUserImpersonationRequestsSuccessResponse, ListMediaFilesByBatchPathInfo, ListMediaFilesByBatchSuccessResponse, ListMediaFilesForUserPathInfo, @@ -995,6 +1000,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux UploadEngineAssetMediaSuccessResponse, UploadImageMediaFileForm, UploadImageMediaFileSuccessResponse, + UserImpersonationRequestResponse, UploadMediaSuccessResponse, UploadNewEngineAssetFileForm, UploadNewEngineAssetSuccessResponse, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs index 2dac44a86f..9fa718c173 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs @@ -1 +1,2 @@ +pub mod moderator_list_user_session_impersonation_requests_for_user_handler; pub mod moderator_user_session_impersonation_request_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_for_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_for_user_handler.rs new file mode 100644 index 0000000000..03316bf24f --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_for_user_handler.rs @@ -0,0 +1,152 @@ +use std::sync::Arc; + +use actix_web::web::{Json, Path, Query}; +use actix_web::{web, HttpRequest}; +use chrono::{DateTime, Utc}; +use log::warn; +use utoipa::{IntoParams, ToSchema}; + +use mysql_queries::queries::user_impersonation_requests::list_user_impersonation_requests_for_user::{ + list_user_impersonation_requests_for_user, ListUserImpersonationRequestsArgs, +}; +use tokens::tokens::users::UserToken; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::web_utils::user_session::require_moderator::{ + require_moderator, UseDatabase, +}; +use crate::state::server_state::ServerState; + +const CURSOR_NAME: &str = "moduserimp"; +const DEFAULT_LIMIT: u32 = 50; +const MAX_LIMIT: u32 = 1000; + +// --- Request --- + +#[derive(Deserialize, ToSchema)] +pub struct ListImpersonationRequestsPathInfo { + user_token: UserToken, +} + +#[derive(Deserialize, ToSchema, IntoParams)] +pub struct ListImpersonationRequestsQueryParams { + pub cursor: Option, + pub limit: Option, +} + +// --- Response --- + +#[derive(Serialize, ToSchema)] +pub struct ListUserImpersonationRequestsSuccessResponse { + pub success: bool, + pub impersonation_requests: Vec, + pub maybe_cursor: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct UserImpersonationRequestResponse { + pub impersonator_user_token: UserToken, + pub impersonator_username: String, + pub impersonator_display_name: String, + pub impersonated_user_token: UserToken, + pub impersonated_username: String, + pub impersonated_display_name: String, + pub is_redeemed: bool, + pub is_expired: bool, + pub expires_at: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +// --- Handler --- + +/// List impersonation requests for a given user. Moderators only. +#[utoipa::path( + get, + tag = "Moderation", + path = "/v1/moderation/user_sessions/impersonation_requests/{user_token}", + params( + ListImpersonationRequestsQueryParams, + ), + responses( + (status = 200, description = "Success", body = ListUserImpersonationRequestsSuccessResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderator_list_user_session_impersonation_requests_for_user_handler( + http_request: HttpRequest, + path: Path, + query: Query, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + let _user_session = require_moderator( + &http_request, + &server_state, + UseDatabase::GrabNewConnection, + ).await.map_err(|err| { + warn!("Moderator check failed: {:?}", err); + AdvancedCommonWebError::NotAuthorized + })?; + + let limit = query.limit + .unwrap_or(DEFAULT_LIMIT) + .min(MAX_LIMIT); + + let maybe_cursor_id = match &query.cursor { + None => None, + Some(cursor_str) => { + let decoded = server_state.opaque_cursors + .decode_cursor_expecting_name(CURSOR_NAME, cursor_str) + .map_err(|err| { + warn!("Failed to decode cursor: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage( + "Invalid cursor".to_string()) + })?; + decoded.last_id + } + }; + + let records = list_user_impersonation_requests_for_user( + ListUserImpersonationRequestsArgs { + user_token: path.user_token.as_str(), + maybe_cursor_id, + limit, + mysql_pool: &server_state.mysql_pool, + }, + ).await.map_err(|err| { + warn!("Failed to list impersonation requests: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + let maybe_cursor = records.last().map(|last| { + server_state.opaque_cursors + .encode_last_id_cursor(CURSOR_NAME, last.id) + }).transpose().map_err(|err| { + warn!("Failed to encode cursor: {:?}", err); + AdvancedCommonWebError::server_error_with_message("Failed to encode cursor") + })?; + + let impersonation_requests = records.into_iter().map(|r| { + UserImpersonationRequestResponse { + impersonator_user_token: r.impersonator_user_token, + impersonator_username: r.impersonator_username, + impersonator_display_name: r.impersonator_display_name, + impersonated_user_token: r.impersonated_user_token, + impersonated_username: r.impersonated_username, + impersonated_display_name: r.impersonated_display_name, + is_redeemed: r.is_redeemed, + is_expired: r.is_expired, + expires_at: r.expires_at, + created_at: r.created_at, + updated_at: r.updated_at, + } + }).collect(); + + Ok(Json(ListUserImpersonationRequestsSuccessResponse { + success: true, + impersonation_requests, + maybe_cursor, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs index ac96f71691..f657df59af 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs @@ -36,6 +36,7 @@ use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_st use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler; use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler; use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::edit_user_feature_flags_handler; +use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler; use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler; pub fn add_moderator_routes (app: App) -> App @@ -109,6 +110,10 @@ pub fn add_moderator_routes (app: App) -> App .route(web::post().to(moderator_user_session_impersonation_request_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/impersonation_requests/{user_token}") + .route(web::get().to(moderator_list_user_session_impersonation_requests_for_user_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) .service(web::resource("/wallet_ledger_entry/{wallet_ledger_entry_token}") .route(web::get().to(moderator_get_wallet_ledger_entry_handler)) diff --git a/crates/service/web/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs index 34a56b5a9c..bf2083bac0 100644 --- a/crates/service/web/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -46,8 +46,6 @@ use bootstrap::bootstrap::{bootstrap, BootstrapArgs}; use chrono::Utc; use cloud_storage::bucket_client::BucketClient; use component_traits::traits::internal_user_lookup::InternalUserLookup; -use shared_env_var_config::logging::DEFAULT_RUST_LOG; -use shared_env_var_config::redis::env_get_redis_0_connection_string_or_default; use elasticsearch::http::transport::Transport; use elasticsearch::Elasticsearch; use errors::AnyhowResult; @@ -60,6 +58,8 @@ use mysql_queries::mediators::firehose_publisher::FirehosePublisher; use redis::Client; use redis_caching::redis_ttl_cache::RedisTtlCache; use reusable_types::server_environment::ServerEnvironment; +use shared_env_var_config::logging::DEFAULT_RUST_LOG; +use shared_env_var_config::redis::env_get_redis_0_connection_string_or_default; use tokio::runtime::Runtime; use url_config::third_party_url_redirector::ThirdPartyUrlRedirector; use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge; @@ -71,16 +71,15 @@ use crate::billing::stripe_internal_user_lookup_impl::StripeInternalUserLookupIm use crate::configs::app_startup::redis_rate_limiters::configure_redis_rate_limiters; use crate::configs::connect_to_database::connect_to_database; use crate::configs::static_api_tokens::StaticApiTokenSet; -use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager; use crate::http_server::middleware::error_alerting_middleware::error_alerting_middleware::ErrorAlertingMiddleware; use crate::http_server::middleware::pushback_filter_middleware::PushbackFilter; use crate::http_server::routes::add_routes::add_routes; use crate::http_server::session::session_checker::SessionChecker; use crate::http_server::web_utils::handle_multipart_error::handle_multipart_error; use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator; +use crate::startup::build_pager::build_pager; use crate::state::certs::google_sign_in_cert::GoogleSignInCert; use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache; -use crate::startup::build_pager::build_pager; use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans, WorldLabsData}; use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread; @@ -89,6 +88,8 @@ use crate::threads::poll_model_token_info_thread::poll_model_token_info_thread; use crate::util::encrypted_sort_id::SortKeyCrypto; use crate::util::troll_user_bans::load_troll_user_ban_list_from_directory::load_user_token_ban_list_from_directory; use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList; +use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager; +use opaque_cursors::v2::opaque_cursor_encoder_v2::OpaqueCursorEncoderV2; pub mod billing; pub mod configs; @@ -280,7 +281,9 @@ async fn main() -> AnyhowResult<()> { // We can even rotate it without too much impact to users. let sort_key_crypto_secret = easyenv::get_env_string_or_default("SORT_KEY_SECRET", "webscale"); + let sort_key_crypto = SortKeyCrypto::new(&sort_key_crypto_secret); + let opaque_cursor_encoder = OpaqueCursorEncoderV2::new(&sort_key_crypto_secret); let health_check_interval = easyenv::get_env_duration_seconds_or_default( "HEALTH_CHECK_INTERVAL_SECS", Duration::from_secs(3)); @@ -470,6 +473,7 @@ async fn main() -> AnyhowResult<()> { auto_gc_bucket_client, audio_uploads_bucket_root, sort_key_crypto, + opaque_cursors: opaque_cursor_encoder, static_api_token_set, fal: FalData { api_key: fal_api_key, diff --git a/crates/service/web/storyteller_web/src/state/server_state.rs b/crates/service/web/storyteller_web/src/state/server_state.rs index 80478fd253..1f4c1263d2 100644 --- a/crates/service/web/storyteller_web/src/state/server_state.rs +++ b/crates/service/web/storyteller_web/src/state/server_state.rs @@ -1,3 +1,22 @@ +use crate::configs::app_startup::username_set::UsernameSet; +use crate::configs::static_api_tokens::StaticApiTokenSet; +use crate::http_server::deprecated_endpoints::categories::tts::list_fully_computed_assigned_tts_categories::list_fully_computed_assigned_tts_categories::ModelTokensByCategoryToken; +use crate::http_server::deprecated_endpoints::leaderboard::get_leaderboard::LeaderboardInfo; +use crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::ListFeaturedMediaFilesQueryParams; +use crate::http_server::endpoints::stats::result_transformer::CacheableQueueStats; +use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse; +use crate::http_server::endpoints::voice_conversion::list_voice_conversion_models_handler::VoiceConversionModel; +use crate::http_server::session::session_checker::SessionChecker; +use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter; +use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator; +use crate::state::certs::google_sign_in_cert::GoogleSignInCert; +use crate::state::flags::paging_flags::PagingFlags; +use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache; +use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; +use crate::util::encrypted_sort_id::SortKeyCrypto; +use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList; + +use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager; use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager; use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSet; use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList; @@ -9,7 +28,6 @@ use elasticsearch::Elasticsearch; use fal_client::creds::fal_api_key::FalApiKey; use memory_caching::arc_ttl_sieve::ArcTtlSieve; use memory_caching::single_item_ttl_cache::SingleItemTtlCache; -use pager::client::pager::Pager; use mysql_queries::mediators::badge_granter::BadgeGranter; use mysql_queries::mediators::firehose_publisher::FirehosePublisher; use mysql_queries::queries::generic_inference::web::get_pending_inference_job_count::InferenceQueueLengthResult; @@ -17,31 +35,14 @@ use mysql_queries::queries::media_files::list::list_featured_media_files::Featur use mysql_queries::queries::model_categories::list_categories_query_builder::CategoryList; use mysql_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_count::TtsQueueLengthResult; use mysql_queries::queries::w2l::w2l_templates::list_w2l_templates::W2lTemplateRecordForList; +use opaque_cursors::v2::opaque_cursor_encoder_v2::OpaqueCursorEncoderV2; +use pager::client::pager::Pager; use redis::Client; use redis_caching::redis_ttl_cache::RedisTtlCache; use reusable_types::server_environment::ServerEnvironment; use sqlx::MySqlPool; use url_config::third_party_url_redirector::ThirdPartyUrlRedirector; -use crate::configs::app_startup::username_set::UsernameSet; -use crate::configs::static_api_tokens::StaticApiTokenSet; -use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager; -use crate::http_server::deprecated_endpoints::categories::tts::list_fully_computed_assigned_tts_categories::list_fully_computed_assigned_tts_categories::ModelTokensByCategoryToken; -use crate::http_server::deprecated_endpoints::leaderboard::get_leaderboard::LeaderboardInfo; -use crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::ListFeaturedMediaFilesQueryParams; -use crate::http_server::endpoints::stats::result_transformer::CacheableQueueStats; -use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse; -use crate::http_server::endpoints::voice_conversion::list_voice_conversion_models_handler::VoiceConversionModel; -use crate::http_server::session::session_checker::SessionChecker; -use crate::state::flags::paging_flags::PagingFlags; -use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter; -use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator; -use crate::state::certs::google_sign_in_cert::GoogleSignInCert; -use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache; -use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; -use crate::util::encrypted_sort_id::SortKeyCrypto; -use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList; - /// State that is injected into every endpoint. pub struct ServerState { /// Configuration from ENV vars. @@ -108,6 +109,7 @@ pub struct ServerState { pub audio_uploads_bucket_root: String, pub sort_key_crypto: SortKeyCrypto, + pub opaque_cursors: OpaqueCursorEncoderV2, pub ip_ban_list: IpBanList,