mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 10:09:29 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ad838f3e | ||
|
|
fcfc8bf485 | ||
|
|
646ef895b3 | ||
|
|
ac7058c252 | ||
|
|
a6ba4c90d5 | ||
|
|
c93ff4a3aa |
@@ -0,0 +1,29 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Stores the media files (images) used as input when creating a character.
|
||||
CREATE TABLE character_creation_assets (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Foreign key to the characters table.
|
||||
character_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The media file used as a creation input (e.g. reference image).
|
||||
asset_media_file_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY index_character_token (character_token),
|
||||
KEY index_asset_media_file_token (asset_media_file_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,28 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Stores prompts/descriptions used when creating a character.
|
||||
CREATE TABLE character_creation_prompts (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Foreign key to the characters table.
|
||||
character_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The prompt or description used during character creation.
|
||||
prompt TEXT DEFAULT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY index_character_token (character_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,71 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Maps our characters to their representations in third-party systems.
|
||||
-- A single character may have entries in multiple third-party systems
|
||||
-- (e.g. Kinovi for Seedance, Runway for Gen-3, etc.).
|
||||
-- If a third-party character needs to be recreated, a new row is inserted.
|
||||
CREATE TABLE character_third_party_entities (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Effective "primary key" (PUBLIC)
|
||||
-- We may need to recreate the character in a third party, so this gets its own token.
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Foreign key to the characters table.
|
||||
character_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Whether this third-party entity is active and usable.
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== THIRD PARTY DETAILS ==========
|
||||
|
||||
-- Which third-party system the character lives in (enum crate type).
|
||||
-- e.g. "kinovi", "runway", etc.
|
||||
third_party VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Which types of models can use this character (enum crate type).
|
||||
-- e.g. "seedance_2_pro", "seedance_2_fast", etc.
|
||||
third_party_scopes VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The identifier in the third-party system.
|
||||
-- e.g. Kinovi's "char_1774752056469_2wlxoq"
|
||||
third_party_id VARCHAR(64) NOT NULL,
|
||||
|
||||
-- The name used by the third party.
|
||||
-- Important for Kinovi since characters are referenced by name in prompts.
|
||||
third_party_name VARCHAR(255) NOT NULL,
|
||||
|
||||
-- An upstream third party of the third party (if any).
|
||||
-- e.g. Kinovi's assetId "asset-20260329104101-kk5kl"
|
||||
maybe_fourth_party_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- ========== JOB TRACKING ==========
|
||||
|
||||
-- Our job system token (if the character was created via a job).
|
||||
maybe_origin_job_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Third party's job system token (if any).
|
||||
maybe_origin_third_party_job_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP DEFAULT NULL,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY index_character_token (character_token),
|
||||
KEY index_third_party (third_party),
|
||||
KEY index_third_party_id (third_party_id),
|
||||
KEY index_is_active (is_active)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,60 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Characters are reusable entities that can be referenced in video/image generations.
|
||||
-- A character is created from one or more reference images and registered with third-party
|
||||
-- systems (e.g. Kinovi/Seedance) that support character-based generation.
|
||||
CREATE TABLE characters (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Effective "primary key" (PUBLIC)
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Whether the character has been fully created and is available for use.
|
||||
-- A character may exist in the database before its creation job completes.
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== CHARACTER DETAILS ==========
|
||||
|
||||
-- Display name for the character.
|
||||
name VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Optional description of the character.
|
||||
maybe_description VARCHAR(512) DEFAULT NULL,
|
||||
|
||||
-- ========== MEDIA ==========
|
||||
|
||||
-- Avatar image (small/thumbnail).
|
||||
maybe_avatar_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Full-size reference image.
|
||||
maybe_full_image_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== CREATOR ==========
|
||||
|
||||
-- The user who created this character.
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- For abuse tracking.
|
||||
creator_ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP DEFAULT NULL,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY index_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_is_active (is_active),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -46,6 +46,7 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- * background_removal
|
||||
inference_category VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Enum: InferenceJobExternalThirdParty
|
||||
-- If the job is externally enqueued, this is the third-party service
|
||||
-- responsible for the job. Typically, we won't run an inference job for
|
||||
-- jobs being run on a third party.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS characters;
|
||||
@@ -0,0 +1,55 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE characters (
|
||||
-- Not used for anything except replication (private, not even used in queries)
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Public-facing "primary key" (PUBLIC)
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Whether the character has been fully created and is available for use.
|
||||
-- A character may exist in the database before its creation job completes.
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== CHARACTER DETAILS ==========
|
||||
|
||||
-- User's display name for the character.
|
||||
name VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Optional user description of the character.
|
||||
maybe_description VARCHAR(512) DEFAULT NULL,
|
||||
|
||||
-- ========== MEDIA ==========
|
||||
|
||||
-- Avatar image (cropped to face, ideally).
|
||||
maybe_avatar_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Full-size reference image.
|
||||
maybe_full_image_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== CREATOR ==========
|
||||
|
||||
-- The user who created this character.
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- For abuse tracking.
|
||||
creator_ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP DEFAULT NULL,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY index_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_is_active (is_active),
|
||||
KEY index_created_at (created_at),
|
||||
KEY index_deleted_at (deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS character_third_party_entities;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE character_third_party_entities (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Foreign key to the characters table.
|
||||
character_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Whether this third-party entity is active and usable.
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== THIRD PARTY DETAILS ==========
|
||||
|
||||
-- Which third-party system the character lives in (enum crate type).
|
||||
-- e.g. "kinovi", "runway", etc.
|
||||
third_party VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The identifier in the third-party system.
|
||||
-- e.g. Kinovi's "char_1774752056469_2wlxoq"
|
||||
third_party_id VARCHAR(64) NOT NULL,
|
||||
|
||||
-- Which types of models can use this character (enum crate type).
|
||||
-- e.g. "kinovi_seedance_2_pro", "kinovi_seedance_2_fast", etc.
|
||||
third_party_scopes VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The name used by the third party.
|
||||
-- Important for Kinovi since characters are referenced by name in prompts.
|
||||
third_party_name VARCHAR(255) NOT NULL,
|
||||
|
||||
-- An upstream third party of the third party (if any).
|
||||
-- e.g. Kinovi's assetId "asset-20260329104101-kk5kl"
|
||||
maybe_fourth_party_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- ========== JOB TRACKING ==========
|
||||
|
||||
-- Our job system token (if the character was created via a job).
|
||||
maybe_origin_job_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Third party's job system token (if any).
|
||||
maybe_origin_third_party_job_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP DEFAULT NULL,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY unique_third_party_id (third_party, third_party_id),
|
||||
KEY index_character_token (character_token),
|
||||
KEY index_third_party (third_party),
|
||||
KEY index_third_party_id (third_party_id),
|
||||
KEY index_is_active (is_active)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS character_creation_assets;
|
||||
@@ -0,0 +1,26 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Stores the media files (images) used as input when creating a character.
|
||||
CREATE TABLE character_creation_assets (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Foreign key to the characters table.
|
||||
character_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The media file used as a creation input (e.g. reference image).
|
||||
asset_media_file_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY index_character_token (character_token),
|
||||
KEY index_asset_media_file_token (asset_media_file_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS character_creation_prompts;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Stores prompts/descriptions used when creating a character.
|
||||
CREATE TABLE character_creation_prompts (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Foreign key to the characters table.
|
||||
character_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The prompt or description used during character creation.
|
||||
prompt TEXT DEFAULT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY index_character_token (character_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,26 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Request body for creating a character.
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateCharacterRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// The model to create the character for.
|
||||
pub model: CommonModelType,
|
||||
|
||||
/// The reference image media file token.
|
||||
pub image_media_token: MediaFileToken,
|
||||
}
|
||||
|
||||
/// Response body for creating a character.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CreateCharacterResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Path parameters for deleting a character.
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct DeleteCharacterPathInfo {
|
||||
pub character_token: CharacterToken,
|
||||
}
|
||||
|
||||
/// Response body for deleting a character.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DeleteCharacterResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Request body for editing a character.
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct EditCharacterRequest {
|
||||
/// The character to edit.
|
||||
pub token: CharacterToken,
|
||||
|
||||
/// Updated name (if changing).
|
||||
pub updated_name: Option<String>,
|
||||
|
||||
/// Updated description (if changing).
|
||||
pub updated_description: Option<String>,
|
||||
}
|
||||
|
||||
/// Response body for editing a character.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct EditCharacterResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Path parameters for getting a character.
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct GetCharacterPathInfo {
|
||||
pub character_token: CharacterToken,
|
||||
}
|
||||
|
||||
/// Response body for getting a character.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetCharacterResponse {
|
||||
pub success: bool,
|
||||
pub character: GetCharacterDetails,
|
||||
}
|
||||
|
||||
/// Full character details returned by the get endpoint.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetCharacterDetails {
|
||||
pub token: CharacterToken,
|
||||
|
||||
/// Which models this character can be used with.
|
||||
pub models: Vec<CommonModelType>,
|
||||
|
||||
pub name: String,
|
||||
|
||||
pub maybe_description: Option<String>,
|
||||
|
||||
pub maybe_avatar: Option<MediaLinks>,
|
||||
|
||||
pub maybe_full_image: Option<MediaLinks>,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Query string parameters for listing characters.
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct ListCharactersQuery {
|
||||
/// Optional cursor for pagination.
|
||||
pub cursor: Option<u64>,
|
||||
}
|
||||
|
||||
/// Response body for listing characters in the current session.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListCharactersResponse {
|
||||
pub success: bool,
|
||||
pub characters: Vec<ListCharactersEntry>,
|
||||
pub next_cursor: Option<u64>,
|
||||
}
|
||||
|
||||
/// A character entry in the list response.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListCharactersEntry {
|
||||
pub token: CharacterToken,
|
||||
|
||||
/// Which models this character can be used with.
|
||||
pub models: Vec<CommonModelType>,
|
||||
|
||||
pub name: String,
|
||||
|
||||
pub maybe_description: Option<String>,
|
||||
|
||||
pub maybe_avatar: Option<MediaLinks>,
|
||||
|
||||
pub maybe_full_image: Option<MediaLinks>,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod create_character;
|
||||
pub mod delete_character;
|
||||
pub mod edit_character;
|
||||
pub mod get_character;
|
||||
pub mod list_characters;
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod analytics;
|
||||
pub mod characters;
|
||||
pub mod common;
|
||||
pub mod credits;
|
||||
pub mod generate;
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(crate) enum TokenPrefix {
|
||||
BatchGeneration,
|
||||
BetaKey,
|
||||
BrowserSessionLog,
|
||||
Character,
|
||||
Comment,
|
||||
DownloadJob,
|
||||
EmailSenderJob,
|
||||
@@ -111,6 +112,7 @@ impl PrefixGenerator for TokenPrefix {
|
||||
Self::BatchGeneration => "batch_g_",
|
||||
Self::BetaKey => "beta_key_",
|
||||
Self::BrowserSessionLog => "bsl_",
|
||||
Self::Character => "character_",
|
||||
Self::Comment => "comment_",
|
||||
Self::DownloadJob => "jdown_", // NB: Previously "JGUP:"
|
||||
Self::EmailSenderJob => "email_job_",
|
||||
@@ -339,6 +341,7 @@ mod tests {
|
||||
|| prefix == "email_job_"
|
||||
|| prefix == "pw_reset_"
|
||||
|| prefix == "tts_task_"
|
||||
|| prefix == "app_session_"
|
||||
{
|
||||
// TODO/FIXME: I'm too tired at 5AM to replacen from the left. Make this test valid.
|
||||
// These tokens are from the AIChatBot sidecar, so asserting their validity is less important.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::prefixes::TokenPrefix;
|
||||
|
||||
/// The primary key for Characters
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "database", sqlx(transparent))]
|
||||
pub struct CharacterToken(pub String);
|
||||
|
||||
impl_string_token!(CharacterToken);
|
||||
impl_mysql_token_from_row!(CharacterToken);
|
||||
impl_crockford_generator!(CharacterToken, 32usize, TokenPrefix::Character, CrockfordLower);
|
||||
@@ -6,6 +6,7 @@ pub mod audit_logs;
|
||||
pub mod batch_generations;
|
||||
pub mod beta_keys;
|
||||
pub mod browser_session_logs;
|
||||
pub mod characters;
|
||||
pub mod comments;
|
||||
pub mod email_sender_job_token;
|
||||
pub mod firehose_entries;
|
||||
|
||||
@@ -279,6 +279,11 @@ use artcraft_api_defs::subscriptions::get_session_subscription::*;
|
||||
use artcraft_api_defs::media_file::list_batch_generated_media_files::*;
|
||||
// Handler modules with locally-defined types
|
||||
use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::*;
|
||||
use artcraft_api_defs::characters::create_character::*;
|
||||
use artcraft_api_defs::characters::delete_character::*;
|
||||
use artcraft_api_defs::characters::edit_character::*;
|
||||
use artcraft_api_defs::characters::get_character::*;
|
||||
use artcraft_api_defs::characters::list_characters::*;
|
||||
use artcraft_api_defs::moderation::alerts::moderation_send_alert::*;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::*;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::*;
|
||||
@@ -476,6 +481,12 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::enqueue_infer_seed_vc_handler,
|
||||
// TTS
|
||||
crate::http_server::endpoints::tts::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler,
|
||||
// Characters
|
||||
crate::http_server::endpoints::characters::list_characters_handler::list_characters_handler,
|
||||
crate::http_server::endpoints::characters::create_character_handler::create_character_handler,
|
||||
crate::http_server::endpoints::characters::get_character_handler::get_character_handler,
|
||||
crate::http_server::endpoints::characters::edit_character_handler::edit_character_handler,
|
||||
crate::http_server::endpoints::characters::delete_character_handler::delete_character_handler,
|
||||
// Omni Gen
|
||||
crate::http_server::endpoints::omni_gen::cost::image::omni_gen_image_cost_handler::omni_gen_image_cost_handler,
|
||||
crate::http_server::endpoints::omni_gen::cost::video::omni_gen_video_cost_handler::omni_gen_video_cost_handler,
|
||||
@@ -693,6 +704,17 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ModeratorUserLookupRequest,
|
||||
ModeratorUserLookupSuccessResponse,
|
||||
ModeratorUserLookupUserDetails,
|
||||
CreateCharacterRequest,
|
||||
CreateCharacterResponse,
|
||||
DeleteCharacterPathInfo,
|
||||
DeleteCharacterResponse,
|
||||
EditCharacterRequest,
|
||||
EditCharacterResponse,
|
||||
GetCharacterDetails,
|
||||
GetCharacterPathInfo,
|
||||
GetCharacterResponse,
|
||||
ListCharactersEntry,
|
||||
ListCharactersResponse,
|
||||
OmniGenImageCostAndGenerateRequest,
|
||||
OmniGenImageCostResponse,
|
||||
OmniGenImageGenerateResponse,
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
|
||||
use artcraft_api_defs::characters::create_character::{CreateCharacterRequest, CreateCharacterResponse};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Create a new character.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Characters",
|
||||
path = "/v1/character/create",
|
||||
request_body = CreateCharacterRequest,
|
||||
responses(
|
||||
(status = 200, description = "Success", body = CreateCharacterResponse),
|
||||
(status = 400, description = "Bad input"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 402, description = "Payment required"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn create_character_handler(
|
||||
_http_request: HttpRequest,
|
||||
_request: Json<CreateCharacterRequest>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<CreateCharacterResponse>, CommonWebError> {
|
||||
todo!()
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
|
||||
use artcraft_api_defs::characters::delete_character::{DeleteCharacterPathInfo, DeleteCharacterResponse};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Delete a character.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
tag = "Characters",
|
||||
path = "/v1/character/{character_token}",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = DeleteCharacterResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
params(
|
||||
("character_token" = CharacterToken, Path, description = "Character token"),
|
||||
)
|
||||
)]
|
||||
pub async fn delete_character_handler(
|
||||
_http_request: HttpRequest,
|
||||
_path: Path<DeleteCharacterPathInfo>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<DeleteCharacterResponse>, CommonWebError> {
|
||||
todo!()
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
|
||||
use artcraft_api_defs::characters::edit_character::{EditCharacterRequest, EditCharacterResponse};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Edit a character's name or description.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Characters",
|
||||
path = "/v1/character/edit",
|
||||
request_body = EditCharacterRequest,
|
||||
responses(
|
||||
(status = 200, description = "Success", body = EditCharacterResponse),
|
||||
(status = 400, description = "Bad input"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn edit_character_handler(
|
||||
_http_request: HttpRequest,
|
||||
_request: Json<EditCharacterRequest>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<EditCharacterResponse>, CommonWebError> {
|
||||
todo!()
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::characters::get_character::{GetCharacterDetails, GetCharacterPathInfo, GetCharacterResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Get a character by token.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Characters",
|
||||
path = "/v1/character/{character_token}",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GetCharacterResponse),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
params(
|
||||
("character_token" = CharacterToken, Path, description = "Character token"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_character_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetCharacterPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<GetCharacterResponse>, CommonWebError> {
|
||||
// TODO: Replace with real character query. This is test data using a real media file (Ernest).
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
let ernest_media_token = MediaFileToken::new_from_str("m_1997908e4tcbaj34wa0s5bvr4y4szt");
|
||||
|
||||
let maybe_links = match get_media_file(&ernest_media_token, false, &server_state.mysql_pool).await {
|
||||
Ok(Some(record)) => {
|
||||
let path = MediaFileBucketPath::from_object_hash(
|
||||
&record.public_bucket_directory_hash,
|
||||
record.maybe_public_bucket_prefix.as_deref(),
|
||||
record.maybe_public_bucket_extension.as_deref(),
|
||||
);
|
||||
Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path))
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("Ernest media file not found in database");
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to look up Ernest media file: {:?}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let character = GetCharacterDetails {
|
||||
token: path.character_token.clone(),
|
||||
models: vec![CommonModelType::Seedance2p0],
|
||||
name: "Ernest".to_string(),
|
||||
maybe_description: Some("Ernest P. Worrell".to_string()),
|
||||
maybe_avatar: maybe_links.clone(),
|
||||
maybe_full_image: maybe_links,
|
||||
};
|
||||
|
||||
Ok(Json(GetCharacterResponse {
|
||||
success: true,
|
||||
character,
|
||||
}))
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::characters::list_characters::{ListCharactersEntry, ListCharactersQuery, ListCharactersResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// List characters for the current session.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Characters",
|
||||
path = "/v1/characters/session",
|
||||
params(ListCharactersQuery),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListCharactersResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn list_characters_handler(
|
||||
http_request: HttpRequest,
|
||||
_query: Query<ListCharactersQuery>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListCharactersResponse>, CommonWebError> {
|
||||
// TODO: Replace with real character queries. This is test data using real media files.
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
struct TestCharacter {
|
||||
token: &'static str,
|
||||
name: &'static str,
|
||||
description: &'static str,
|
||||
media_token: &'static str,
|
||||
}
|
||||
|
||||
let test_characters = vec![
|
||||
TestCharacter { token: "character_mock_ernest_001", name: "Ernest", description: "Ernest P. Worrell", media_token: "m_1997908e4tcbaj34wa0s5bvr4y4szt" },
|
||||
TestCharacter { token: "character_mock_juno_002", name: "Juno", description: "Juno the corgi", media_token: "m_m1bz02z1kkzanxy6rb4vk1kvq9de9g" },
|
||||
TestCharacter { token: "character_mock_mochi_003", name: "Mochi", description: "Mochi with glasses", media_token: "m_5y81rcnpnrbey55da6cc2s9z4d2f45" },
|
||||
];
|
||||
|
||||
let media_tokens: Vec<MediaFileToken> = test_characters.iter()
|
||||
.map(|c| MediaFileToken::new_from_str(c.media_token))
|
||||
.collect();
|
||||
|
||||
let media_records = batch_get_media_files_by_tokens(&server_state.mysql_pool, &media_tokens, false)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to look up media files for test characters: {:?}", err);
|
||||
vec![]
|
||||
});
|
||||
|
||||
let characters = test_characters.iter().map(|tc| {
|
||||
let maybe_links = media_records.iter()
|
||||
.find(|r| r.token.as_str() == tc.media_token)
|
||||
.map(|r| {
|
||||
let path = MediaFileBucketPath::from_object_hash(
|
||||
&r.public_bucket_directory_hash,
|
||||
r.maybe_public_bucket_prefix.as_deref(),
|
||||
r.maybe_public_bucket_extension.as_deref(),
|
||||
);
|
||||
MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path)
|
||||
});
|
||||
|
||||
ListCharactersEntry {
|
||||
token: CharacterToken::new_from_str(tc.token),
|
||||
models: vec![CommonModelType::Seedance2p0],
|
||||
name: tc.name.to_string(),
|
||||
maybe_description: Some(tc.description.to_string()),
|
||||
maybe_avatar: maybe_links.clone(),
|
||||
maybe_full_image: maybe_links,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListCharactersResponse {
|
||||
success: true,
|
||||
characters,
|
||||
next_cursor: None,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod create_character_handler;
|
||||
pub mod delete_character_handler;
|
||||
pub mod edit_character_handler;
|
||||
pub mod get_character_handler;
|
||||
pub mod list_characters_handler;
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod analytics;
|
||||
pub mod app_state;
|
||||
pub mod beta_keys;
|
||||
pub mod characters;
|
||||
pub mod comments;
|
||||
pub mod credits;
|
||||
pub mod download_job;
|
||||
|
||||
+2
@@ -1,4 +1,5 @@
|
||||
use crate::http_server::routes::application_routes::analytics_routes::add_analytics_routes;
|
||||
use crate::http_server::routes::application_routes::character_routes::add_character_routes;
|
||||
use crate::http_server::routes::application_routes::comments_routes::add_comments_routes;
|
||||
use crate::http_server::routes::application_routes::credits_routes::add_credits_routes;
|
||||
use crate::http_server::routes::application_routes::featured_item_routes::add_featured_item_routes;
|
||||
@@ -42,6 +43,7 @@ where
|
||||
// Artcraft surface area
|
||||
app = add_generate_routes(app); // /v1/generate/...
|
||||
app = add_omni_gen_routes(app); // /v1/omni_gen/...
|
||||
app = add_character_routes(app); // /v1/character/... and /v1/characters/...
|
||||
app = add_webhook_routes(app); // /v1/webhooks/... (fal)
|
||||
|
||||
// Legacy FakeYou surface area that might be useful again one day
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
use crate::http_server::endpoints::characters::create_character_handler::create_character_handler;
|
||||
use crate::http_server::endpoints::characters::delete_character_handler::delete_character_handler;
|
||||
use crate::http_server::endpoints::characters::edit_character_handler::edit_character_handler;
|
||||
use crate::http_server::endpoints::characters::get_character_handler::get_character_handler;
|
||||
use crate::http_server::endpoints::characters::list_characters_handler::list_characters_handler;
|
||||
|
||||
pub fn add_character_routes<T, B>(app: App<T>) -> App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
app
|
||||
// List characters for the current session
|
||||
.service(web::resource("/v1/characters/session")
|
||||
.route(web::get().to(list_characters_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
// Create a new character
|
||||
.service(web::resource("/v1/character/create")
|
||||
.route(web::post().to(create_character_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
// Edit a character
|
||||
.service(web::resource("/v1/character/edit")
|
||||
.route(web::post().to(edit_character_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
// Get or delete a character by token
|
||||
.service(web::resource("/v1/character/{character_token}")
|
||||
.route(web::get().to(get_character_handler))
|
||||
.route(web::delete().to(delete_character_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod analytics_routes;
|
||||
mod character_routes;
|
||||
mod comments_routes;
|
||||
mod credits_routes;
|
||||
mod featured_item_routes;
|
||||
|
||||
@@ -10,6 +10,27 @@ art_craft
|
||||
artcraft
|
||||
hanashi
|
||||
|
||||
# Models
|
||||
chatgpt
|
||||
flux
|
||||
gemini
|
||||
gpt
|
||||
grok
|
||||
grokimagine
|
||||
ltx
|
||||
midjourney
|
||||
nanobanana
|
||||
nanobanana2
|
||||
nanobananapro
|
||||
seedance
|
||||
seedance2
|
||||
seedream
|
||||
sora
|
||||
veo
|
||||
veo2
|
||||
veo3
|
||||
wan
|
||||
|
||||
# Possible uses
|
||||
# See https://www.wired.com/story/by-seizing-music-elon-musk-shows-he-doesnt-know-what-made-twitter-good/
|
||||
3d
|
||||
@@ -282,6 +303,7 @@ category
|
||||
channel
|
||||
channels
|
||||
character
|
||||
characters
|
||||
checkout_cancel
|
||||
checkout_success
|
||||
clone
|
||||
@@ -319,6 +341,7 @@ search
|
||||
signup
|
||||
stream
|
||||
studio
|
||||
studios
|
||||
terms
|
||||
testing
|
||||
text_to_image
|
||||
@@ -621,6 +644,7 @@ upvote
|
||||
amazon
|
||||
anthropic
|
||||
apple
|
||||
bytedance
|
||||
cash
|
||||
cashapp
|
||||
epic
|
||||
|
||||
Reference in New Issue
Block a user