mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 15:55:41 -05:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf2aff205b | ||
|
|
5b2b70e1fc | ||
|
|
de2592b1ae | ||
|
|
b625d5ff79 | ||
|
|
d8fd0bfe64 | ||
|
|
3cc02ae7bf | ||
|
|
2c943b61ab | ||
|
|
d92e6fc75c | ||
|
|
c0ca274168 | ||
|
|
27cc125089 | ||
|
|
4ce3f240c7 | ||
|
|
3a2e3b30ec | ||
|
|
82c0a7a4d9 |
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_log_level = ?,\n maybe_creator_user_token = ?,\n maybe_ip_address = ?,\n maybe_url = ?,\n message = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "32a8d1cfec89ba2056463a33ddc501a8a99283c7c00982a7fa79f7ff382c9b22"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO voice_clone_requests\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n maybe_user_token = ?,\n email_address = ?,\n discord_username = ?,\n\n is_for_private_use = ?,\n is_for_public_use = ?,\n\n is_for_studio = ?,\n is_for_twitch_tts = ?,\n is_for_api_use = ?,\n is_for_music = ?,\n is_for_games = ?,\n is_for_other = ?,\n optional_notes_on_use = ?,\n\n is_own_voice = ?,\n is_third_party_voice = ?,\n optional_notes_on_subject = ?,\n\n has_clean_audio_recordings = ?,\n has_good_microphone = ?,\n\n optional_questions = ?,\n optional_extra_comments = ?,\n\n ip_address_creation = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 22
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4cea10944b8869419ecdfea30c58f49a87c090b9083078043177c33737ecb949"
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n COUNT(distinct token) as record_count,\n NOW() as present_time\nFROM\n(\n SELECT token\n FROM tts_inference_jobs\n WHERE status = \"started\"\n AND created_at > (CURDATE() - INTERVAL 5 MINUTE)\nUNION\n SELECT token\n FROM tts_inference_jobs\n WHERE status IN (\"pending\", \"attempt_failed\")\n) as t\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "record_count",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "present_time",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6098826a8ada75442be5b7ac1d0199694e01c5959666f59986cd8b2ed6f51882"
|
||||
}
|
||||
-256
@@ -1,256 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token,\n\n maybe_user_token,\n email_address,\n discord_username,\n\n is_for_private_use,\n is_for_public_use,\n\n is_for_studio,\n is_for_twitch_tts,\n is_for_api_use,\n is_for_music,\n is_for_games,\n is_for_other,\n optional_notes_on_use,\n\n is_own_voice,\n is_third_party_voice,\n optional_notes_on_subject,\n\n has_clean_audio_recordings,\n has_good_microphone,\n\n optional_questions,\n optional_extra_comments,\n\n created_at,\n updated_at\nFROM voice_clone_requests\nWHERE\n token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "maybe_user_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "discord_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_for_private_use",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_for_public_use",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_for_studio",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_for_twitch_tts",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_for_api_use",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "is_for_music",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "is_for_games",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "is_for_other",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "optional_notes_on_use",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "BLOB | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "is_own_voice",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_third_party_voice",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "optional_notes_on_subject",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "has_clean_audio_recordings",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "has_good_microphone",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "optional_questions",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "BLOB | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "optional_extra_comments",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "BLOB | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "922a7dbcfa24a2c5901d0453c229b066b3b4eea2c73fa533651bb27365f17be0"
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n id as `id: u64`,\n event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n message,\n created_at\nFROM debug_logs\nWHERE event_token = ?\nORDER BY id ASC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 96
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "message",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a62b88fec40a11952cc199383090ea338f97091e8bfc6b68aad67416685889af"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_creator_user_token = ?,\n message = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a9745ecf60b564ab096ee1dbe124221b5f288a4ae38e788e19bbd359db0b580f"
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n e.maybe_user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `gross_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN e.amount_usd_cents < 0 THEN -e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `refund_usd_cents!: u64`,\n CAST(SUM(e.amount_usd_cents) AS SIGNED) AS `net_spend_usd_cents!: i64`,\n CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN 1 ELSE 0 END) AS UNSIGNED) AS `payment_count!: u64`,\n CAST(COALESCE(SUM(e.maybe_credits_granted), 0) AS UNSIGNED) AS `credits_granted!: u64`\nFROM user_spend_events e\nJOIN users u ON u.token = e.maybe_user_token\nWHERE e.maybe_user_token IS NOT NULL\n AND e.is_production = TRUE\n AND e.payment_occurred_at >= ?\n AND (? IS NULL OR e.payments_namespace = ?)\nGROUP BY e.maybe_user_token, u.username, u.display_name, u.email_gravatar_hash\nORDER BY SUM(e.amount_usd_cents) DESC, e.maybe_user_token ASC\nLIMIT ? OFFSET ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_token!: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username!",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "display_name!",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email_gravatar_hash!",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "gross_spend_usd_cents!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "refund_usd_cents!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "net_spend_usd_cents!: i64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "payment_count!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "credits_granted!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c2ee41eb6bc1a6a551411998155423142411010f298f0815eacc7d9c3138bdd8"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.maybe_ip_address,\n d.maybe_url,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.maybe_creator_user_token = ?\n AND d.id < ?\nORDER BY d.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 96
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "message",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_user_display_name?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_user_username?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_user_gravatar_hash?",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d1ade7fcfd3efac1cd3163c392110327bb73edb74fa0d9693fee5feddb8a3223"
|
||||
}
|
||||
-256
@@ -1,256 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token,\n\n maybe_user_token,\n email_address,\n discord_username,\n\n is_for_private_use,\n is_for_public_use,\n\n is_for_studio,\n is_for_twitch_tts,\n is_for_api_use,\n is_for_music,\n is_for_games,\n is_for_other,\n optional_notes_on_use,\n\n is_own_voice,\n is_third_party_voice,\n optional_notes_on_subject,\n\n has_clean_audio_recordings,\n has_good_microphone,\n\n optional_questions,\n optional_extra_comments,\n\n created_at,\n updated_at\nFROM voice_clone_requests\nWHERE\n maybe_user_token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "maybe_user_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "discord_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_for_private_use",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_for_public_use",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_for_studio",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_for_twitch_tts",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_for_api_use",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "is_for_music",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "is_for_games",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "is_for_other",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "optional_notes_on_use",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "BLOB | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "is_own_voice",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_third_party_voice",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "optional_notes_on_subject",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "has_clean_audio_recordings",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "has_good_microphone",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "optional_questions",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "BLOB | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "optional_extra_comments",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "BLOB | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e5ef2bda28221a570da6dcfa73e76cae9910a0b4ee53005a87176ecf1be73dd7"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.maybe_ip_address,\n d.maybe_url,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.event_token = ?\nORDER BY d.id ASC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 96
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "message",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_user_display_name?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_user_username?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_user_gravatar_hash?",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e9a149ffc963a59019fdd69bd53128fad946856d432108bf9b923dfeaf2442c5"
|
||||
}
|
||||
Generated
+15
@@ -3621,6 +3621,7 @@ dependencies = [
|
||||
"env_logger 0.11.9",
|
||||
"log",
|
||||
"storyteller_root",
|
||||
"trace_id",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -6766,6 +6767,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"env_logger 0.11.9",
|
||||
"log",
|
||||
"trace_id",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -8286,6 +8288,7 @@ dependencies = [
|
||||
"rootly_client",
|
||||
"sha2 0.11.0-rc.5",
|
||||
"tokio",
|
||||
"trace_id",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -11678,6 +11681,7 @@ dependencies = [
|
||||
"tokens",
|
||||
"tokio",
|
||||
"toml 1.0.7+spec-1.1.0",
|
||||
"trace_id",
|
||||
"tts_common",
|
||||
"url",
|
||||
"url_config",
|
||||
@@ -13316,6 +13320,17 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "trace_id"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
|
||||
@@ -130,6 +130,7 @@ members = [
|
||||
"crates/schema/public/enums",
|
||||
"crates/schema/public/reusable_types", # TODO(bt, 2022-12-21): Deprecate
|
||||
"crates/schema/public/tokens",
|
||||
"crates/schema/trace_id",
|
||||
|
||||
# Services: API servers, jobs, workers, etc.
|
||||
"crates/service/job/gmicloud_job",
|
||||
@@ -293,6 +294,7 @@ subprocess_common = { path = "crates/lib/deprecated/subprocess_common" }
|
||||
test_data = { path = "crates/testing/test_data" }
|
||||
test_utils = { path = "crates/testing/test_utils" }
|
||||
tokens = { path = "crates/schema/public/tokens" }
|
||||
trace_id = { path = "crates/schema/trace_id" }
|
||||
url_utils = { path = "crates/lib/url_utils" }
|
||||
users = { path = "crates/lib/users" }
|
||||
uuid_utils = { path = "crates/lib/uuid_utils" }
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
DROP COLUMN maybe_log_level;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
ADD COLUMN maybe_log_level VARCHAR(16) DEFAULT NULL;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
DROP INDEX index_maybe_log_level;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
ADD INDEX index_maybe_log_level (maybe_log_level);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
DROP COLUMN maybe_ip_address;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
ADD COLUMN maybe_ip_address VARCHAR(40) DEFAULT NULL;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
DROP COLUMN maybe_url;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
ADD COLUMN maybe_url VARCHAR(255) DEFAULT NULL;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
DROP INDEX idx_creator_created_at,
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Composite index for the library / user profile list queries:
|
||||
-- WHERE maybe_creator_user_token = ? ... ORDER BY created_at DESC LIMIT n
|
||||
--
|
||||
-- Without this, the optimizer picks an index-merge intersection against huge
|
||||
-- low-selectivity indexes (e.g. index_user_deleted_at, ~4.75M NULL entries),
|
||||
-- scanning millions of index records per page load.
|
||||
--
|
||||
-- NB: ADD INDEX is online DDL (ALGORITHM=INPLACE, LOCK=NONE) — reads and writes
|
||||
-- continue during the build. Expect a few minutes of elevated I/O on a table
|
||||
-- this size (~8.5M rows).
|
||||
ALTER TABLE media_files
|
||||
ADD INDEX idx_creator_created_at (maybe_creator_user_token, created_at),
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
@@ -13,9 +13,18 @@ CREATE TABLE debug_logs (
|
||||
-- The type of debug log entry.
|
||||
debug_log_type VARCHAR(24) NOT NULL,
|
||||
|
||||
-- The severity level of the log entry (info, warn, error, debug, trace).
|
||||
maybe_log_level VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- The user who created the log entry (if any).
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- The client IP address of the request (if any). Wide enough for IPv6.
|
||||
maybe_ip_address VARCHAR(40) DEFAULT NULL,
|
||||
|
||||
-- The request URL (if any). Truncated to 255 characters on insert.
|
||||
maybe_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- The log message body.
|
||||
message MEDIUMTEXT NOT NULL,
|
||||
|
||||
@@ -25,6 +34,7 @@ CREATE TABLE debug_logs (
|
||||
PRIMARY KEY (id),
|
||||
KEY index_event_token (event_token),
|
||||
KEY index_debug_log_type (debug_log_type),
|
||||
KEY index_maybe_log_level (maybe_log_level),
|
||||
KEY index_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
|
||||
@@ -364,6 +364,7 @@ CREATE TABLE media_files (
|
||||
KEY index_user_deleted_at (user_deleted_at),
|
||||
KEY index_mod_deleted_at (mod_deleted_at),
|
||||
KEY index_media_files_created_at (created_at),
|
||||
KEY index_media_files_updated_at (updated_at)
|
||||
KEY index_media_files_updated_at (updated_at),
|
||||
KEY idx_creator_created_at (maybe_creator_user_token, created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// A single debug log row.
|
||||
/// Shared by the moderation debug-log list endpoints.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationDebugLogEntry {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_log_level: Option<DebugLogLevel>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
/// The client IP address of the request (if recorded).
|
||||
pub maybe_ip_address: Option<String>,
|
||||
/// The request URL (if recorded; truncated to 255 chars).
|
||||
pub maybe_url: Option<String>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// The creator user, joined in when the row has a user token.
|
||||
pub maybe_user: Option<ModerationDebugLogUser>,
|
||||
}
|
||||
|
||||
/// The creator user of a debug log row.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationDebugLogUser {
|
||||
pub user_token: UserToken,
|
||||
pub display_name: String,
|
||||
pub username: String,
|
||||
pub gravatar_hash: String,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod debug_log_entry;
|
||||
pub mod moderation_list_all_debug_logs;
|
||||
pub mod moderation_list_debug_logs_for_token;
|
||||
pub mod moderation_list_debug_logs_for_user;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use enums::error::enum_error::EnumError;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogUser;
|
||||
|
||||
pub const MODERATION_LIST_ALL_DEBUG_LOGS_PATH: &str = "/v1/moderation/debug_logs/list_all";
|
||||
|
||||
// ── Query params ──
|
||||
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
pub struct ModerationListAllDebugLogsQueryParams {
|
||||
/// Optional comma-separated list of severity levels to include,
|
||||
/// eg. `severities=error,warn`. Omit to include all rows.
|
||||
pub severities: Option<String>,
|
||||
|
||||
/// Cursor for pagination. Pass the `next_cursor` from a previous response.
|
||||
pub cursor: Option<u64>,
|
||||
|
||||
/// Max rows per page (default 50, max 200).
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl ModerationListAllDebugLogsQueryParams {
|
||||
/// Parse the comma-separated `severities` parameter.
|
||||
/// Returns `None` when the parameter is absent or empty.
|
||||
pub fn parsed_severities(&self) -> Result<Option<Vec<DebugLogLevel>>, EnumError> {
|
||||
let raw = match self.severities.as_deref() {
|
||||
Some(value) if !value.trim().is_empty() => value,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let levels = raw
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|part| !part.is_empty())
|
||||
.map(DebugLogLevel::from_str)
|
||||
.collect::<Result<Vec<DebugLogLevel>, EnumError>>()?;
|
||||
|
||||
Ok(if levels.is_empty() { None } else { Some(levels) })
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationListAllDebugLogsSuccessResponse {
|
||||
pub success: bool,
|
||||
/// Most recent first.
|
||||
pub debug_logs: Vec<ModerationListAllDebugLogsEntry>,
|
||||
/// Cursor for the next page. `None` if there are no more results.
|
||||
pub next_cursor: Option<u64>,
|
||||
}
|
||||
|
||||
/// A debug log row with the creator user joined in (when one exists).
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationListAllDebugLogsEntry {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_log_level: Option<DebugLogLevel>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
/// The client IP address of the request (if recorded).
|
||||
pub maybe_ip_address: Option<String>,
|
||||
/// The request URL (if recorded; truncated to 255 chars).
|
||||
pub maybe_url: Option<String>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub maybe_user: Option<ModerationDebugLogUser>,
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
|
||||
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry;
|
||||
|
||||
pub const MODERATION_LIST_DEBUG_LOGS_FOR_TOKEN_PATH: &str = "/v1/moderation/debug_logs/list/{token}";
|
||||
|
||||
// ── Path params ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModerationListDebugLogsForTokenPathInfo {
|
||||
pub token: DebugLogEventToken,
|
||||
}
|
||||
|
||||
// ── Query params ──
|
||||
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
pub struct ModerationListDebugLogsForTokenQueryParams {
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationListDebugLogsForTokenSuccessResponse {
|
||||
pub success: bool,
|
||||
pub debug_logs: Vec<ModerationDebugLogEntry>,
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry;
|
||||
|
||||
pub const MODERATION_LIST_DEBUG_LOGS_FOR_USER_PATH: &str = "/v1/moderation/debug_logs/user_list/{user_token}";
|
||||
|
||||
// ── Path params ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModerationListDebugLogsForUserPathInfo {
|
||||
pub user_token: UserToken,
|
||||
}
|
||||
|
||||
// ── Query params ──
|
||||
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
pub struct ModerationListDebugLogsForUserQueryParams {
|
||||
/// Cursor for pagination. Pass the `next_cursor` from a previous response.
|
||||
pub cursor: Option<u64>,
|
||||
|
||||
/// Max rows per page (default 50, max 200).
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationListDebugLogsForUserSuccessResponse {
|
||||
pub success: bool,
|
||||
/// Most recent first.
|
||||
pub debug_logs: Vec<ModerationDebugLogEntry>,
|
||||
/// Cursor for the next page. `None` if there are no more results.
|
||||
pub next_cursor: Option<u64>,
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod alerts;
|
||||
pub mod debug_logs;
|
||||
pub mod top_spenders;
|
||||
pub mod jobs;
|
||||
pub mod user;
|
||||
pub mod user_referrals;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::users::UserToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
pub const MODERATOR_LIST_TOP_SPENDERS_PATH: &str =
|
||||
"/v1/moderation/top_spenders/list";
|
||||
|
||||
/// Rolling aggregation window for the top spenders list.
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, ToSchema)]
|
||||
pub enum TopSpendersWindow {
|
||||
#[serde(rename = "24h")]
|
||||
TwentyFourHours,
|
||||
#[serde(rename = "7d")]
|
||||
SevenDays,
|
||||
#[serde(rename = "14d")]
|
||||
FourteenDays,
|
||||
#[serde(rename = "30d")]
|
||||
ThirtyDays,
|
||||
#[serde(rename = "60d")]
|
||||
SixtyDays,
|
||||
#[serde(rename = "90d")]
|
||||
NinetyDays,
|
||||
#[serde(rename = "180d")]
|
||||
OneHundredEightyDays,
|
||||
#[serde(rename = "365d")]
|
||||
ThreeHundredSixtyFiveDays,
|
||||
}
|
||||
|
||||
impl TopSpendersWindow {
|
||||
/// Window length in hours.
|
||||
pub fn as_hours(self) -> i64 {
|
||||
match self {
|
||||
TopSpendersWindow::TwentyFourHours => 24,
|
||||
TopSpendersWindow::SevenDays => 7 * 24,
|
||||
TopSpendersWindow::FourteenDays => 14 * 24,
|
||||
TopSpendersWindow::ThirtyDays => 30 * 24,
|
||||
TopSpendersWindow::SixtyDays => 60 * 24,
|
||||
TopSpendersWindow::NinetyDays => 90 * 24,
|
||||
TopSpendersWindow::OneHundredEightyDays => 180 * 24,
|
||||
TopSpendersWindow::ThreeHundredSixtyFiveDays => 365 * 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct ModeratorListTopSpendersQueryParams {
|
||||
/// Aggregation window: `24h`, `7d`, `14d`, `30d` (default), `60d`, `90d`,
|
||||
/// `180d`, or `365d`.
|
||||
pub window: Option<TopSpendersWindow>,
|
||||
/// Row offset for pagination (page size is fixed at 100). Pass `next_offset` from the previous page.
|
||||
pub offset: Option<u64>,
|
||||
/// Optional namespace filter (e.g. `artcraft`). Omit to return all namespaces.
|
||||
pub payments_namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModeratorListTopSpendersResponse {
|
||||
pub success: bool,
|
||||
/// The window the results were aggregated over (echoed back, post-default).
|
||||
pub window: TopSpendersWindow,
|
||||
pub spenders: Vec<TopSpenderEntry>,
|
||||
/// Offset for the next page, or `None` if this was the last page.
|
||||
pub maybe_next_offset: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct TopSpenderEntry {
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_gravatar_hash: String,
|
||||
/// Sum of positive amounts in the window.
|
||||
pub gross_spend_usd_cents: u64,
|
||||
/// Refund/chargeback magnitude in the window (positive number).
|
||||
pub refund_usd_cents: u64,
|
||||
/// Gross minus refunds. Can be negative if refunds outweigh purchases.
|
||||
pub net_spend_usd_cents: i64,
|
||||
/// Number of positive money events in the window.
|
||||
pub payment_count: u64,
|
||||
/// Credits granted in the window.
|
||||
pub credits_granted: u64,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod list;
|
||||
@@ -82,6 +82,7 @@ fn do_build_cors_config(is_production: bool) -> Cors {
|
||||
actix_http::header::ACCESS_CONTROL_ALLOW_CREDENTIALS, // https://stackoverflow.com/a/46412839
|
||||
actix_http::header::HeaderName::from_static("x-requested-with"), // Tabulator Ajax sends
|
||||
actix_http::header::HeaderName::from_static("session"), // Custom header sent by Three.js Storyteller Studio
|
||||
actix_http::header::HeaderName::from_static("x-artcraft-version"), // ArtCraft client app release, surfaced in error alerts
|
||||
])
|
||||
.max_age(3600)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-
|
||||
# External
|
||||
dotenv.workspace = true
|
||||
env_logger = "0.11.3"
|
||||
trace_id.workspace = true
|
||||
log = "0.4.14"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -29,6 +29,37 @@ pub fn init_env_logger(default_if_absent: Option<&str>) {
|
||||
env::set_var(ENV_RUST_LOG, default_log_level);
|
||||
}
|
||||
|
||||
env_logger::init();
|
||||
// Custom format so that log lines emitted while serving an HTTP request
|
||||
// automatically carry the request's trace id (see the `trace_id` crate and
|
||||
// the `TraceIdMiddleware` in storyteller-web). Outside of a request scope
|
||||
// the trace segment is simply omitted.
|
||||
env_logger::Builder::from_env(env_logger::Env::default())
|
||||
.format(|buf, record| {
|
||||
use std::io::Write;
|
||||
|
||||
let timestamp = buf.timestamp();
|
||||
let level_style = buf.default_level_style(record.level());
|
||||
|
||||
match trace_id::current_trace_id() {
|
||||
Some(trace) => writeln!(
|
||||
buf,
|
||||
"[{} {level_style}{}{level_style:#} {} {}] {}",
|
||||
timestamp,
|
||||
record.level(),
|
||||
record.target(),
|
||||
trace,
|
||||
record.args(),
|
||||
),
|
||||
None => writeln!(
|
||||
buf,
|
||||
"[{} {level_style}{}{level_style:#} {}] {}",
|
||||
timestamp,
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
),
|
||||
}
|
||||
})
|
||||
.init();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# External
|
||||
env_logger = "0.11.3"
|
||||
trace_id.workspace = true
|
||||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -68,6 +68,37 @@ pub fn init_env_logger(default_if_absent: Option<&str>) {
|
||||
env::set_var(ENV_RUST_LOG, default_log_level);
|
||||
}
|
||||
|
||||
env_logger::init();
|
||||
// Custom format so that log lines emitted while serving an HTTP request
|
||||
// automatically carry the request's trace id (see the `trace_id` crate and
|
||||
// the `TraceIdMiddleware` in storyteller-web). Outside of a request scope
|
||||
// the trace segment is simply omitted.
|
||||
env_logger::Builder::from_env(env_logger::Env::default())
|
||||
.format(|buf, record| {
|
||||
use std::io::Write;
|
||||
|
||||
let timestamp = buf.timestamp();
|
||||
let level_style = buf.default_level_style(record.level());
|
||||
|
||||
match trace_id::current_trace_id() {
|
||||
Some(trace) => writeln!(
|
||||
buf,
|
||||
"[{} {level_style}{}{level_style:#} {} {}] {}",
|
||||
timestamp,
|
||||
record.level(),
|
||||
record.target(),
|
||||
trace,
|
||||
record.args(),
|
||||
),
|
||||
None => writeln!(
|
||||
buf,
|
||||
"[{} {level_style}{}{level_style:#} {}] {}",
|
||||
timestamp,
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
),
|
||||
}
|
||||
})
|
||||
.init();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ chrono.workspace = true
|
||||
sha2.workspace = true
|
||||
log.workspace = true
|
||||
tokio.workspace = true
|
||||
trace_id.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -63,6 +63,12 @@ mod tests {
|
||||
media_file_token: None,
|
||||
inference_job_token: None,
|
||||
third_party_id: None,
|
||||
http_host: None,
|
||||
http_origin: None,
|
||||
http_referer: None,
|
||||
http_user_agent: None,
|
||||
artcraft_version: None,
|
||||
trace_id: None,
|
||||
request_ip_address: None,
|
||||
avt_cookie_token: None,
|
||||
session_token: None,
|
||||
@@ -228,6 +234,12 @@ mod tests {
|
||||
media_file_token: None,
|
||||
inference_job_token: None,
|
||||
third_party_id: None,
|
||||
http_host: None,
|
||||
http_origin: None,
|
||||
http_referer: None,
|
||||
http_user_agent: None,
|
||||
artcraft_version: None,
|
||||
trace_id: None,
|
||||
request_ip_address: None,
|
||||
avt_cookie_token: None,
|
||||
session_token: None,
|
||||
|
||||
@@ -37,6 +37,27 @@ pub struct NotificationDetails {
|
||||
/// HTTP status code associated with the event, if any.
|
||||
pub(crate) http_status_code: Option<u16>,
|
||||
|
||||
/// Hostname the client used to reach this server (from the `Host` or
|
||||
/// forwarded-host headers), if any. Useful when a server is reachable
|
||||
/// via multiple domains (eg. api.storyteller.ai, api.fakeyou.com).
|
||||
pub(crate) http_host: Option<String>,
|
||||
|
||||
/// `Origin` header of the request, if any. For browser XHR/fetch this is
|
||||
/// the scheme+host of the calling frontend.
|
||||
pub(crate) http_origin: Option<String>,
|
||||
|
||||
/// `Referer` header of the request, if any. For browser XHR/fetch this is
|
||||
/// the URL of the page that made the call (possibly truncated to the
|
||||
/// origin by the browser's referrer policy).
|
||||
pub(crate) http_referer: Option<String>,
|
||||
|
||||
/// `User-Agent` header of the request, if any.
|
||||
pub(crate) http_user_agent: Option<String>,
|
||||
|
||||
/// `X-ArtCraft-Version` header of the request, if any. Sent by ArtCraft
|
||||
/// clients to identify which app release made the call.
|
||||
pub(crate) artcraft_version: Option<String>,
|
||||
|
||||
/// User token associated with the event, if any.
|
||||
pub(crate) user_token: Option<String>,
|
||||
|
||||
@@ -49,6 +70,9 @@ pub struct NotificationDetails {
|
||||
/// Third-party identifier associated with the event, if any.
|
||||
pub(crate) third_party_id: Option<String>,
|
||||
|
||||
/// The request-scoped trace id associated with the event, if any.
|
||||
pub(crate) trace_id: Option<String>,
|
||||
|
||||
/// IP address of the request that triggered this notification, if any.
|
||||
pub(crate) request_ip_address: Option<String>,
|
||||
|
||||
@@ -185,10 +209,34 @@ impl NotificationDetails {
|
||||
parts.push(format!("HTTP Status Code: {}", status_code));
|
||||
}
|
||||
|
||||
if let Some(host) = &self.http_host {
|
||||
parts.push(format!("HTTP Host: {}", host));
|
||||
}
|
||||
|
||||
if let Some(origin) = &self.http_origin {
|
||||
parts.push(format!("HTTP Origin: {}", origin));
|
||||
}
|
||||
|
||||
if let Some(referer) = &self.http_referer {
|
||||
parts.push(format!("HTTP Referer: {}", referer));
|
||||
}
|
||||
|
||||
if let Some(user_agent) = &self.http_user_agent {
|
||||
parts.push(format!("User Agent: {}", user_agent));
|
||||
}
|
||||
|
||||
if let Some(version) = &self.artcraft_version {
|
||||
parts.push(format!("ArtCraft Version: {}", version));
|
||||
}
|
||||
|
||||
if let Some(ip) = &self.request_ip_address {
|
||||
parts.push(format!("Request IP: {}", ip));
|
||||
}
|
||||
|
||||
if let Some(trace_id) = &self.trace_id {
|
||||
parts.push(format!("Trace ID: {}", trace_id));
|
||||
}
|
||||
|
||||
if parts.is_empty() { None } else { Some(parts.join("\n")) }
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ pub struct NotificationDetailsBuilder {
|
||||
pub(crate) http_method: Option<String>,
|
||||
pub(crate) http_path: Option<String>,
|
||||
pub(crate) http_status_code: Option<u16>,
|
||||
pub(crate) http_host: Option<String>,
|
||||
pub(crate) http_origin: Option<String>,
|
||||
pub(crate) http_referer: Option<String>,
|
||||
pub(crate) http_user_agent: Option<String>,
|
||||
pub(crate) artcraft_version: Option<String>,
|
||||
|
||||
pub(crate) user_token: Option<String>,
|
||||
pub(crate) media_file_token: Option<String>,
|
||||
@@ -31,6 +36,10 @@ pub struct NotificationDetailsBuilder {
|
||||
pub(crate) avt_cookie_token: Option<String>,
|
||||
pub(crate) session_token: Option<String>,
|
||||
pub(crate) session_user_token: Option<String>,
|
||||
|
||||
/// The request-scoped trace id, if any. Auto-filled from the tokio
|
||||
/// task-local at `build()` time when not explicitly set.
|
||||
pub(crate) trace_id: Option<String>,
|
||||
}
|
||||
|
||||
impl NotificationDetailsBuilder {
|
||||
@@ -49,6 +58,11 @@ impl NotificationDetailsBuilder {
|
||||
http_method: None,
|
||||
http_path: None,
|
||||
http_status_code: None,
|
||||
http_host: None,
|
||||
http_origin: None,
|
||||
http_referer: None,
|
||||
http_user_agent: None,
|
||||
artcraft_version: None,
|
||||
user_token: None,
|
||||
media_file_token: None,
|
||||
inference_job_token: None,
|
||||
@@ -57,6 +71,7 @@ impl NotificationDetailsBuilder {
|
||||
avt_cookie_token: None,
|
||||
session_token: None,
|
||||
session_user_token: None,
|
||||
trace_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +94,11 @@ impl NotificationDetailsBuilder {
|
||||
http_method: None,
|
||||
http_path: None,
|
||||
http_status_code: None,
|
||||
http_host: None,
|
||||
http_origin: None,
|
||||
http_referer: None,
|
||||
http_user_agent: None,
|
||||
artcraft_version: None,
|
||||
user_token: None,
|
||||
media_file_token: None,
|
||||
inference_job_token: None,
|
||||
@@ -87,6 +107,7 @@ impl NotificationDetailsBuilder {
|
||||
avt_cookie_token: None,
|
||||
session_token: None,
|
||||
session_user_token: None,
|
||||
trace_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +153,31 @@ impl NotificationDetailsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_http_host(mut self, http_host: Option<String>) -> Self {
|
||||
self.http_host = http_host;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_http_origin(mut self, http_origin: Option<String>) -> Self {
|
||||
self.http_origin = http_origin;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_http_referer(mut self, http_referer: Option<String>) -> Self {
|
||||
self.http_referer = http_referer;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_http_user_agent(mut self, http_user_agent: Option<String>) -> Self {
|
||||
self.http_user_agent = http_user_agent;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_artcraft_version(mut self, artcraft_version: Option<String>) -> Self {
|
||||
self.artcraft_version = artcraft_version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_user_token(mut self, user_token: Option<String>) -> Self {
|
||||
self.user_token = user_token;
|
||||
self
|
||||
@@ -167,6 +213,11 @@ impl NotificationDetailsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_trace_id(mut self, trace_id: Option<String>) -> Self {
|
||||
self.trace_id = trace_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_session_user_token(mut self, session_user_token: Option<String>) -> Self {
|
||||
self.session_user_token = session_user_token;
|
||||
self
|
||||
@@ -191,6 +242,11 @@ impl NotificationDetailsBuilder {
|
||||
http_method: self.http_method,
|
||||
http_path: self.http_path,
|
||||
http_status_code: self.http_status_code,
|
||||
http_host: self.http_host,
|
||||
http_origin: self.http_origin,
|
||||
http_referer: self.http_referer,
|
||||
http_user_agent: self.http_user_agent,
|
||||
artcraft_version: self.artcraft_version,
|
||||
user_token: self.user_token,
|
||||
media_file_token: self.media_file_token,
|
||||
inference_job_token: self.inference_job_token,
|
||||
@@ -199,6 +255,11 @@ impl NotificationDetailsBuilder {
|
||||
avt_cookie_token: self.avt_cookie_token,
|
||||
session_token: self.session_token,
|
||||
session_user_token: self.session_user_token,
|
||||
// Auto-fill from the request's task-local so any page fired while
|
||||
// serving a request is correlated even if the caller never set it.
|
||||
trace_id: self.trace_id.or_else(|| {
|
||||
trace_id::current_trace_id().map(|t| t.to_string())
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-1
@@ -3,6 +3,7 @@ use std::marker::PhantomData;
|
||||
use log::warn;
|
||||
use sqlx::{Executor, MySql, QueryBuilder};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
@@ -18,6 +19,12 @@ where
|
||||
/// The user associated with these debug log events (if any).
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
|
||||
/// The client IP address of the request (if any). Truncated on insert.
|
||||
pub maybe_ip_address: Option<&'e str>,
|
||||
|
||||
/// The request URL (if any). Truncated to 255 characters on insert.
|
||||
pub maybe_url: Option<&'e str>,
|
||||
|
||||
/// The events to insert.
|
||||
pub events: Vec<DebugLogEvent>,
|
||||
|
||||
@@ -31,6 +38,7 @@ where
|
||||
/// Callers should use `serde_json::to_string(...)` before constructing this.
|
||||
pub struct DebugLogEvent {
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_log_level: Option<DebugLogLevel>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
@@ -54,9 +62,11 @@ where
|
||||
}
|
||||
|
||||
let maybe_user_token_str = args.maybe_creator_user_token.map(|t| t.as_str().to_string());
|
||||
let maybe_ip_address = args.maybe_ip_address.map(|ip| truncate_chars(ip, MAX_IP_ADDRESS_CHARS));
|
||||
let maybe_url = args.maybe_url.map(|url| truncate_chars(url, MAX_URL_CHARS));
|
||||
|
||||
let mut query_builder = QueryBuilder::new(
|
||||
"INSERT INTO debug_logs (event_token, debug_log_type, maybe_creator_user_token, message) VALUES "
|
||||
"INSERT INTO debug_logs (event_token, debug_log_type, maybe_log_level, maybe_creator_user_token, maybe_ip_address, maybe_url, message) VALUES "
|
||||
);
|
||||
|
||||
for (i, event) in args.events.iter().enumerate() {
|
||||
@@ -65,8 +75,14 @@ where
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(event.debug_log_type.to_str());
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(event.maybe_log_level.map(|l| l.to_str()));
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(&maybe_user_token_str);
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(maybe_ip_address);
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(maybe_url);
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(&event.message);
|
||||
query_builder.push(")");
|
||||
|
||||
@@ -81,3 +97,17 @@ where
|
||||
|
||||
Ok(event_token)
|
||||
}
|
||||
|
||||
/// Maximum stored length for `maybe_ip_address` (VARCHAR(40)).
|
||||
const MAX_IP_ADDRESS_CHARS: usize = 40;
|
||||
|
||||
/// Maximum stored length for `maybe_url` (VARCHAR(255)).
|
||||
const MAX_URL_CHARS: usize = 255;
|
||||
|
||||
/// Truncate to at most `max_chars` characters (UTF-8 safe).
|
||||
fn truncate_chars(value: &str, max_chars: usize) -> &str {
|
||||
match value.char_indices().nth(max_chars) {
|
||||
Some((idx, _)) => &value[..idx],
|
||||
None => value,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::marker::PhantomData;
|
||||
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
@@ -20,9 +21,18 @@ where
|
||||
/// The type of debug log event.
|
||||
pub debug_log_type: DebugLogType,
|
||||
|
||||
/// The severity level of the log event (if any).
|
||||
pub maybe_log_level: Option<DebugLogLevel>,
|
||||
|
||||
/// The log message body (pre-serialized JSON or any text).
|
||||
pub message: &'e str,
|
||||
|
||||
/// The client IP address of the request (if any). Truncated on insert.
|
||||
pub maybe_ip_address: Option<&'e str>,
|
||||
|
||||
/// The request URL (if any). Truncated to 255 characters on insert.
|
||||
pub maybe_url: Option<&'e str>,
|
||||
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
@@ -47,12 +57,18 @@ INSERT INTO debug_logs
|
||||
SET
|
||||
event_token = ?,
|
||||
debug_log_type = ?,
|
||||
maybe_log_level = ?,
|
||||
maybe_creator_user_token = ?,
|
||||
maybe_ip_address = ?,
|
||||
maybe_url = ?,
|
||||
message = ?
|
||||
"#,
|
||||
event_token.as_str(),
|
||||
args.debug_log_type.to_str(),
|
||||
args.maybe_log_level.map(|l| l.to_str()),
|
||||
args.maybe_creator_user_token.map(|t| t.as_str()),
|
||||
args.maybe_ip_address.map(|ip| truncate_chars(ip, MAX_IP_ADDRESS_CHARS)),
|
||||
args.maybe_url.map(|url| truncate_chars(url, MAX_URL_CHARS)),
|
||||
args.message,
|
||||
)
|
||||
.execute(args.mysql_executor)
|
||||
@@ -60,3 +76,17 @@ SET
|
||||
|
||||
Ok(event_token)
|
||||
}
|
||||
|
||||
/// Maximum stored length for `maybe_ip_address` (VARCHAR(40)).
|
||||
const MAX_IP_ADDRESS_CHARS: usize = 40;
|
||||
|
||||
/// Maximum stored length for `maybe_url` (VARCHAR(255)).
|
||||
const MAX_URL_CHARS: usize = 255;
|
||||
|
||||
/// Truncate to at most `max_chars` characters (UTF-8 safe).
|
||||
fn truncate_chars(value: &str, max_chars: usize) -> &str {
|
||||
match value.char_indices().nth(max_chars) {
|
||||
Some((idx, _)) => &value[..idx],
|
||||
None => value,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::mysql::MySqlRow;
|
||||
use sqlx::{Executor, MySql, QueryBuilder, Row};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
const DEFAULT_LIMIT: u32 = 50;
|
||||
const MAX_LIMIT: u32 = 200;
|
||||
|
||||
pub struct ListAllDebugLogsArgs<'c, E>
|
||||
where
|
||||
E: Executor<'c, Database = MySql>,
|
||||
{
|
||||
/// If provided, only rows with one of these log levels are returned.
|
||||
/// (Rows with a NULL log level are excluded when filtering.)
|
||||
pub maybe_log_levels: Option<Vec<DebugLogLevel>>,
|
||||
|
||||
/// Cursor for pagination: only rows with `id` strictly below this are
|
||||
/// returned. Pass the `next_cursor` from a previous page.
|
||||
pub maybe_id_cursor: Option<u64>,
|
||||
|
||||
pub limit: Option<u32>,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub struct ListAllDebugLogsResult {
|
||||
pub debug_logs: Vec<AllDebugLogRow>,
|
||||
/// Cursor for the next page. `None` if there are no more results.
|
||||
pub next_cursor: Option<u64>,
|
||||
}
|
||||
|
||||
/// A debug log row left-outer-joined against the creator user (if any).
|
||||
#[derive(Debug)]
|
||||
pub struct AllDebugLogRow {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_log_level: Option<DebugLogLevel>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_ip_address: Option<String>,
|
||||
pub maybe_url: Option<String>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
// Joined user fields (present when the creator user exists).
|
||||
pub maybe_user_display_name: Option<String>,
|
||||
pub maybe_user_username: Option<String>,
|
||||
pub maybe_user_gravatar_hash: Option<String>,
|
||||
}
|
||||
|
||||
/// List all debug logs, most recent first, cursor-paginated by `id`,
|
||||
/// optionally scoped to a set of log levels.
|
||||
///
|
||||
/// NB: Uses `QueryBuilder` (not the compile-time macros) because of the
|
||||
/// dynamic `WHERE maybe_log_level IN (...)` clause.
|
||||
pub async fn list_all_debug_logs<'c, E>(
|
||||
args: ListAllDebugLogsArgs<'c, E>,
|
||||
) -> Result<ListAllDebugLogsResult, sqlx::Error>
|
||||
where
|
||||
E: Executor<'c, Database = MySql>,
|
||||
{
|
||||
let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
|
||||
// Fetch limit + 1 so we can detect whether there's a next page.
|
||||
let fetch_limit = (limit + 1) as i64;
|
||||
|
||||
let id_cursor = args.maybe_id_cursor.unwrap_or(u64::MAX);
|
||||
|
||||
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
|
||||
r#"
|
||||
SELECT
|
||||
d.id,
|
||||
d.event_token,
|
||||
d.debug_log_type,
|
||||
d.maybe_log_level,
|
||||
d.maybe_creator_user_token,
|
||||
d.maybe_ip_address,
|
||||
d.maybe_url,
|
||||
d.message,
|
||||
d.created_at,
|
||||
u.display_name as maybe_user_display_name,
|
||||
u.username as maybe_user_username,
|
||||
u.email_gravatar_hash as maybe_user_gravatar_hash
|
||||
FROM debug_logs d
|
||||
LEFT OUTER JOIN users u
|
||||
ON u.token = d.maybe_creator_user_token
|
||||
WHERE d.id < "#,
|
||||
);
|
||||
query_builder.push_bind(id_cursor);
|
||||
|
||||
if let Some(levels) = args.maybe_log_levels.as_ref().filter(|l| !l.is_empty()) {
|
||||
query_builder.push(" AND d.maybe_log_level IN (");
|
||||
let mut separated = query_builder.separated(", ");
|
||||
for level in levels {
|
||||
separated.push_bind(level.to_str());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
}
|
||||
|
||||
query_builder.push(" ORDER BY d.id DESC LIMIT ");
|
||||
query_builder.push_bind(fetch_limit);
|
||||
|
||||
let raw_rows: Vec<MySqlRow> = query_builder
|
||||
.build()
|
||||
.fetch_all(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
let mut debug_logs = raw_rows
|
||||
.into_iter()
|
||||
.map(row_from_mysql_row)
|
||||
.collect::<Result<Vec<AllDebugLogRow>, sqlx::Error>>()?;
|
||||
|
||||
let next_cursor = if debug_logs.len() > limit as usize {
|
||||
debug_logs.truncate(limit as usize);
|
||||
debug_logs.last().map(|row| row.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ListAllDebugLogsResult {
|
||||
debug_logs,
|
||||
next_cursor,
|
||||
})
|
||||
}
|
||||
|
||||
fn row_from_mysql_row(row: MySqlRow) -> Result<AllDebugLogRow, sqlx::Error> {
|
||||
let created_at: DateTime<Utc> = row.try_get("created_at")?;
|
||||
|
||||
Ok(AllDebugLogRow {
|
||||
id: row.try_get("id")?,
|
||||
event_token: row.try_get("event_token")?,
|
||||
debug_log_type: row.try_get("debug_log_type")?,
|
||||
maybe_log_level: row.try_get("maybe_log_level")?,
|
||||
maybe_creator_user_token: row.try_get("maybe_creator_user_token")?,
|
||||
maybe_ip_address: row.try_get("maybe_ip_address")?,
|
||||
maybe_url: row.try_get("maybe_url")?,
|
||||
message: row.try_get("message")?,
|
||||
created_at,
|
||||
maybe_user_display_name: row.try_get("maybe_user_display_name")?,
|
||||
maybe_user_username: row.try_get("maybe_user_username")?,
|
||||
maybe_user_gravatar_hash: row.try_get("maybe_user_gravatar_hash")?,
|
||||
})
|
||||
}
|
||||
+38
-9
@@ -3,6 +3,7 @@ use std::marker::PhantomData;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
@@ -24,9 +25,17 @@ pub struct DebugLogRow {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_log_level: Option<DebugLogLevel>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_ip_address: Option<String>,
|
||||
pub maybe_url: Option<String>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
// Joined user fields (present when the creator user exists).
|
||||
pub maybe_user_display_name: Option<String>,
|
||||
pub maybe_user_username: Option<String>,
|
||||
pub maybe_user_gravatar_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -34,9 +43,15 @@ struct RawDebugLogRow {
|
||||
id: u64,
|
||||
event_token: DebugLogEventToken,
|
||||
debug_log_type: DebugLogType,
|
||||
maybe_log_level: Option<DebugLogLevel>,
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
maybe_ip_address: Option<String>,
|
||||
maybe_url: Option<String>,
|
||||
message: String,
|
||||
created_at: NaiveDateTime,
|
||||
maybe_user_display_name: Option<String>,
|
||||
maybe_user_username: Option<String>,
|
||||
maybe_user_gravatar_hash: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list_debug_logs_for_token<'e, 'c: 'e, E>(
|
||||
@@ -51,15 +66,23 @@ where
|
||||
RawDebugLogRow,
|
||||
r#"
|
||||
SELECT
|
||||
id as `id: u64`,
|
||||
event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
|
||||
debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,
|
||||
maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
message,
|
||||
created_at
|
||||
FROM debug_logs
|
||||
WHERE event_token = ?
|
||||
ORDER BY id ASC
|
||||
d.id as `id: u64`,
|
||||
d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
|
||||
d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,
|
||||
d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,
|
||||
d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
d.maybe_ip_address,
|
||||
d.maybe_url,
|
||||
d.message,
|
||||
d.created_at,
|
||||
u.display_name as `maybe_user_display_name?`,
|
||||
u.username as `maybe_user_username?`,
|
||||
u.email_gravatar_hash as `maybe_user_gravatar_hash?`
|
||||
FROM debug_logs d
|
||||
LEFT OUTER JOIN users u
|
||||
ON u.token = d.maybe_creator_user_token
|
||||
WHERE d.event_token = ?
|
||||
ORDER BY d.id ASC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.event_token.as_str(),
|
||||
@@ -73,9 +96,15 @@ LIMIT ?
|
||||
id: row.id,
|
||||
event_token: row.event_token,
|
||||
debug_log_type: row.debug_log_type,
|
||||
maybe_log_level: row.maybe_log_level,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
maybe_ip_address: row.maybe_ip_address,
|
||||
maybe_url: row.maybe_url,
|
||||
message: row.message,
|
||||
created_at: row.created_at.and_utc(),
|
||||
maybe_user_display_name: row.maybe_user_display_name,
|
||||
maybe_user_username: row.maybe_user_username,
|
||||
maybe_user_gravatar_hash: row.maybe_user_gravatar_hash,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
const DEFAULT_LIMIT: u32 = 50;
|
||||
const MAX_LIMIT: u32 = 200;
|
||||
|
||||
pub struct ListDebugLogsForUserArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
pub user_token: &'e UserToken,
|
||||
|
||||
/// Cursor for pagination: only rows with `id` strictly below this are
|
||||
/// returned. Pass the `next_cursor` from a previous page.
|
||||
pub maybe_id_cursor: Option<u64>,
|
||||
|
||||
pub limit: Option<u32>,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub struct ListDebugLogsForUserResult {
|
||||
pub debug_logs: Vec<UserDebugLogRow>,
|
||||
/// Cursor for the next page. `None` if there are no more results.
|
||||
pub next_cursor: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserDebugLogRow {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_log_level: Option<DebugLogLevel>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_ip_address: Option<String>,
|
||||
pub maybe_url: Option<String>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
// Joined user fields (present when the creator user exists).
|
||||
pub maybe_user_display_name: Option<String>,
|
||||
pub maybe_user_username: Option<String>,
|
||||
pub maybe_user_gravatar_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RawUserDebugLogRow {
|
||||
id: u64,
|
||||
event_token: DebugLogEventToken,
|
||||
debug_log_type: DebugLogType,
|
||||
maybe_log_level: Option<DebugLogLevel>,
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
maybe_ip_address: Option<String>,
|
||||
maybe_url: Option<String>,
|
||||
message: String,
|
||||
created_at: NaiveDateTime,
|
||||
maybe_user_display_name: Option<String>,
|
||||
maybe_user_username: Option<String>,
|
||||
maybe_user_gravatar_hash: Option<String>,
|
||||
}
|
||||
|
||||
/// List debug logs for a user, most recent first, cursor-paginated by `id`.
|
||||
pub async fn list_debug_logs_for_user<'e, 'c: 'e, E>(
|
||||
args: ListDebugLogsForUserArgs<'e, 'c, E>,
|
||||
) -> Result<ListDebugLogsForUserResult, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
|
||||
// Fetch limit + 1 so we can detect whether there's a next page.
|
||||
let fetch_limit = (limit + 1) as i64;
|
||||
|
||||
// Use u64::MAX when no cursor so all rows are included.
|
||||
let id_cursor = args.maybe_id_cursor.unwrap_or(u64::MAX);
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
RawUserDebugLogRow,
|
||||
r#"
|
||||
SELECT
|
||||
d.id as `id: u64`,
|
||||
d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
|
||||
d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,
|
||||
d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,
|
||||
d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
d.maybe_ip_address,
|
||||
d.maybe_url,
|
||||
d.message,
|
||||
d.created_at,
|
||||
u.display_name as `maybe_user_display_name?`,
|
||||
u.username as `maybe_user_username?`,
|
||||
u.email_gravatar_hash as `maybe_user_gravatar_hash?`
|
||||
FROM debug_logs d
|
||||
LEFT OUTER JOIN users u
|
||||
ON u.token = d.maybe_creator_user_token
|
||||
WHERE d.maybe_creator_user_token = ?
|
||||
AND d.id < ?
|
||||
ORDER BY d.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.user_token.as_str(),
|
||||
id_cursor,
|
||||
fetch_limit,
|
||||
)
|
||||
.fetch_all(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
let mut debug_logs: Vec<UserDebugLogRow> = rows.into_iter().map(|row| {
|
||||
UserDebugLogRow {
|
||||
id: row.id,
|
||||
event_token: row.event_token,
|
||||
debug_log_type: row.debug_log_type,
|
||||
maybe_log_level: row.maybe_log_level,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
maybe_ip_address: row.maybe_ip_address,
|
||||
maybe_url: row.maybe_url,
|
||||
message: row.message,
|
||||
created_at: row.created_at.and_utc(),
|
||||
maybe_user_display_name: row.maybe_user_display_name,
|
||||
maybe_user_username: row.maybe_user_username,
|
||||
maybe_user_gravatar_hash: row.maybe_user_gravatar_hash,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let next_cursor = if debug_logs.len() > limit as usize {
|
||||
debug_logs.truncate(limit as usize);
|
||||
debug_logs.last().map(|row| row.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ListDebugLogsForUserResult {
|
||||
debug_logs,
|
||||
next_cursor,
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod bulk_insert_debug_logs;
|
||||
pub mod insert_debug_log;
|
||||
pub mod list_all_debug_logs;
|
||||
pub mod list_debug_logs_for_token;
|
||||
pub mod list_debug_logs_for_user;
|
||||
|
||||
+29
-9
@@ -107,6 +107,7 @@ pub async fn list_media_files_for_user(args: ListMediaFileForUserArgs<'_>) -> An
|
||||
args.sort_ascending,
|
||||
args.view_as,
|
||||
count_fields.as_str(),
|
||||
true,
|
||||
);
|
||||
|
||||
let row_count_query = count_query_builder.build_query_scalar::<i64>();
|
||||
@@ -126,6 +127,7 @@ pub async fn list_media_files_for_user(args: ListMediaFileForUserArgs<'_>) -> An
|
||||
args.sort_ascending,
|
||||
args.view_as,
|
||||
result_fields.as_str(),
|
||||
false,
|
||||
);
|
||||
|
||||
let query = query.build_query_as::<MediaFileListItemInternal>();
|
||||
@@ -250,13 +252,20 @@ fn query_builder<'a>(
|
||||
sort_ascending: bool,
|
||||
view_as: ViewAs,
|
||||
select_fields: &'a str,
|
||||
count_only: bool,
|
||||
) -> QueryBuilder<'a, MySql> {
|
||||
|
||||
// NB: Query cannot be statically checked by sqlx
|
||||
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
|
||||
format!(r#"
|
||||
SELECT
|
||||
{select_fields}
|
||||
// NB: The count query only needs the `users` join (for the username predicate).
|
||||
// The other joins are all 1:1 and contribute nothing to the count — MySQL does
|
||||
// not eliminate LEFT JOINs, so including them costs four index lookups per row.
|
||||
let from_clause = if count_only {
|
||||
r#"
|
||||
FROM media_files AS m
|
||||
LEFT OUTER JOIN users AS u
|
||||
ON m.maybe_creator_user_token = u.token
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
FROM media_files AS m
|
||||
LEFT OUTER JOIN users AS u
|
||||
ON m.maybe_creator_user_token = u.token
|
||||
@@ -270,6 +279,14 @@ LEFT OUTER JOIN entity_stats
|
||||
LEFT OUTER JOIN prompts
|
||||
ON prompts.token = m.maybe_prompt_token
|
||||
"#
|
||||
};
|
||||
|
||||
// NB: Query cannot be statically checked by sqlx
|
||||
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
|
||||
format!(r#"
|
||||
SELECT
|
||||
{select_fields}
|
||||
{from_clause}"#
|
||||
));
|
||||
|
||||
query_builder.push(" WHERE u.username = ");
|
||||
@@ -353,10 +370,13 @@ LEFT OUTER JOIN prompts
|
||||
}
|
||||
}
|
||||
|
||||
if sort_ascending {
|
||||
query_builder.push(" ORDER BY m.created_at ASC ");
|
||||
} else {
|
||||
query_builder.push(" ORDER BY m.created_at DESC ");
|
||||
// NB: Ordering is meaningless for the single-row COUNT aggregate.
|
||||
if !count_only {
|
||||
if sort_ascending {
|
||||
query_builder.push(" ORDER BY m.created_at ASC ");
|
||||
} else {
|
||||
query_builder.push(" ORDER BY m.created_at DESC ");
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_limits {
|
||||
|
||||
@@ -47,7 +47,6 @@ pub mod user_spend_events;
|
||||
pub mod user_spend_summaries;
|
||||
pub mod users;
|
||||
pub mod vocoder;
|
||||
pub mod voice_clone_requests;
|
||||
pub mod voice_conversion;
|
||||
pub mod voice_designer;
|
||||
pub mod wallet_ledger_entries;
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::helpers::numeric_converters::try_i64_to_u64_or_min;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct QueueStatsRow {
|
||||
pub queue_type: String,
|
||||
pub pending_job_count: u64,
|
||||
pub present_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
struct QueueStatsRowInternal {
|
||||
queue_type: Option<String>, // FIXME: Shouldn't be nullable
|
||||
pending_job_count: i64,
|
||||
present_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
// NB: Returning Option<T> is a hack to fit with outer Redis caching.
|
||||
// I'm only doing this because I'm in a hurry. Don't do this.
|
||||
pub async fn get_unified_queue_stats(
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Vec<QueueStatsRow>> {
|
||||
// NB (1): We query as a union since "started" jobs can get picked off mid-run and go stale
|
||||
// forever. We currently have no automated means of collecting those jobs.
|
||||
// NB (2): We query the server timestamp so we can cache the results.
|
||||
// The frontend can then monotonically adjust the count based on timestamp by ignoring
|
||||
// past times.
|
||||
let rows : Vec<QueueStatsRowInternal> = sqlx::query_as!(
|
||||
QueueStatsRowInternal,
|
||||
r#"
|
||||
SELECT
|
||||
job_type as queue_type,
|
||||
count(*) as pending_job_count,
|
||||
NOW() as present_time
|
||||
FROM (
|
||||
SELECT
|
||||
token,
|
||||
job_type
|
||||
FROM generic_inference_jobs
|
||||
WHERE status IN ("pending", "attempt_failed")
|
||||
AND job_type IS NOT NULL
|
||||
UNION
|
||||
SELECT
|
||||
token,
|
||||
job_type
|
||||
FROM generic_inference_jobs
|
||||
WHERE status IN ("started")
|
||||
AND created_at > (CURDATE() - INTERVAL 15 MINUTE)
|
||||
AND job_type IS NOT NULL
|
||||
) as generic_inner
|
||||
GROUP BY queue_type
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(|row|
|
||||
QueueStatsRow {
|
||||
queue_type: row.queue_type.unwrap_or_else(|| "unknown".to_string()),
|
||||
pending_job_count: try_i64_to_u64_or_min(row.pending_job_count),
|
||||
present_time: row.present_time,
|
||||
}).collect::<Vec<_>>())
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod get_on_prem_worker_stats;
|
||||
pub mod get_unified_queue_stats;
|
||||
pub mod get_voice_count_stats;
|
||||
pub mod get_voice_count_stats;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod stats;
|
||||
pub mod tts_category_assignments;
|
||||
pub mod tts_download_jobs;
|
||||
pub mod tts_inference_jobs;
|
||||
pub mod tts_model_upload_jobs;
|
||||
pub mod tts_models;
|
||||
pub mod tts_results;
|
||||
pub mod tts_results;
|
||||
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
use sqlx::MySql;
|
||||
use sqlx::pool::PoolConnection;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct TtsLeaderboardRecordForList {
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub gravatar_hash: String,
|
||||
pub creator_user_token: String,
|
||||
pub uploaded_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TtsLeaderboardRecordForListRaw {
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub gravatar_hash: String,
|
||||
pub creator_user_token: String,
|
||||
pub uploaded_count: i64,
|
||||
}
|
||||
|
||||
// NB: This may need to become an offline query if it is expensive.
|
||||
pub async fn calculate_tts_model_leaderboard(
|
||||
mysql_pool: &mut PoolConnection<MySql>
|
||||
) -> AnyhowResult<Vec<TtsLeaderboardRecordForList>> {
|
||||
|
||||
// NB(1): We're not requiring "is_public_listing_approved IS TRUE" for TTS models
|
||||
// This is the opposite for W2L templates (for now at least)
|
||||
// NB(2): We're excluding a few users (eg. Vegito's alternate account)
|
||||
// U:DRB1SKEMKHT1J - https://fakeyou.com/profile/bluefusion/uploaded-tts-models
|
||||
let maybe_results = sqlx::query_as!(
|
||||
TtsLeaderboardRecordForListRaw,
|
||||
r#"
|
||||
SELECT
|
||||
users.username,
|
||||
users.display_name,
|
||||
users.email_gravatar_hash as gravatar_hash,
|
||||
creator_user_token,
|
||||
count(*) as uploaded_count
|
||||
FROM tts_models
|
||||
JOIN users
|
||||
ON
|
||||
users.token = creator_user_token
|
||||
WHERE
|
||||
users.is_banned IS FALSE
|
||||
AND users.token NOT IN ("U:DRB1SKEMKHT1J")
|
||||
AND tts_models.user_deleted_at IS NULL
|
||||
AND tts_models.mod_deleted_at IS NULL
|
||||
GROUP BY creator_user_token
|
||||
ORDER BY uploaded_count desc
|
||||
LIMIT 25;
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&mut **mysql_pool)
|
||||
.await;
|
||||
|
||||
let results : Vec<TtsLeaderboardRecordForListRaw> = match maybe_results {
|
||||
Ok(results) => results,
|
||||
Err(err) => {
|
||||
warn!("Error: {:?}", err);
|
||||
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
return Ok(Vec::new());
|
||||
},
|
||||
_ => {
|
||||
warn!("tts leaderboard query error: {:?}", err);
|
||||
return Err(anyhow!("tts leaderboard query error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(results.into_iter()
|
||||
.map(|result| {
|
||||
TtsLeaderboardRecordForList {
|
||||
username: result.username.to_string(),
|
||||
display_name: result.display_name.to_string(),
|
||||
gravatar_hash: result.gravatar_hash.to_string(),
|
||||
creator_user_token: result.creator_user_token.to_string(),
|
||||
uploaded_count: result.uploaded_count,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<TtsLeaderboardRecordForList>>())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod calculate_tts_model_leaderboard;
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::helpers::numeric_converters::try_i64_to_u64_or_min;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TtsQueueLengthResult {
|
||||
pub record_count: u64,
|
||||
pub present_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
struct QueryResultInternal {
|
||||
record_count: i64,
|
||||
present_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
pub async fn get_pending_tts_inference_job_count(
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<TtsQueueLengthResult> {
|
||||
// NB (1): We query as a union since "started" jobs can get picked off mid-run and go stale
|
||||
// forever. We currently have no automated means of collecting those jobs.
|
||||
// NB (2): We query the server timestamp so we can cache the results.
|
||||
// The frontend can then monotonically adjust the count based on timestamp by ignoring
|
||||
// past times.
|
||||
let result : QueryResultInternal = sqlx::query_as!(
|
||||
QueryResultInternal,
|
||||
r#"
|
||||
SELECT
|
||||
COUNT(distinct token) as record_count,
|
||||
NOW() as present_time
|
||||
FROM
|
||||
(
|
||||
SELECT token
|
||||
FROM tts_inference_jobs
|
||||
WHERE status = "started"
|
||||
AND created_at > (CURDATE() - INTERVAL 5 MINUTE)
|
||||
UNION
|
||||
SELECT token
|
||||
FROM tts_inference_jobs
|
||||
WHERE status IN ("pending", "attempt_failed")
|
||||
) as t
|
||||
"#,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(TtsQueueLengthResult {
|
||||
record_count: try_i64_to_u64_or_min(result.record_count),
|
||||
present_time: result.present_time,
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod _keys;
|
||||
pub mod get_pending_tts_inference_job_count;
|
||||
pub mod get_pending_tts_inference_job_detailed_stats;
|
||||
pub mod get_tts_inference_job_status;
|
||||
pub mod insert_tts_inference_job;
|
||||
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
use sqlx::error::Error::Database;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::tts_model_upload_jobs::TtsModelUploadJobToken;
|
||||
|
||||
pub struct InsertTtsModelUploadJobArgs<'a> {
|
||||
pub uuid: &'a str,
|
||||
pub creator_user_token: &'a str,
|
||||
pub creator_ip_address: &'a str,
|
||||
pub creator_set_visibility: &'a str,
|
||||
pub title: &'a str,
|
||||
pub tts_model_type: &'a str,
|
||||
pub download_url: &'a str,
|
||||
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
// NB: Returns job token
|
||||
pub async fn insert_tts_model_upload_job(args: InsertTtsModelUploadJobArgs<'_>) -> AnyhowResult<String> {
|
||||
// This token is returned to the client.
|
||||
let job_token = TtsModelUploadJobToken::generate().to_string();
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO tts_model_upload_jobs
|
||||
SET
|
||||
token = ?,
|
||||
uuid_idempotency_token = ?,
|
||||
creator_user_token = ?,
|
||||
creator_ip_address = ?,
|
||||
creator_set_visibility = ?,
|
||||
title = ?,
|
||||
tts_model_type = ?,
|
||||
download_url = ?,
|
||||
status = "pending"
|
||||
"#,
|
||||
job_token.to_string(),
|
||||
args.uuid,
|
||||
args.creator_user_token,
|
||||
args.creator_ip_address,
|
||||
args.creator_set_visibility,
|
||||
args.title,
|
||||
args.tts_model_type,
|
||||
args.download_url,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await;
|
||||
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => {
|
||||
res.last_insert_id()
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("New tts model upload creation DB error: {:?}", err);
|
||||
|
||||
// NB: SQLSTATE[23000]: Integrity constraint violation
|
||||
// NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access)
|
||||
match err {
|
||||
Database(ref err) => {
|
||||
let _maybe_code = err.code().map(|c| c.into_owned());
|
||||
/*match maybe_code.as_deref() {
|
||||
Some("23000") => {
|
||||
if err.message().contains("username") {
|
||||
return Err(UsernameTaken);
|
||||
} else if err.message().contains("email_address") {
|
||||
return Err(EmailTaken);
|
||||
}
|
||||
}
|
||||
_ => {},
|
||||
}*/
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
return Err(anyhow!("error inserting model upload job record: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(job_token)
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod get_tts_model_upload_job_status;
|
||||
pub mod insert_tts_model_upload_job;
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn delete_tts_inference_result_as_user(
|
||||
inference_result_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE tts_results
|
||||
SET
|
||||
user_deleted_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_tts_inference_result_as_mod(
|
||||
inference_result_token: &str,
|
||||
mod_user_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE tts_results
|
||||
SET
|
||||
mod_deleted_at = CURRENT_TIMESTAMP,
|
||||
maybe_mod_user_token = ?
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
mod_user_token,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn undelete_tts_inference_result_as_user(
|
||||
inference_result_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE tts_results
|
||||
SET
|
||||
user_deleted_at = NULL
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn undelete_tts_inference_result_as_mod(
|
||||
inference_result_token: &str,
|
||||
mod_user_token: &str,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<()> {
|
||||
let _r = sqlx::query!(
|
||||
r#"
|
||||
UPDATE tts_results
|
||||
SET
|
||||
mod_deleted_at = NULL,
|
||||
maybe_mod_user_token = ?
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
mod_user_token,
|
||||
inference_result_token,
|
||||
)
|
||||
.execute(mysql_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod delete_tts_result_various_scopes;
|
||||
pub mod edit_tts_result;
|
||||
pub mod insert_tts_result;
|
||||
pub mod list_tts_results_query_builder;
|
||||
pub mod query_tts_result;
|
||||
pub mod query_tts_result;
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// One user's aggregated spend within the requested window, joined to their
|
||||
/// display info. Only attributed (non-null user), production events count.
|
||||
pub struct TopSpenderForWindow {
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_gravatar_hash: String,
|
||||
pub gross_spend_usd_cents: u64,
|
||||
pub refund_usd_cents: u64,
|
||||
pub net_spend_usd_cents: i64,
|
||||
pub payment_count: u64,
|
||||
pub credits_granted: u64,
|
||||
}
|
||||
|
||||
pub struct ListTopSpendersForWindowArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
/// Inclusive start of the aggregation window.
|
||||
pub window_start: DateTime<Utc>,
|
||||
/// Optional namespace filter. `None` aggregates across all namespaces.
|
||||
pub maybe_payments_namespace: Option<&'e str>,
|
||||
pub limit: i64,
|
||||
pub offset: i64,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
/// Top spenders since `window_start`, sorted by net spend descending.
|
||||
/// Aggregates `user_spend_events` directly (exact rolling windows, unlike the
|
||||
/// daily rollups) — the table is payment-scale, so this stays cheap.
|
||||
pub async fn list_top_spenders_for_window<'e, 'c: 'e, E>(
|
||||
args: ListTopSpendersForWindowArgs<'e, 'c, E>,
|
||||
) -> Result<Vec<TopSpenderForWindow>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let rows = sqlx::query_as!(
|
||||
TopSpenderForWindow,
|
||||
r#"
|
||||
SELECT
|
||||
e.maybe_user_token AS `user_token!: UserToken`,
|
||||
u.username AS `username!`,
|
||||
u.display_name AS `display_name!`,
|
||||
u.email_gravatar_hash AS `email_gravatar_hash!`,
|
||||
CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `gross_spend_usd_cents!: u64`,
|
||||
CAST(SUM(CASE WHEN e.amount_usd_cents < 0 THEN -e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `refund_usd_cents!: u64`,
|
||||
CAST(SUM(e.amount_usd_cents) AS SIGNED) AS `net_spend_usd_cents!: i64`,
|
||||
CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN 1 ELSE 0 END) AS UNSIGNED) AS `payment_count!: u64`,
|
||||
CAST(COALESCE(SUM(e.maybe_credits_granted), 0) AS UNSIGNED) AS `credits_granted!: u64`
|
||||
FROM user_spend_events e
|
||||
JOIN users u ON u.token = e.maybe_user_token
|
||||
WHERE e.maybe_user_token IS NOT NULL
|
||||
AND e.is_production = TRUE
|
||||
AND e.payment_occurred_at >= ?
|
||||
AND (? IS NULL OR e.payments_namespace = ?)
|
||||
GROUP BY e.maybe_user_token, u.username, u.display_name, u.email_gravatar_hash
|
||||
ORDER BY SUM(e.amount_usd_cents) DESC, e.maybe_user_token ASC
|
||||
LIMIT ? OFFSET ?
|
||||
"#,
|
||||
args.window_start,
|
||||
args.maybe_payments_namespace,
|
||||
args.maybe_payments_namespace,
|
||||
args.limit,
|
||||
args.offset,
|
||||
)
|
||||
.fetch_all(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod list_top_spenders_for_window;
|
||||
pub mod list_user_spend_events_for_moderation;
|
||||
pub mod backfill_get_user_token_by_email;
|
||||
pub mod backfill_list_artcraft_customer_links;
|
||||
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::helpers::boolean_converters::i8_to_bool;
|
||||
|
||||
/// Lookup by voice clone request token
|
||||
pub async fn get_voice_clone_request_by_token(
|
||||
voice_clone_request_token: &str,
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<VoiceCloneRequest>> {
|
||||
internal_voice_clone_request_by_token(voice_clone_request_token, pool).await
|
||||
}
|
||||
|
||||
/// Lookup by user
|
||||
pub async fn get_voice_clone_request_by_user_token(
|
||||
user_token: &str,
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<VoiceCloneRequest>> {
|
||||
internal_voice_clone_request_by_user_token(user_token, pool).await
|
||||
}
|
||||
|
||||
/// Used for both *BY TOKEN* and for *BY USER* lookups.
|
||||
/// The user field should be optional for one and not for the other, but code reuse is more
|
||||
/// important and this is a quick / hasty feature.
|
||||
#[derive(Debug)]
|
||||
pub struct VoiceCloneRequest {
|
||||
pub token: String,
|
||||
|
||||
// Contact
|
||||
pub maybe_user_token: Option<String>,
|
||||
pub email_address: String,
|
||||
pub discord_username: String,
|
||||
|
||||
// Visibility
|
||||
pub is_for_private_use: bool,
|
||||
pub is_for_public_use: bool,
|
||||
|
||||
// Use
|
||||
pub is_for_studio: bool,
|
||||
pub is_for_twitch_tts: bool,
|
||||
pub is_for_api_use: bool,
|
||||
pub is_for_music: bool,
|
||||
pub is_for_games: bool,
|
||||
pub is_for_other: bool,
|
||||
pub optional_notes_on_use: Option<String>,
|
||||
|
||||
// Subject/Ownership
|
||||
pub is_own_voice: bool,
|
||||
pub is_third_party_voice: bool,
|
||||
pub optional_notes_on_subject: Option<String>,
|
||||
|
||||
// Equipment
|
||||
pub has_clean_audio_recordings: bool,
|
||||
pub has_good_microphone: bool,
|
||||
|
||||
// Comments
|
||||
pub optional_questions: Option<String>,
|
||||
pub optional_extra_comments: Option<String>,
|
||||
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
async fn internal_voice_clone_request_by_token(
|
||||
voice_clone_request_token: &str,
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<VoiceCloneRequest>> {
|
||||
let maybe_record = sqlx::query_as!(
|
||||
VoiceCloneRequestInternal,
|
||||
r#"
|
||||
SELECT
|
||||
token,
|
||||
|
||||
maybe_user_token,
|
||||
email_address,
|
||||
discord_username,
|
||||
|
||||
is_for_private_use,
|
||||
is_for_public_use,
|
||||
|
||||
is_for_studio,
|
||||
is_for_twitch_tts,
|
||||
is_for_api_use,
|
||||
is_for_music,
|
||||
is_for_games,
|
||||
is_for_other,
|
||||
optional_notes_on_use,
|
||||
|
||||
is_own_voice,
|
||||
is_third_party_voice,
|
||||
optional_notes_on_subject,
|
||||
|
||||
has_clean_audio_recordings,
|
||||
has_good_microphone,
|
||||
|
||||
optional_questions,
|
||||
optional_extra_comments,
|
||||
|
||||
created_at,
|
||||
updated_at
|
||||
FROM voice_clone_requests
|
||||
WHERE
|
||||
token = ?
|
||||
"#,
|
||||
voice_clone_request_token,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
|
||||
handle_result(maybe_record)
|
||||
}
|
||||
|
||||
async fn internal_voice_clone_request_by_user_token(
|
||||
user_token: &str,
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<VoiceCloneRequest>> {
|
||||
let maybe_record = sqlx::query_as!(
|
||||
VoiceCloneRequestInternal,
|
||||
r#"
|
||||
SELECT
|
||||
token,
|
||||
|
||||
maybe_user_token,
|
||||
email_address,
|
||||
discord_username,
|
||||
|
||||
is_for_private_use,
|
||||
is_for_public_use,
|
||||
|
||||
is_for_studio,
|
||||
is_for_twitch_tts,
|
||||
is_for_api_use,
|
||||
is_for_music,
|
||||
is_for_games,
|
||||
is_for_other,
|
||||
optional_notes_on_use,
|
||||
|
||||
is_own_voice,
|
||||
is_third_party_voice,
|
||||
optional_notes_on_subject,
|
||||
|
||||
has_clean_audio_recordings,
|
||||
has_good_microphone,
|
||||
|
||||
optional_questions,
|
||||
optional_extra_comments,
|
||||
|
||||
created_at,
|
||||
updated_at
|
||||
FROM voice_clone_requests
|
||||
WHERE
|
||||
maybe_user_token = ?
|
||||
"#,
|
||||
user_token,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
|
||||
handle_result(maybe_record)
|
||||
}
|
||||
|
||||
fn handle_result(
|
||||
maybe_record: Result<VoiceCloneRequestInternal, sqlx::Error>,
|
||||
) -> AnyhowResult<Option<VoiceCloneRequest>> {
|
||||
let record : VoiceCloneRequestInternal = match maybe_record {
|
||||
Ok(record) => record,
|
||||
Err(sqlx::Error::RowNotFound) => return Ok(None),
|
||||
Err(ref err) => return Err(anyhow!("database query error: {:?}", err)),
|
||||
};
|
||||
|
||||
Ok(Some(VoiceCloneRequest {
|
||||
token: record.token,
|
||||
maybe_user_token: record.maybe_user_token,
|
||||
email_address: record.email_address,
|
||||
discord_username: record.discord_username,
|
||||
is_for_private_use: i8_to_bool(record.is_for_private_use),
|
||||
is_for_public_use: i8_to_bool(record.is_for_public_use),
|
||||
is_for_studio: i8_to_bool(record.is_for_studio),
|
||||
is_for_twitch_tts: i8_to_bool(record.is_for_twitch_tts),
|
||||
is_for_api_use: i8_to_bool(record.is_for_api_use),
|
||||
is_for_music: i8_to_bool(record.is_for_music),
|
||||
is_for_games: i8_to_bool(record.is_for_games),
|
||||
is_for_other: i8_to_bool(record.is_for_other),
|
||||
optional_notes_on_use: record.optional_notes_on_use,
|
||||
is_own_voice: i8_to_bool(record.is_own_voice),
|
||||
is_third_party_voice: i8_to_bool(record.is_third_party_voice),
|
||||
optional_notes_on_subject: record.optional_notes_on_subject,
|
||||
has_clean_audio_recordings: i8_to_bool(record.has_clean_audio_recordings),
|
||||
has_good_microphone: i8_to_bool(record.has_good_microphone),
|
||||
optional_questions: record.optional_questions,
|
||||
optional_extra_comments: record.optional_extra_comments,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct VoiceCloneRequestInternal {
|
||||
pub token: String,
|
||||
|
||||
// Contact
|
||||
pub maybe_user_token: Option<String>,
|
||||
pub email_address: String,
|
||||
pub discord_username: String,
|
||||
|
||||
// Visibility
|
||||
pub is_for_private_use: i8,
|
||||
pub is_for_public_use: i8,
|
||||
|
||||
// Use
|
||||
pub is_for_studio: i8,
|
||||
pub is_for_twitch_tts: i8,
|
||||
pub is_for_api_use: i8,
|
||||
pub is_for_music: i8,
|
||||
pub is_for_games: i8,
|
||||
pub is_for_other: i8,
|
||||
pub optional_notes_on_use: Option<String>,
|
||||
|
||||
// Subject/Ownership
|
||||
pub is_own_voice: i8,
|
||||
pub is_third_party_voice: i8,
|
||||
pub optional_notes_on_subject: Option<String>,
|
||||
|
||||
// Equipment
|
||||
pub has_clean_audio_recordings: i8,
|
||||
pub has_good_microphone: i8,
|
||||
|
||||
// Comments
|
||||
pub optional_questions: Option<String>,
|
||||
pub optional_extra_comments: Option<String>,
|
||||
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
pub updated_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::voice_clone_requests::VoiceCloneRequestToken;
|
||||
|
||||
pub struct InsertVoiceCloneRequestBuilder {
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
// Contact
|
||||
pub maybe_user_token: Option<String>,
|
||||
pub email_address: String,
|
||||
pub discord_username: String,
|
||||
|
||||
// Visibility
|
||||
pub is_for_private_use: bool,
|
||||
pub is_for_public_use: bool,
|
||||
|
||||
// Use
|
||||
pub is_for_studio: bool,
|
||||
pub is_for_twitch_tts: bool,
|
||||
pub is_for_api_use: bool,
|
||||
pub is_for_music: bool,
|
||||
pub is_for_games: bool,
|
||||
pub is_for_other: bool,
|
||||
pub optional_notes_on_use: Option<String>,
|
||||
|
||||
// Subject/Ownership
|
||||
pub is_own_voice: bool,
|
||||
pub is_third_party_voice: bool,
|
||||
pub optional_notes_on_subject: Option<String>,
|
||||
|
||||
// Equipment
|
||||
pub has_clean_audio_recordings: bool,
|
||||
pub has_good_microphone: bool,
|
||||
|
||||
// Comments
|
||||
pub optional_questions: Option<String>,
|
||||
pub optional_extra_comments: Option<String>,
|
||||
|
||||
pub ip_address_creation: String,
|
||||
}
|
||||
|
||||
impl InsertVoiceCloneRequestBuilder {
|
||||
|
||||
/// Returns the newly generated token.
|
||||
pub async fn insert(&self, mysql_pool: &MySqlPool) -> AnyhowResult<String> {
|
||||
|
||||
let token = VoiceCloneRequestToken::generate().to_string();
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO voice_clone_requests
|
||||
SET
|
||||
token = ?,
|
||||
uuid_idempotency_token = ?,
|
||||
|
||||
maybe_user_token = ?,
|
||||
email_address = ?,
|
||||
discord_username = ?,
|
||||
|
||||
is_for_private_use = ?,
|
||||
is_for_public_use = ?,
|
||||
|
||||
is_for_studio = ?,
|
||||
is_for_twitch_tts = ?,
|
||||
is_for_api_use = ?,
|
||||
is_for_music = ?,
|
||||
is_for_games = ?,
|
||||
is_for_other = ?,
|
||||
optional_notes_on_use = ?,
|
||||
|
||||
is_own_voice = ?,
|
||||
is_third_party_voice = ?,
|
||||
optional_notes_on_subject = ?,
|
||||
|
||||
has_clean_audio_recordings = ?,
|
||||
has_good_microphone = ?,
|
||||
|
||||
optional_questions = ?,
|
||||
optional_extra_comments = ?,
|
||||
|
||||
ip_address_creation = ?
|
||||
"#,
|
||||
&token,
|
||||
&self.uuid_idempotency_token,
|
||||
|
||||
&self.maybe_user_token,
|
||||
&self.email_address,
|
||||
&self.discord_username,
|
||||
|
||||
&self.is_for_private_use,
|
||||
&self.is_for_public_use,
|
||||
|
||||
&self.is_for_studio,
|
||||
&self.is_for_twitch_tts,
|
||||
&self.is_for_api_use,
|
||||
&self.is_for_music,
|
||||
&self.is_for_games,
|
||||
&self.is_for_other,
|
||||
&self.optional_notes_on_use,
|
||||
|
||||
&self.is_own_voice,
|
||||
&self.is_third_party_voice,
|
||||
&self.optional_notes_on_subject,
|
||||
|
||||
&self.has_clean_audio_recordings,
|
||||
&self.has_good_microphone,
|
||||
|
||||
&self.optional_questions,
|
||||
&self.optional_extra_comments,
|
||||
|
||||
&self.ip_address_creation,
|
||||
);
|
||||
|
||||
let query_result = query.execute(mysql_pool).await;
|
||||
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => res.last_insert_id(),
|
||||
Err(err) => return Err(anyhow!("Error creating Voice Clone Request: {:?}", err)),
|
||||
};
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod get_voice_clone_request;
|
||||
pub mod insert_voice_clone_requests_builder;
|
||||
@@ -17,10 +17,6 @@ impl RedisCacheKeys {
|
||||
format!("get_tts_inf2:{}", model_token)
|
||||
}
|
||||
|
||||
pub fn get_unified_queue_stats_endpoint() -> &'static str {
|
||||
"q_stats_2" // NB: Schema changed, hence _2
|
||||
}
|
||||
|
||||
pub fn session_record_light(session_token: &str) -> String {
|
||||
format!("ses_light:{}", session_token)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
/// Maximum serialized string length for database storage.
|
||||
pub const MAX_LENGTH: usize = 16;
|
||||
|
||||
/// Used in the `debug_logs` table in a `VARCHAR(16)` field (`maybe_log_level`).
|
||||
///
|
||||
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DebugLogLevel {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(DebugLogLevel);
|
||||
impl_mysql_enum_coders!(DebugLogLevel);
|
||||
impl_mysql_from_row!(DebugLogLevel);
|
||||
|
||||
impl DebugLogLevel {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Info => "info",
|
||||
Self::Warn => "warn",
|
||||
Self::Error => "error",
|
||||
Self::Debug => "debug",
|
||||
Self::Trace => "trace",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
||||
match value {
|
||||
"info" => Ok(Self::Info),
|
||||
"warn" => Ok(Self::Warn),
|
||||
"error" => Ok(Self::Error),
|
||||
"debug" => Ok(Self::Debug),
|
||||
"trace" => Ok(Self::Trace),
|
||||
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::Info,
|
||||
Self::Warn,
|
||||
Self::Error,
|
||||
Self::Debug,
|
||||
Self::Trace,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use crate::by_table::debug_logs::debug_log_level::MAX_LENGTH;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(DebugLogLevel::Info, "info");
|
||||
assert_serialization(DebugLogLevel::Warn, "warn");
|
||||
assert_serialization(DebugLogLevel::Error, "error");
|
||||
assert_serialization(DebugLogLevel::Debug, "debug");
|
||||
assert_serialization(DebugLogLevel::Trace, "trace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(DebugLogLevel::Info.to_str(), "info");
|
||||
assert_eq!(DebugLogLevel::Warn.to_str(), "warn");
|
||||
assert_eq!(DebugLogLevel::Error.to_str(), "error");
|
||||
assert_eq!(DebugLogLevel::Debug.to_str(), "debug");
|
||||
assert_eq!(DebugLogLevel::Trace.to_str(), "trace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(DebugLogLevel::from_str("info").unwrap(), DebugLogLevel::Info);
|
||||
assert_eq!(DebugLogLevel::from_str("warn").unwrap(), DebugLogLevel::Warn);
|
||||
assert_eq!(DebugLogLevel::from_str("error").unwrap(), DebugLogLevel::Error);
|
||||
assert_eq!(DebugLogLevel::from_str("debug").unwrap(), DebugLogLevel::Debug);
|
||||
assert_eq!(DebugLogLevel::from_str("trace").unwrap(), DebugLogLevel::Trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_err() {
|
||||
let result = DebugLogLevel::from_str("invalid");
|
||||
assert!(result.is_err());
|
||||
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
|
||||
assert_eq!(value, "invalid");
|
||||
} else {
|
||||
panic!("Expected EnumError::CouldNotConvertFromString");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = DebugLogLevel::all_variants();
|
||||
assert_eq!(variants.len(), 5);
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Info));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Warn));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Error));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Debug));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Trace));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(DebugLogLevel::all_variants().len(), DebugLogLevel::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in DebugLogLevel::all_variants() {
|
||||
assert_eq!(variant, DebugLogLevel::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, DebugLogLevel::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, DebugLogLevel::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_length() {
|
||||
for variant in DebugLogLevel::all_variants() {
|
||||
assert!(variant.to_str().len() <= MAX_LENGTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,16 @@ pub const MAX_LENGTH: usize = 24;
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DebugLogType {
|
||||
// ========== First party ==========
|
||||
|
||||
/// A user request, typically the Json<T> actix_web handlers read.
|
||||
HttpRequest,
|
||||
|
||||
/// A backend/pipeline failure while serving a request.
|
||||
BackendFailure,
|
||||
|
||||
// ========== Third party ==========
|
||||
|
||||
FalRequest,
|
||||
KinoviRequest,
|
||||
GrokApiRequest,
|
||||
@@ -34,6 +43,7 @@ impl DebugLogType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::HttpRequest => "http_request",
|
||||
Self::BackendFailure => "backend_failure",
|
||||
Self::FalRequest => "fal_request",
|
||||
Self::KinoviRequest => "kinovi_request",
|
||||
Self::GrokApiRequest => "grok_api_request",
|
||||
@@ -45,6 +55,7 @@ impl DebugLogType {
|
||||
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
||||
match value {
|
||||
"http_request" => Ok(Self::HttpRequest),
|
||||
"backend_failure" => Ok(Self::BackendFailure),
|
||||
"fal_request" => Ok(Self::FalRequest),
|
||||
"kinovi_request" => Ok(Self::KinoviRequest),
|
||||
"grok_api_request" => Ok(Self::GrokApiRequest),
|
||||
@@ -57,6 +68,7 @@ impl DebugLogType {
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::HttpRequest,
|
||||
Self::BackendFailure,
|
||||
Self::FalRequest,
|
||||
Self::KinoviRequest,
|
||||
Self::GrokApiRequest,
|
||||
@@ -79,6 +91,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(DebugLogType::HttpRequest, "http_request");
|
||||
assert_serialization(DebugLogType::BackendFailure, "backend_failure");
|
||||
assert_serialization(DebugLogType::FalRequest, "fal_request");
|
||||
assert_serialization(DebugLogType::KinoviRequest, "kinovi_request");
|
||||
assert_serialization(DebugLogType::GrokApiRequest, "grok_api_request");
|
||||
@@ -89,6 +102,7 @@ mod tests {
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(DebugLogType::HttpRequest.to_str(), "http_request");
|
||||
assert_eq!(DebugLogType::BackendFailure.to_str(), "backend_failure");
|
||||
assert_eq!(DebugLogType::FalRequest.to_str(), "fal_request");
|
||||
assert_eq!(DebugLogType::KinoviRequest.to_str(), "kinovi_request");
|
||||
assert_eq!(DebugLogType::GrokApiRequest.to_str(), "grok_api_request");
|
||||
@@ -99,6 +113,7 @@ mod tests {
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(DebugLogType::from_str("http_request").unwrap(), DebugLogType::HttpRequest);
|
||||
assert_eq!(DebugLogType::from_str("backend_failure").unwrap(), DebugLogType::BackendFailure);
|
||||
assert_eq!(DebugLogType::from_str("fal_request").unwrap(), DebugLogType::FalRequest);
|
||||
assert_eq!(DebugLogType::from_str("kinovi_request").unwrap(), DebugLogType::KinoviRequest);
|
||||
assert_eq!(DebugLogType::from_str("grok_api_request").unwrap(), DebugLogType::GrokApiRequest);
|
||||
@@ -120,8 +135,9 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = DebugLogType::all_variants();
|
||||
assert_eq!(variants.len(), 6);
|
||||
assert_eq!(variants.len(), 7);
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::HttpRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::BackendFailure));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::FalRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::KinoviRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::GrokApiRequest));
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod debug_log_level;
|
||||
pub mod debug_log_type;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "trace_id"
|
||||
edition = "2018"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "trace_id"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
rand = { version = "0.8.5", features = ["std_rng"] }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tokio.workspace = true
|
||||
@@ -0,0 +1,156 @@
|
||||
//! trace_id
|
||||
//!
|
||||
//! Request-scoped trace identifiers for observability.
|
||||
//!
|
||||
//! A `TraceId` is generated for every inbound HTTP request (see the
|
||||
//! `trace_id_middleware` in `storyteller-web`), stored in a tokio task-local,
|
||||
//! and emitted with every log line and pager notification produced while
|
||||
//! serving that request.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// The string prefix for trace ids.
|
||||
pub const TRACE_ID_PREFIX: &str = "trace_";
|
||||
|
||||
/// Number of Crockford base32 characters after the prefix.
|
||||
/// 26 characters × 5 bits = 130 bits of entropy (≥ the 128-bit industry
|
||||
/// standard for trace ids). Total length: 6 + 26 = 32 characters.
|
||||
pub const TRACE_ID_ENTROPY_CHARS: usize = 26;
|
||||
|
||||
/// Total serialized length of a trace id ("trace_" + entropy).
|
||||
pub const TRACE_ID_TOTAL_LENGTH: usize = 32;
|
||||
|
||||
/// Crockford base32 alphabet, lowercase (no i, l, o, u).
|
||||
const CROCKFORD_LOWERCASE_CHARSET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
|
||||
|
||||
/// A unique-per-request trace identifier, eg. `trace_1h3x9k2m4p7q8r5t6v0w2y3z4a`.
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
|
||||
pub struct TraceId(String);
|
||||
|
||||
impl TraceId {
|
||||
/// Generate a new random trace id. Infallible.
|
||||
#[inline]
|
||||
pub fn generate() -> Self {
|
||||
use rand::Rng;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let entropy: String = (0..TRACE_ID_ENTROPY_CHARS)
|
||||
.map(|_| {
|
||||
let idx = rng.gen_range(0..CROCKFORD_LOWERCASE_CHARSET.len());
|
||||
CROCKFORD_LOWERCASE_CHARSET[idx] as char
|
||||
})
|
||||
.collect();
|
||||
|
||||
TraceId(format!("{}{}", TRACE_ID_PREFIX, entropy))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TraceId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TraceId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
tokio::task_local! {
|
||||
/// The trace id for the currently-executing request, if any.
|
||||
///
|
||||
/// Set by the HTTP middleware via [`TRACE_ID_TASK_LOCAL.scope(...)`], and
|
||||
/// readable from anywhere within the request's task — including the
|
||||
/// logger's format closure and the pager — via [`current_trace_id`].
|
||||
pub static TRACE_ID_TASK_LOCAL: TraceId;
|
||||
}
|
||||
|
||||
/// The trace id of the currently-executing request, if one is in scope.
|
||||
///
|
||||
/// Returns `None` outside of a request context (startup, background threads,
|
||||
/// detached `tokio::spawn` tasks that weren't explicitly re-scoped).
|
||||
#[inline]
|
||||
pub fn current_trace_id() -> Option<TraceId> {
|
||||
TRACE_ID_TASK_LOCAL.try_with(|trace_id| trace_id.clone()).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod generation {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn total_length_is_32() {
|
||||
assert_eq!(TraceId::generate().as_str().len(), TRACE_ID_TOTAL_LENGTH);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_prefix() {
|
||||
assert!(TraceId::generate().as_str().starts_with(TRACE_ID_PREFIX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entropy_uses_crockford_lowercase_only() {
|
||||
let trace_id = TraceId::generate();
|
||||
let entropy = &trace_id.as_str()[TRACE_ID_PREFIX.len()..];
|
||||
assert_eq!(entropy.len(), TRACE_ID_ENTROPY_CHARS);
|
||||
for c in entropy.chars() {
|
||||
assert!(
|
||||
CROCKFORD_LOWERCASE_CHARSET.contains(&(c as u8)),
|
||||
"unexpected character: {}",
|
||||
c
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ids_are_unique() {
|
||||
let a = TraceId::generate();
|
||||
let b = TraceId::generate();
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
mod task_local {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn absent_outside_scope() {
|
||||
assert!(current_trace_id().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn present_inside_scope() {
|
||||
let trace_id = TraceId::generate();
|
||||
let expected = trace_id.clone();
|
||||
TRACE_ID_TASK_LOCAL
|
||||
.scope(trace_id, async move {
|
||||
assert_eq!(current_trace_id(), Some(expected));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn survives_await_points() {
|
||||
let trace_id = TraceId::generate();
|
||||
let expected = trace_id.clone();
|
||||
TRACE_ID_TASK_LOCAL
|
||||
.scope(trace_id, async move {
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(current_trace_id(), Some(expected));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ seedance2pro_client.workspace = true
|
||||
server_environment = { path = "../../../lib/server_environment" }
|
||||
thumbnail_generator = { path = "../../../api_clients/thumbnail_generator" }
|
||||
tokens.workspace = true
|
||||
trace_id.workspace = true
|
||||
tts_common = { path = "../../../lib/deprecated/tts_common" }
|
||||
url_config = { path = "../../../lib/url_config" }
|
||||
url_utils = { path = "../../../lib/url_utils" }
|
||||
|
||||
@@ -16,7 +16,6 @@ use crate::http_server::common_responses::tag_info::TagInfo;
|
||||
use crate::http_server::common_responses::user_details_lite::{UserDefaultAvatarInfo, UserDetailsLight};
|
||||
use crate::http_server::deprecated_endpoints::conversion::enqueue_fbx_to_gltf_handler::*;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::*;
|
||||
use crate::http_server::deprecated_endpoints::image_gen::enqueue_image_generation::EnqueueImageGenRequestSuccessResponse;
|
||||
use crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_face_fusion_workflow_handler::*;
|
||||
use crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_live_portrait_workflow_handler::*;
|
||||
use crate::http_server::deprecated_endpoints::workflows::enqueue::vst_common::vst_error::*;
|
||||
@@ -88,7 +87,10 @@ 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 artcraft_api_defs::generate::video::edit::beeble_switchx_edit_video::*;
|
||||
use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::*;
|
||||
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::*;
|
||||
use artcraft_api_defs::moderation::debug_logs::moderation_list_all_debug_logs::*;
|
||||
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_token::*;
|
||||
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_user::*;
|
||||
use crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::*;
|
||||
@@ -134,6 +136,7 @@ use artcraft_api_defs::moderation::user_spend_summaries::get_summary::*;
|
||||
use artcraft_api_defs::moderation::user_spend_summaries::reengagement_list::*;
|
||||
use artcraft_api_defs::moderation::user_spend_summaries::top_users_list::*;
|
||||
use artcraft_api_defs::moderation::user_daily_spends::user_daily_spends_list::*;
|
||||
use artcraft_api_defs::moderation::top_spenders::list::*;
|
||||
use artcraft_api_defs::moderation::user_spend_events::list::*;
|
||||
use artcraft_api_defs::folders::subfolder::*;
|
||||
use artcraft_api_defs::moderation::user_referrals::list_global_user_referrals::*;
|
||||
@@ -214,8 +217,6 @@ use billing_component::stripe::http_endpoints::checkout::create::stripe_create_c
|
||||
use crate::http_server::endpoints::billing_fakeyou::list_active_user_subscriptions_handler::*;
|
||||
use crate::http_server::endpoints::service::status_alert_handler::*;
|
||||
use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*;
|
||||
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::*;
|
||||
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::EnqueueStudioGen2Request;
|
||||
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::*;
|
||||
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::*;
|
||||
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::*;
|
||||
@@ -238,9 +239,6 @@ use crate::http_server::endpoints::users::login_handler::LoginErrorResponse;
|
||||
use crate::http_server::endpoints::users::logout_handler::*;
|
||||
use crate::http_server::endpoints::users::session_info_handler::*;
|
||||
use crate::http_server::endpoints::users::session_token_info_handler::*;
|
||||
use crate::http_server::endpoints::voice_conversion::enqueue_voice_conversion_inference_handler::*;
|
||||
use crate::http_server::endpoints::voice_designer::inference::enqueue_tts_request::*;
|
||||
use crate::http_server::endpoints::voice_designer::voice_datasets::list_datasets_by_user::*;
|
||||
use crate::http_server::endpoints::weights::delete::delete_weight_handler::*;
|
||||
use crate::http_server::endpoints::weights::get::get_weight_handler::*;
|
||||
use crate::http_server::endpoints::weights::list::list_available_weights_handler::*;
|
||||
@@ -385,7 +383,6 @@ use crate::http_server::endpoints::video_info::video_info_upload_handler::*;
|
||||
use crate::http_server::endpoints::video_info::video_info_notes_handler::*;
|
||||
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::*;
|
||||
use crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::*;
|
||||
use crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::*;
|
||||
use crate::http_server::endpoints::credits::get_session_credits_handler::*;
|
||||
use crate::http_server::endpoints::subscriptions::get_session_subscription_handler::*;
|
||||
use crate::http_server::endpoints::analytics::log_app_active_user_handler::*;
|
||||
@@ -399,6 +396,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::moderation::user_spend_summaries::moderator_reengagement_list_handler::moderator_reengagement_list_handler,
|
||||
crate::http_server::endpoints::moderation::user_spend_summaries::moderator_top_users_list_handler::moderator_top_users_list_handler,
|
||||
crate::http_server::endpoints::moderation::user_daily_spends::moderator_user_daily_spends_handler::moderator_user_daily_spends_handler,
|
||||
crate::http_server::endpoints::moderation::top_spenders::moderator_list_top_spenders_handler::moderator_list_top_spenders_handler,
|
||||
crate::http_server::endpoints::moderation::user_spend_events::moderator_list_user_spend_events_handler::moderator_list_user_spend_events_handler,
|
||||
billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::stripe_create_checkout_session_json_handler,
|
||||
crate::http_server::endpoints::billing_fakeyou::list_active_user_subscriptions_handler::list_active_user_subscriptions_handler,
|
||||
@@ -484,7 +482,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::prompts::get_prompt_handler::get_prompt_handler,
|
||||
crate::http_server::endpoints::service::status_alert_handler::status_alert_handler,
|
||||
crate::http_server::endpoints::stats::get_unified_queue_stats_handler::get_unified_queue_stats_handler,
|
||||
crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::enqueue_studio_gen2_handler,
|
||||
crate::http_server::endpoints::tags::list_tags_for_entity_handler::list_tags_for_entity_handler,
|
||||
crate::http_server::endpoints::tags::set_tags_for_entity_handler::set_tags_for_entity_handler,
|
||||
crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::enqueue_infer_tts_handler,
|
||||
@@ -506,9 +503,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::users::logout_handler::logout_handler,
|
||||
crate::http_server::endpoints::users::session_info_handler::session_info_handler,
|
||||
crate::http_server::endpoints::users::session_token_info_handler::session_token_info_handler,
|
||||
crate::http_server::endpoints::voice_conversion::enqueue_voice_conversion_inference_handler::enqueue_voice_conversion_inference_handler,
|
||||
crate::http_server::endpoints::voice_designer::inference::enqueue_tts_request::enqueue_tts_request,
|
||||
crate::http_server::endpoints::voice_designer::voice_datasets::list_datasets_by_user::list_datasets_by_user_handler,
|
||||
crate::http_server::endpoints::weights::delete::delete_weight_handler::delete_weight_handler,
|
||||
crate::http_server::endpoints::weights::get::get_weight_handler::get_weight_handler,
|
||||
crate::http_server::endpoints::weights::list::list_available_weights_handler::list_available_weights_handler,
|
||||
@@ -564,9 +558,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::analytics::log_app_active_user_handler::log_app_active_user_handler,
|
||||
crate::http_server::endpoints::analytics::log_app_active_user_json_handler::log_app_active_user_json_handler,
|
||||
// Voice Conversion
|
||||
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,
|
||||
@@ -602,7 +594,9 @@ 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::debug_logs::moderation_list_all_debug_logs_handler::moderation_list_all_debug_logs_handler,
|
||||
crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::moderation_list_debug_logs_for_token_handler,
|
||||
crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_user_handler::moderation_list_debug_logs_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::moderation_get_job_by_token_handler,
|
||||
crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler,
|
||||
crate::http_server::endpoints::moderation::user_referrals::moderator_list_global_user_referrals_handler::moderator_list_global_user_referrals_handler,
|
||||
@@ -663,6 +657,9 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
TopUserEntry,
|
||||
TopUsersWindow,
|
||||
ModeratorUserDailySpendsResponse,
|
||||
ModeratorListTopSpendersResponse,
|
||||
TopSpenderEntry,
|
||||
TopSpendersWindow,
|
||||
ModeratorListUserSpendEventsResponse,
|
||||
UserSpendEventListEntry,
|
||||
UserDailySpendEntry,
|
||||
@@ -922,21 +919,13 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
EnqueueFbxToGltfRequestSuccessResponse,
|
||||
EnqueueGptSovitsModelDownloadRequest,
|
||||
EnqueueGptSovitsModelDownloadSuccessResponse,
|
||||
EnqueueImageGenRequestSuccessResponse,
|
||||
EnqueueLivePortraitCropDimensions,
|
||||
EnqueueLivePortraitWorkflowRequest,
|
||||
EnqueueLivePortraitWorkflowSuccessResponse,
|
||||
EnqueueStudioGen2Request,
|
||||
EnqueueStudioGen2Response,
|
||||
EnqueueTTSRequest,
|
||||
EnqueueTTSRequestSuccessResponse,
|
||||
EnqueueVideoStyleTransferRequest,
|
||||
EnqueueVideoStyleTransferSuccessResponse,
|
||||
EnqueueVoiceConversionInferenceRequest,
|
||||
EnqueueVoiceConversionInferenceSuccessResponse,
|
||||
FeaturedMediaFile,
|
||||
FeaturedModelWeightForList,
|
||||
FundamentalFrequencyMethod,
|
||||
GenerateFlux1DevTextToImageAspectRatio,
|
||||
GenerateFlux1DevTextToImageNumImages,
|
||||
GenerateFlux1DevTextToImageRequest,
|
||||
@@ -994,15 +983,12 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
GetProfilePathInfo,
|
||||
GetPromptPathInfo,
|
||||
GetPromptSuccessResponse,
|
||||
GetUnifiedQueueStatsError,
|
||||
GetUnifiedQueueStatsSuccessResponse,
|
||||
GetUserRatingResponse,
|
||||
GetWeightPathInfo,
|
||||
GetWeightResponse,
|
||||
GoogleCreateAccountRequest,
|
||||
GoogleCreateAccountSuccessResponse,
|
||||
InferTtsError,
|
||||
InferTtsRequest,
|
||||
InferTtsSuccessResponse,
|
||||
InferenceJobStatusResponsePayload,
|
||||
InferenceJobTokenType,
|
||||
@@ -1016,9 +1002,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListCommentsError,
|
||||
ListCommentsPathInfo,
|
||||
ListCommentsSuccessResponse,
|
||||
ListDatasetsByUserError,
|
||||
ListDatasetsByUserPathInfo,
|
||||
ListDatasetsByUserSuccessResponse,
|
||||
ListFeaturedMediaFilesQueryParams,
|
||||
ListFeaturedMediaFilesSuccessResponse,
|
||||
ListFeaturedWeightsError,
|
||||
@@ -1030,10 +1013,17 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
GetJobByTokenPathInfo,
|
||||
GetJobByTokenSuccessResponse,
|
||||
ModerationJobResponse,
|
||||
ListDebugLogsPathInfo,
|
||||
ListDebugLogsQueryParams,
|
||||
ListDebugLogsSuccessResponse,
|
||||
DebugLogEntry,
|
||||
ModerationDebugLogEntry,
|
||||
ModerationDebugLogUser,
|
||||
ModerationListAllDebugLogsEntry,
|
||||
ModerationListAllDebugLogsQueryParams,
|
||||
ModerationListAllDebugLogsSuccessResponse,
|
||||
ModerationListDebugLogsForTokenPathInfo,
|
||||
ModerationListDebugLogsForTokenQueryParams,
|
||||
ModerationListDebugLogsForTokenSuccessResponse,
|
||||
ModerationListDebugLogsForUserPathInfo,
|
||||
ModerationListDebugLogsForUserQueryParams,
|
||||
ModerationListDebugLogsForUserSuccessResponse,
|
||||
ListStaffAuditLogsQueryParams,
|
||||
ListStaffAuditLogsSuccessResponse,
|
||||
ListImpersonationRequestsQueryParams,
|
||||
@@ -1182,7 +1172,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
WeightsData,
|
||||
WriteEngineAssetMediaSuccessResponse,
|
||||
WriteSceneFileMediaSuccessResponse,
|
||||
ZsDatasetRecord,
|
||||
|
||||
// Cost Estimate types
|
||||
EstimateImageCostRequest,
|
||||
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Deserialize, Serialize, ToSchema)]
|
||||
pub enum EnqueueImageGenType {
|
||||
#[serde(rename = "upload_lora")]
|
||||
UploadLoRA,
|
||||
#[serde(rename = "upload_sd")]
|
||||
Checkpoint,
|
||||
#[serde(rename = "inference")]
|
||||
Inference,
|
||||
}
|
||||
|
||||
impl EnqueueImageGenType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::UploadLoRA => "upload_lora",
|
||||
Self::Checkpoint => "upload_sd",
|
||||
Self::Inference => "inference",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"upload_lora" => Ok(Self::UploadLoRA),
|
||||
"upload_sd" => Ok(Self::Checkpoint),
|
||||
"inference" => Ok(Self::Inference),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::UploadLoRA,
|
||||
Self::Checkpoint,
|
||||
Self::Inference
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_to_str() {
|
||||
assert_eq!(EnqueueImageGenType::UploadLoRA.to_str(), "upload_lora");
|
||||
assert_eq!(EnqueueImageGenType::Checkpoint.to_str(), "upload_sd");
|
||||
assert_eq!(EnqueueImageGenType::Inference.to_str(), "inference");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(EnqueueImageGenType::from_str("upload_lora").unwrap(), EnqueueImageGenType::UploadLoRA);
|
||||
assert_eq!(EnqueueImageGenType::from_str("upload_sd").unwrap(), EnqueueImageGenType::Checkpoint);
|
||||
assert_eq!(EnqueueImageGenType::from_str("inference").unwrap(), EnqueueImageGenType::Inference);
|
||||
assert!(EnqueueImageGenType::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_variants() {
|
||||
let variants = EnqueueImageGenType::all_variants();
|
||||
assert_eq!(variants.len(), 3);
|
||||
assert!(variants.contains(&EnqueueImageGenType::UploadLoRA));
|
||||
assert!(variants.contains(&EnqueueImageGenType::Checkpoint));
|
||||
assert!(variants.contains(&EnqueueImageGenType::Inference));
|
||||
}
|
||||
}
|
||||
-560
@@ -1,560 +0,0 @@
|
||||
#![forbid(unused_mut)]
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::error;
|
||||
use log::warn;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
use web::Data;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{
|
||||
GenericInferenceArgs,
|
||||
InferenceCategoryAbbreviated,
|
||||
PolymorphicInferenceArgs,
|
||||
};
|
||||
use mysql_queries::payloads::generic_inference_args::inner_payloads::image_generation_payload::StableDiffusionArgs;
|
||||
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{
|
||||
insert_generic_inference_job,
|
||||
InsertGenericInferenceArgs,
|
||||
};
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// This is the number of images (batch size) to generate for each request.
|
||||
/// We should allow all users to have multiple images generated at once as this
|
||||
/// is what other providers do.
|
||||
const MINIMUM_IMAGE_COUNT : u32 = 4;
|
||||
|
||||
/// The maximum number of images (batch size) to generate for each request.
|
||||
const MAXIMUM_IMAGE_COUNT : u32 = 8;
|
||||
|
||||
/// Debug requests can get routed to special "debug-only" workers, which can
|
||||
/// be used to trial new code, run debugging, etc.
|
||||
const DEBUG_HEADER_NAME: &str = "enable-debug-mode";
|
||||
|
||||
/// The routing tag header can send workloads to particular k8s hosts.
|
||||
/// This is useful for catching the live logs or intercepting the job.
|
||||
const ROUTING_TAG_HEADER_NAME: &str = "routing-tag";
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum TypeOfInference {
|
||||
#[serde(rename = "lora")]
|
||||
Lora,
|
||||
#[serde(rename = "model")]
|
||||
Model,
|
||||
#[serde(rename = "inference")]
|
||||
Inference
|
||||
}
|
||||
impl Display for TypeOfInference {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
TypeOfInference::Lora => write!(f, "lora"),
|
||||
TypeOfInference::Model => write!(f, "model"),
|
||||
TypeOfInference::Inference => write!(f, "inference"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid_string(input: &str) -> bool {
|
||||
let valid_strings = [
|
||||
"DPM++ 2M Karras",
|
||||
"DPM++ SDE Karras",
|
||||
"DPM++ 2M SDE Exponential",
|
||||
"DPM++ 2M SDE Karras",
|
||||
"Euler a",
|
||||
"Euler",
|
||||
"LMS",
|
||||
"Heun",
|
||||
"DPM2",
|
||||
"DPM2 a",
|
||||
"DPM++ 2S a",
|
||||
"DPM++ 2M",
|
||||
"DPM++ SDE",
|
||||
"DPM++ 2M SDE",
|
||||
"DPM++ 2M SDE Heun",
|
||||
"DPM++ 2M SDE Heun Karras",
|
||||
"DPM++ 2M SDE Heun Exponential",
|
||||
"DPM++ 3M SDE",
|
||||
"DPM++ 3M SDE Karras",
|
||||
"DPM++ 3M SDE Exponential",
|
||||
"DPM fast",
|
||||
"DPM adaptive",
|
||||
"LMS Karras",
|
||||
"DPM2 Karras",
|
||||
"DPM2 a Karras",
|
||||
"DPM++ 2S a Karras",
|
||||
];
|
||||
valid_strings.contains(&input)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, ToSchema)]
|
||||
pub struct EnqueueImageGenRequest {
|
||||
pub uuid_idempotency_token: String,
|
||||
pub maybe_image_source: Option<String>,
|
||||
pub maybe_sd_model_token: Option<String>,
|
||||
pub maybe_lora_model_token: Option<String>,
|
||||
pub maybe_prompt: Option<String>,
|
||||
pub maybe_n_prompt: Option<String>,
|
||||
pub maybe_seed: Option<i64>,
|
||||
pub maybe_width: Option<u32>,
|
||||
pub maybe_height: Option<u32>,
|
||||
pub maybe_sampler: Option<String>,
|
||||
pub maybe_upload_path: Option<String>,
|
||||
pub maybe_lora_upload_path: Option<String>,
|
||||
pub maybe_cfg_scale: Option<u32>,
|
||||
pub maybe_number_of_samples: Option<u32>,
|
||||
pub maybe_batch_count: Option<u32>,
|
||||
pub maybe_name: Option<String>,
|
||||
pub maybe_description: Option<String>,
|
||||
pub maybe_version: Option<u32>,
|
||||
|
||||
// Optional cover image on LoRA or SD upload
|
||||
pub maybe_cover_image_media_file_token: Option<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct EnqueueImageGenRequestSuccessResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
// Implementation for enqueuing a TTS request
|
||||
// Reference enqueue_infer_tts_handler.rs for checks: rate limiting / user sessions
|
||||
// insert generic inference job.rs
|
||||
// Need to convert it to generic inference job.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/inference/enqueue_image_gen/",
|
||||
responses(
|
||||
(
|
||||
status = 200,
|
||||
description = "Enqueue TTS generically",
|
||||
body = EnqueueImageGenRequestSuccessResponse,
|
||||
),
|
||||
(status = 400, description = "Bad input", body = CommonWebError),
|
||||
(status = 401, description = "Not authorized", body = CommonWebError),
|
||||
(status = 429, description = "Rate limited", body = CommonWebError),
|
||||
(status = 500, description = "Server error", body = CommonWebError)
|
||||
),
|
||||
params(("request" = EnqueueImageGenRequest, description = "Payload for TTS Request"))
|
||||
)]
|
||||
pub async fn enqueue_image_generation_request(
|
||||
http_request: HttpRequest,
|
||||
request: Json<EnqueueImageGenRequest>,
|
||||
server_state: Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, CommonWebError> {
|
||||
|
||||
let inference_mode = inference_mode_from_http_request(&http_request)
|
||||
.ok_or(CommonWebError::BadInputWithSimpleMessage("Invalid request".to_string()))?;
|
||||
|
||||
validate_request(&request, inference_mode)?;
|
||||
|
||||
// TODO: Brandon need to figure out premium vs not premium
|
||||
|
||||
let mut maybe_user_token: Option<UserToken> = None;
|
||||
let visbility = enums::common::visibility::Visibility::Public;
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// ==================== USER SESSION ==================== //
|
||||
|
||||
let maybe_user_session = server_state.session_checker
|
||||
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection).await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
|
||||
if let Some(user_session) = maybe_user_session.as_ref() {
|
||||
maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token));
|
||||
}
|
||||
|
||||
// ==================== PLANS ==================== //
|
||||
|
||||
// Plan should handle "first anonymous use" and "investor" cases.
|
||||
let plan = get_correct_plan_for_session(
|
||||
server_state.server_environment,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
// Separate priority for animation.
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
// ==================== DEBUG MODE + ROUTING TAG ==================== //
|
||||
|
||||
let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME).is_some();
|
||||
|
||||
let maybe_routing_tag = get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME).map(
|
||||
|routing_tag| routing_tag.trim().to_string()
|
||||
);
|
||||
|
||||
|
||||
// ==================== BANNED USERS ==================== //
|
||||
|
||||
if let Some(ref user) = maybe_user_session {
|
||||
if user.role.is_banned {
|
||||
return Err(CommonWebError::NotAuthorized);
|
||||
}
|
||||
}
|
||||
|
||||
// DETECT premium user and queue
|
||||
|
||||
// ==================== RATE LIMIT ==================== //
|
||||
|
||||
let rate_limiter = match maybe_user_session {
|
||||
None => &server_state.redis_rate_limiters.logged_out,
|
||||
Some(ref _user) => &server_state.redis_rate_limiters.logged_in,
|
||||
};
|
||||
|
||||
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
|
||||
return Err(CommonWebError::TooManyRequests);
|
||||
}
|
||||
|
||||
// Get up IP address
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
// Check the inference args to make sure everything is all there for upload loRA / model or standard inference
|
||||
|
||||
// ==================== HANDLE IDEMPOTENCY ==================== //
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
// ==================== INFERENCE ARGS ==================== //
|
||||
|
||||
let width = request.maybe_width.unwrap_or(512);
|
||||
let height = request.maybe_height.unwrap_or(512);
|
||||
|
||||
let width = match width {
|
||||
0..=512 => 512,
|
||||
513..=768 => 768,
|
||||
769..=1024 => 1024,
|
||||
_ => 1024,
|
||||
};
|
||||
|
||||
let height = match height {
|
||||
0..=512 => 512,
|
||||
513..=768 => 768,
|
||||
769..=1024 => 1024,
|
||||
_ => 1024,
|
||||
};
|
||||
|
||||
let cfg_scale = match request.maybe_cfg_scale {
|
||||
Some(val) => if val > 32 { 32 } else { val }
|
||||
None => 7,
|
||||
};
|
||||
|
||||
let number_of_samples = match request.maybe_number_of_samples {
|
||||
Some(val) => if val > 128 { 128 } else { val }
|
||||
None => 20,
|
||||
};
|
||||
|
||||
let batch_count = match request.maybe_batch_count {
|
||||
None => MINIMUM_IMAGE_COUNT, // NB: Default to "3" images for everyone
|
||||
Some(0...MINIMUM_IMAGE_COUNT) => MINIMUM_IMAGE_COUNT,
|
||||
Some(val) => if val > MAXIMUM_IMAGE_COUNT { MAXIMUM_IMAGE_COUNT } else { val }
|
||||
};
|
||||
|
||||
let sampler = match request.maybe_sampler.clone() {
|
||||
Some(val) => if is_valid_string(&val) { val.clone() } else { String::from("Euler a") }
|
||||
None => String::from("Euler a"),
|
||||
};
|
||||
|
||||
let sd_weight_token = ModelWeightToken(
|
||||
request.maybe_sd_model_token.clone().unwrap_or_default()
|
||||
);
|
||||
|
||||
let lora_token = ModelWeightToken(request.maybe_lora_model_token.clone().unwrap_or_default());
|
||||
|
||||
let n_prompt = request.maybe_n_prompt.clone().unwrap_or_default();
|
||||
|
||||
// we can only do 1 upload type at a time.
|
||||
// if we are uploading a model.
|
||||
let mut maybe_sd_upload_url = None;
|
||||
let mut maybe_lora_upload_url = None;
|
||||
|
||||
let both_fields = (
|
||||
request.maybe_upload_path.as_deref(),
|
||||
request.maybe_lora_upload_path.as_deref()
|
||||
);
|
||||
|
||||
match both_fields {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Can't upload both lora and model".to_string()));
|
||||
}
|
||||
(Some(sd_url), None) => {
|
||||
maybe_sd_upload_url = Some(sd_url.to_string());
|
||||
}
|
||||
(None, Some(lora_url)) => {
|
||||
maybe_lora_upload_url = Some(lora_url.to_string());
|
||||
}
|
||||
_ => {}, // No-op for inference
|
||||
}
|
||||
|
||||
// NB: This is done to populate the new top-level field in the jobs table.
|
||||
// We won't read this yet, but in the meantime it can serve as analytics, and in the future
|
||||
// we can switch to it.
|
||||
let maybe_either_download_url = maybe_sd_upload_url.as_deref()
|
||||
.or(maybe_lora_upload_url.as_deref())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let seed = request.maybe_seed.unwrap_or(-1);
|
||||
|
||||
let mut version : u32 = 0;
|
||||
|
||||
let type_of_inference = match inference_mode {
|
||||
TypeOfInference::Lora => "lora",
|
||||
TypeOfInference::Model => "model",
|
||||
TypeOfInference::Inference => "inference",
|
||||
};
|
||||
|
||||
if let Some(s) = request.maybe_version {
|
||||
version = s;
|
||||
}
|
||||
|
||||
let inference_args = StableDiffusionArgs {
|
||||
maybe_sd_model_token: Some(sd_weight_token),
|
||||
maybe_lora_model_token: Some(lora_token),
|
||||
maybe_prompt: request.maybe_prompt.clone(),
|
||||
maybe_n_prompt: request.maybe_n_prompt.clone(),
|
||||
maybe_seed: Some(seed),
|
||||
maybe_upload_path: maybe_sd_upload_url,
|
||||
maybe_lora_upload_path: maybe_lora_upload_url,
|
||||
type_of_inference: type_of_inference.to_string(),
|
||||
maybe_cfg_scale: Some(cfg_scale),
|
||||
maybe_number_of_samples: Some(number_of_samples),
|
||||
maybe_batch_count: Some(batch_count),
|
||||
maybe_width: Some(width),
|
||||
maybe_height: Some(height),
|
||||
maybe_sampler: Some(sampler),
|
||||
maybe_description: request.maybe_description.clone(),
|
||||
maybe_name: request.maybe_name.clone(),
|
||||
maybe_version: Some(version),
|
||||
};
|
||||
|
||||
// create the inference args here
|
||||
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
|
||||
|
||||
// create the job record here!
|
||||
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
job_type: InferenceJobType::StableDiffusion,
|
||||
maybe_product_category: None, // This is not a product anymore
|
||||
inference_category: InferenceCategory::ImageGeneration,
|
||||
maybe_model_type: Some(InferenceModelType::StableDiffusion), // NB: Model is static during inference
|
||||
maybe_model_token: None, // NB: Model is static during inference
|
||||
maybe_input_source_token: None,
|
||||
maybe_input_source_token_type: None,
|
||||
maybe_download_url: maybe_either_download_url.as_deref(),
|
||||
maybe_cover_image_media_file_token: request.maybe_cover_image_media_file_token.as_ref(),
|
||||
maybe_raw_inference_text: None,
|
||||
maybe_max_duration_seconds: None,
|
||||
maybe_inference_args: Some(GenericInferenceArgs {
|
||||
inference_category: Some(InferenceCategoryAbbreviated::ImageGeneration),
|
||||
args: Some(PolymorphicInferenceArgs::Ig(inference_args)),
|
||||
}),
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: visbility,
|
||||
priority_level,
|
||||
requires_keepalive: false, //reverse ... TODO fix this. we set it base on account is premium or not ...
|
||||
is_debug_request,
|
||||
maybe_routing_tag: maybe_routing_tag.as_deref(),
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match query_result {
|
||||
Ok((job_token, _id)) => job_token,
|
||||
Err(err) => {
|
||||
warn!("New generic inference job creation DB error: {:?}", err);
|
||||
if err.had_duplicate_idempotency_token() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Duplicate idempotency token".to_string()));
|
||||
}
|
||||
return Err(CommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
let response: EnqueueImageGenRequestSuccessResponse = EnqueueImageGenRequestSuccessResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
};
|
||||
|
||||
let body = serde_json
|
||||
::to_string(&response)
|
||||
?;
|
||||
|
||||
// Error handling 101 rust result type returned like so.
|
||||
Ok(HttpResponse::Ok().content_type("application/json").body(body))
|
||||
}
|
||||
|
||||
fn validate_request(
|
||||
request: &Json<EnqueueImageGenRequest>,
|
||||
inference_mode: TypeOfInference
|
||||
) -> Result<(), CommonWebError> {
|
||||
|
||||
let mut requires_name = false;
|
||||
let mut requires_description = false;
|
||||
let mut requires_sd_model_token = false;
|
||||
let mut requires_lora_upload_path = false;
|
||||
let mut requires_upload_path = false;
|
||||
|
||||
match inference_mode {
|
||||
TypeOfInference::Lora => {
|
||||
requires_name = true;
|
||||
requires_description = true;
|
||||
requires_lora_upload_path = true;
|
||||
}
|
||||
TypeOfInference::Model => {
|
||||
requires_name = true;
|
||||
requires_description = true;
|
||||
requires_upload_path = true;
|
||||
}
|
||||
TypeOfInference::Inference => {
|
||||
requires_sd_model_token = true;
|
||||
}
|
||||
}
|
||||
|
||||
if requires_name && request.maybe_name.is_none() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Missing Model / Lora Name".to_string()));
|
||||
}
|
||||
if requires_description && request.maybe_description.is_none() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Missing Model / Lora Description".to_string()));
|
||||
}
|
||||
if requires_sd_model_token && request.maybe_sd_model_token.is_none() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Missing Model Token".to_string()));
|
||||
}
|
||||
if requires_lora_upload_path && request.maybe_lora_upload_path.is_none() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Missing Lora Upload Path".to_string()));
|
||||
}
|
||||
if requires_upload_path && request.maybe_upload_path.is_none() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Missing Model Upload Path".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fn inference_mode_from_http_request(http_request: &HttpRequest) -> Option<TypeOfInference> {
|
||||
inference_mode_from_url_path(http_request.path())
|
||||
}
|
||||
|
||||
fn inference_mode_from_url_path(url_path: &str) -> Option<TypeOfInference> {
|
||||
let last_segment = url_path.split("/").last();
|
||||
match last_segment {
|
||||
Some("lora") => Some(TypeOfInference::Lora),
|
||||
Some("model") => Some(TypeOfInference::Model),
|
||||
Some("inference") => Some(TypeOfInference::Inference),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::http_server::deprecated_endpoints::image_gen::enqueue_image_generation::{inference_mode_from_url_path, TypeOfInference};
|
||||
|
||||
#[test]
|
||||
fn test_url_paths() {
|
||||
// Valid routes
|
||||
assert_eq!(inference_mode_from_url_path("/v1/image_gen/enqueue/inference"), Some(TypeOfInference::Inference));
|
||||
assert_eq!(inference_mode_from_url_path("/v1/image_gen/upload/model"), Some(TypeOfInference::Model));
|
||||
assert_eq!(inference_mode_from_url_path("/v1/image_gen/upload/lora"), Some(TypeOfInference::Lora));
|
||||
|
||||
// Non-routes
|
||||
assert_eq!(inference_mode_from_url_path(""), None);
|
||||
assert_eq!(inference_mode_from_url_path("/v1/image_gen/enqueue/foo"), None);
|
||||
assert_eq!(inference_mode_from_url_path("/v1/image_gen/upload/foo"), None);
|
||||
}
|
||||
}
|
||||
|
||||
// with LoRA
|
||||
// http://127.0.0.1:12345/v1/image_gen/inference/enqueue_image_gen
|
||||
// {
|
||||
// "uuid_idempotency_token": "12",
|
||||
// "maybe_sd_model_token": "weight_dmmthavhawqc2hj7yqyemcbf8",
|
||||
// "maybe_lora_model_token": "weight_t7gz78fjg27m0wtw6r33gafxs",
|
||||
// "maybe_prompt": "raiden mei a very well drawn, anime girl, with pink and purple hair sitting down on a chair relaxed, highest quality, semi nude, masterpiece, painted",
|
||||
// "maybe_a_prompt": "a anime girl, with pink and purple hair sitting down on a chair relaxed.",
|
||||
// "maybe_n_prompt": "nsfw, black and white, low quality, pixelated",
|
||||
// "maybe_seed": -1,
|
||||
// "maybe_width": 1024,
|
||||
// "maybe_height": 1024,
|
||||
// "maybe_sampler": "DPM++ 2M SDE Heun",
|
||||
// "maybe_cfg_scale": 7,
|
||||
// "maybe_number_of_samples": 64,
|
||||
// "maybe_batch_count": 4,
|
||||
// }
|
||||
|
||||
// without LoRA
|
||||
// {
|
||||
// "uuid_idempotency_token": "132141",
|
||||
// "maybe_sd_model_token": "weight_dmmthavhawqc2hj7yqyemcbf8",
|
||||
// "maybe_prompt": "raiden mei a very well drawn, anime girl, with pink and purple hair sitting down on a chair relaxed, highest quality, semi nude, masterpiece, painted",
|
||||
// "maybe_a_prompt": "a anime girl, with pink and purple hair sitting down on a chair relaxed.",
|
||||
// "maybe_n_prompt": "nsfw, black and white, low quality, pixelated",
|
||||
// "maybe_seed": -1,
|
||||
// "maybe_width": 512,
|
||||
// "maybe_height": 512,
|
||||
// "maybe_sampler": "DPM++ 2M SDE Heun",
|
||||
// "maybe_cfg_scale": 8,
|
||||
// "maybe_number_of_samples": 64,
|
||||
// "maybe_batch_count": 4
|
||||
// }
|
||||
|
||||
// http://127.0.0.1:12345/v1/image_gen/upload/lora
|
||||
// {
|
||||
// "uuid_idempotency_token": "12",
|
||||
// "maybe_lora_upload_path": "https://drive.google.com/file/d/1WRgR2pn0Ky8ls5_9Zq6tQlHTBvyWeach/view?usp=sharing",
|
||||
// "maybe_name":"some_name",
|
||||
// "maybe_description":"some_description"
|
||||
// }
|
||||
|
||||
// http://127.0.0.1:12345/v1/image_gen/upload/model
|
||||
// {
|
||||
// "uuid_idempotency_token": "13",
|
||||
// "maybe_upload_path": "https://drive.google.com/file/d/1WRgR2pn0Ky8ls5_9Zq6tQlHTBvyWeach/view?usp=sharing",
|
||||
// "maybe_name":"some_name",
|
||||
// "maybe_description":"some_description"
|
||||
// }
|
||||
|
||||
// {
|
||||
// "uuid_idempotency_token": "123",
|
||||
// "maybe_lora_upload_path": "https://drive.google.com/file/d/1WRgR2pn0Ky8ls5_9Zq6tQlHTBvyWeach/view?usp=sharing",
|
||||
// "maybe_name":"some_name",
|
||||
// "maybe_description":"some_description"
|
||||
// }
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"uuid_idempotency_token": "123",
|
||||
"type_of_inference": "checkpoint",
|
||||
"maybe_sd_model_token": "weight_dmmthavhawqc2hj7yqyemcbf8",
|
||||
"maybe_lora_model_token": "weight_t7gz78fjg27m0wtw6r33gafxs",
|
||||
"maybe_prompt": "a very well drawn, anime girl, with pink and purple hair sitting down on a chair relaxed.",
|
||||
"maybe_a_prompt": "a anime girl, with pink and purple hair sitting down on a chair relaxed.",
|
||||
"maybe_n_prompt": "nsfw, black and white, low quality, pixelated",
|
||||
"maybe_seed": "-1",
|
||||
"maybe_width": "512",
|
||||
"maybe_height": "512",
|
||||
"maybe_sampler": "DPM++ 2M SDE Heun",
|
||||
"maybe_upload_path": "https://drive.google.com/file/d/1WRgR2pn0Ky8ls5_9Zq6tQlHTBvyWeach/view?usp=sharing",
|
||||
"maybe_lora_upload_path": "https://drive.google.com/file/d/1WRgR2pn0Ky8ls5_9Zq6tQlHTBvyWeach/view?usp=sharing",
|
||||
"maybe_cfg_scale": "7",
|
||||
"maybe_number_of_samples": "32",
|
||||
"maybe_batch_count": "4"
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
pub mod enqueue_image_generation;
|
||||
pub mod enqueue_generation_type;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
|
||||
pub const DEFAULT_INVESTOR_REDIRECT : &str = "https://fakeyou.com";
|
||||
|
||||
pub fn redirect_is_allowed(redirect_url: &str) -> bool {
|
||||
match redirect_url {
|
||||
"http://jungle.horse" => true,
|
||||
"http://jungle.horse/" => true,
|
||||
"https://create.storyteller.io" => true,
|
||||
"https://create.storyteller.io/" => true,
|
||||
"https://fakeyou.com" => true,
|
||||
"https://fakeyou.com/clone" => true,
|
||||
"https://jungle.horse" => true,
|
||||
"https://jungle.horse/" => true,
|
||||
"https://storyteller.io" => true,
|
||||
"https://storyteller.io/" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
//! A temporary cookie for investor demo
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
|
||||
pub const STORYTELLER_DEMO_COOKIE_NAME : &str = "storyteller_demo";
|
||||
|
||||
pub fn request_has_demo_cookie(http_request: &HttpRequest) -> bool {
|
||||
// NB: Payload of the cookie does not matter.
|
||||
http_request.cookie(STORYTELLER_DEMO_COOKIE_NAME).is_some()
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_http::header;
|
||||
use actix_web::cookie::time::OffsetDateTime;
|
||||
use actix_web::cookie::Cookie;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use actix_helpers::extractors::get_request_host::get_request_host;
|
||||
|
||||
use crate::http_server::deprecated_endpoints::investor_demo::default_redirect::{redirect_is_allowed, DEFAULT_INVESTOR_REDIRECT};
|
||||
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::STORYTELLER_DEMO_COOKIE_NAME;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct QueryFields {
|
||||
redirect_to: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn disable_demo_mode_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<QueryFields>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> impl Responder
|
||||
{
|
||||
let unsafe_redirect = query.redirect_to
|
||||
.as_deref()
|
||||
.map(|r| r.to_string())
|
||||
.unwrap_or(DEFAULT_INVESTOR_REDIRECT.to_string());
|
||||
|
||||
let redirect_allowed = redirect_is_allowed(&unsafe_redirect);
|
||||
|
||||
let safe_redirect_url = if redirect_allowed {
|
||||
unsafe_redirect
|
||||
} else {
|
||||
DEFAULT_INVESTOR_REDIRECT.to_string()
|
||||
};
|
||||
|
||||
let maybe_host = get_request_host(&http_request);
|
||||
let maybe_cookie_domain = match maybe_host {
|
||||
Some("jungle.horse") | Some("api.jungle.horse") => Some(".jungle.horse"),
|
||||
Some("fakeyou.com") | Some("api.fakeyou.com") => Some(".fakeyou.com"),
|
||||
Some("storyteller.io") | Some("api.storyteller.io") => Some(".storyteller.io"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Kill the cookie
|
||||
let mut cookie_builder = Cookie::build(STORYTELLER_DEMO_COOKIE_NAME, "")
|
||||
.secure(server_state.env_config.cookie_secure) // HTTPS-only
|
||||
.expires(OffsetDateTime::UNIX_EPOCH)
|
||||
.http_only(false) // This is meant to be exposed to Javascript!
|
||||
// NB: Since we're setting this from direct HTTP access rather than XHR, the browser
|
||||
// implicitly sets the path used to access the cookie, ie. '/demo_mode', which is not what
|
||||
// we want!
|
||||
.path("/")
|
||||
.permanent();
|
||||
|
||||
if let Some(cookie_domain) = maybe_cookie_domain {
|
||||
cookie_builder = cookie_builder.domain(cookie_domain);
|
||||
}
|
||||
|
||||
let mut cookie = cookie_builder.finish();
|
||||
cookie.make_removal(); // Remove the cookie
|
||||
|
||||
HttpResponse::build(StatusCode::FOUND)
|
||||
.append_header((header::LOCATION, safe_redirect_url.to_string()))
|
||||
.cookie(cookie)
|
||||
.finish()
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_http::header;
|
||||
use actix_web::cookie::Cookie;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use actix_helpers::extractors::get_request_host::get_request_host;
|
||||
|
||||
use crate::http_server::deprecated_endpoints::investor_demo::default_redirect::{redirect_is_allowed, DEFAULT_INVESTOR_REDIRECT};
|
||||
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::STORYTELLER_DEMO_COOKIE_NAME;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct QueryFields {
|
||||
redirect_to: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn enable_demo_mode_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<QueryFields>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> impl Responder
|
||||
{
|
||||
let unsafe_redirect = query.redirect_to
|
||||
.as_deref()
|
||||
.map(|r| r.to_string())
|
||||
.unwrap_or(DEFAULT_INVESTOR_REDIRECT.to_string());
|
||||
|
||||
let redirect_allowed = redirect_is_allowed(&unsafe_redirect);
|
||||
|
||||
let safe_redirect_url = if redirect_allowed {
|
||||
unsafe_redirect
|
||||
} else {
|
||||
DEFAULT_INVESTOR_REDIRECT.to_string()
|
||||
};
|
||||
|
||||
let maybe_host = get_request_host(&http_request);
|
||||
let maybe_cookie_domain = match maybe_host {
|
||||
Some("jungle.horse") | Some("api.jungle.horse") => Some(".jungle.horse"),
|
||||
Some("fakeyou.com") | Some("api.fakeyou.com") => Some(".fakeyou.com"),
|
||||
Some("storyteller.io") | Some("api.storyteller.io") => Some(".storyteller.io"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Add the cookie.
|
||||
let mut cookie_builder = Cookie::build(STORYTELLER_DEMO_COOKIE_NAME, "true")
|
||||
.secure(server_state.env_config.cookie_secure) // HTTPS-only
|
||||
.http_only(false) // This is meant to be exposed to Javascript!
|
||||
// NB: Since we're setting this from direct HTTP access rather than XHR, the browser
|
||||
// implicitly sets the path used to access the cookie, ie. '/demo_mode', which is not what
|
||||
// we want!
|
||||
.path("/")
|
||||
.permanent();
|
||||
|
||||
if let Some(cookie_domain) = maybe_cookie_domain {
|
||||
cookie_builder = cookie_builder.domain(cookie_domain);
|
||||
}
|
||||
|
||||
let cookie = cookie_builder.finish();
|
||||
|
||||
HttpResponse::build(StatusCode::FOUND)
|
||||
.append_header((header::LOCATION, safe_redirect_url.to_string()))
|
||||
.cookie(cookie)
|
||||
.finish()
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
pub mod demo_cookie;
|
||||
pub mod disable_demo_mode_handler;
|
||||
pub mod enable_demo_mode_handler;
|
||||
pub mod default_redirect;
|
||||
-199
@@ -1,199 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::{debug, error, warn};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use crate::http_server::common_responses::user_avatars::default_avatar_color_from_username::default_avatar_color_from_username;
|
||||
use crate::http_server::common_responses::user_avatars::default_avatar_from_username::default_avatar_from_username;
|
||||
use crate::http_server::common_responses::user_details_lite::{UserDefaultAvatarInfo, UserDetailsLight};
|
||||
use errors::AnyhowResult;
|
||||
use mysql_queries::queries::tts::stats::calculate_tts_model_leaderboard::calculate_tts_model_leaderboard;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LeaderboardResponse {
|
||||
success: bool,
|
||||
tts_leaderboard: Vec<LeaderboardRow>,
|
||||
w2l_leaderboard: Vec<LeaderboardRow>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct LeaderboardRow {
|
||||
pub user: UserDetailsLight,
|
||||
|
||||
pub uploaded_count: i64,
|
||||
|
||||
#[deprecated(note="switch to the user field (type UserDetailsLight)")]
|
||||
pub creator_user_token: String,
|
||||
|
||||
#[deprecated(note="switch to the user field (type UserDetailsLight)")]
|
||||
pub username: String,
|
||||
|
||||
#[deprecated(note="switch to the user field (type UserDetailsLight)")]
|
||||
pub display_name: String,
|
||||
|
||||
#[deprecated(note="switch to the user field (type UserDetailsLight)")]
|
||||
pub gravatar_hash: String,
|
||||
|
||||
#[deprecated(note="switch to the user field (type UserDetailsLight)")]
|
||||
pub default_avatar_index: u8,
|
||||
|
||||
#[deprecated(note="switch to the user field (type UserDetailsLight)")]
|
||||
pub default_avatar_color_index: u8,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct LeaderboardErrorResponse {
|
||||
pub success: bool,
|
||||
pub error_type: LeaderboardErrorType,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
pub enum LeaderboardErrorType {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl LeaderboardErrorResponse {
|
||||
fn server_error() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: LeaderboardErrorType::ServerError,
|
||||
error_message: "server error".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for LeaderboardErrorResponse {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.error_type {
|
||||
LeaderboardErrorType::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for LeaderboardErrorResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.error_type)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn leaderboard_handler(
|
||||
_http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, LeaderboardErrorResponse> {
|
||||
|
||||
let maybe_cached = server_state.caches.ephemeral.leaderboard.grab_copy_without_bump_if_unexpired()
|
||||
.map_err(|e| {
|
||||
error!("error consulting cache: {:?}", e);
|
||||
LeaderboardErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
|
||||
let leaderboard_info = match maybe_cached {
|
||||
Some(leaderboard) => {
|
||||
leaderboard
|
||||
}
|
||||
None => {
|
||||
debug!("populating leaderboard from database");
|
||||
|
||||
let leaderboard_query_result = query_leaderboard(
|
||||
&server_state.mysql_pool
|
||||
).await;
|
||||
|
||||
match leaderboard_query_result {
|
||||
// If the database misbehaves (eg. DDoS), let's stop spamming it.
|
||||
// We'll attempt to read the old value from the cache and keep going.
|
||||
Err(err) => {
|
||||
warn!("error querying database / inserting into cache: {:?}", err);
|
||||
|
||||
let maybe_cached = server_state.caches.ephemeral.leaderboard.grab_even_expired_and_bump()
|
||||
.map_err(|err| {
|
||||
error!("error consulting cache (even expired): {:?}", err);
|
||||
LeaderboardErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
maybe_cached.ok_or_else(|| {
|
||||
error!("error querying database and subsequently reading cache: {:?}", err);
|
||||
LeaderboardErrorResponse::server_error()
|
||||
})?
|
||||
}
|
||||
|
||||
// Happy path...
|
||||
Ok(leaderboard_info) => {
|
||||
server_state.caches.ephemeral.leaderboard.store_copy(&leaderboard_info)
|
||||
.map_err(|e| {
|
||||
error!("error storing cache: {:?}", e);
|
||||
LeaderboardErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
leaderboard_info
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let response = LeaderboardResponse {
|
||||
success: true,
|
||||
tts_leaderboard: leaderboard_info.tts_leaderboard,
|
||||
w2l_leaderboard: vec![],
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| LeaderboardErrorResponse::server_error())?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LeaderboardInfo {
|
||||
tts_leaderboard: Vec<LeaderboardRow>,
|
||||
}
|
||||
|
||||
async fn query_leaderboard(mysql_pool: &MySqlPool) -> AnyhowResult<LeaderboardInfo> {
|
||||
let mut mysql_connection = mysql_pool.acquire().await?;
|
||||
|
||||
let tts_results = calculate_tts_model_leaderboard(&mut mysql_connection)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|record| LeaderboardRow {
|
||||
creator_user_token: record.creator_user_token.clone(),
|
||||
username: record.username.to_string(),
|
||||
display_name: record.display_name.to_string(),
|
||||
gravatar_hash: record.gravatar_hash.to_string(),
|
||||
default_avatar_index: default_avatar_from_username(&record.username),
|
||||
default_avatar_color_index: default_avatar_color_from_username(&record.username),
|
||||
user: UserDetailsLight {
|
||||
user_token: UserToken::new_from_str(&record.creator_user_token),
|
||||
username: record.username.to_string(),
|
||||
display_name: record.display_name,
|
||||
gravatar_hash: record.gravatar_hash,
|
||||
default_avatar: UserDefaultAvatarInfo::from_username(&record.username),
|
||||
},
|
||||
uploaded_count: record.uploaded_count,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(LeaderboardInfo {
|
||||
tts_leaderboard: tts_results,
|
||||
})
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
pub mod get_leaderboard;
|
||||
-255
@@ -1,255 +0,0 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::{error, info, warn};
|
||||
|
||||
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_extended::UserSessionExtended;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
|
||||
use mysql_queries::payloads::generic_inference_args::inner_payloads::mocap_payload::{MocapArgs, MocapVideoSource};
|
||||
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Debug requests can get routed to special "debug-only" workers, which can
|
||||
/// be used to trial new code, run debugging, etc.
|
||||
const DEBUG_HEADER_NAME : &str = "enable-debug-mode";
|
||||
|
||||
/// The routing tag header can send workloads to particular k8s hosts.
|
||||
/// This is useful for catching the live logs or intercepting the job.
|
||||
const ROUTING_TAG_HEADER_NAME : &str = "routing-tag";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnqueueMocapnetRequest {
|
||||
uuid_idempotency_token: String,
|
||||
|
||||
video_source: Option<MediaFileToken>,
|
||||
creator_set_visibility: Option<Visibility>,
|
||||
}
|
||||
|
||||
/// Treated as an enum. Only one of these may be set.
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EnqueueMocapnetSuccessResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EnqueueMocapnetError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
impl ResponseError for EnqueueMocapnetError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
EnqueueMocapnetError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
EnqueueMocapnetError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
EnqueueMocapnetError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
EnqueueMocapnetError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
EnqueueMocapnetError::BadInput(reason) => reason.to_string(),
|
||||
EnqueueMocapnetError::NotAuthorized => "unauthorized".to_string(),
|
||||
EnqueueMocapnetError::ServerError => "server error".to_string(),
|
||||
EnqueueMocapnetError::RateLimited => "rate limited".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl std::fmt::Display for EnqueueMocapnetError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn enqueue_mocapnet_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<EnqueueMocapnetRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EnqueueMocapnetError>
|
||||
{
|
||||
let mut maybe_user_token : Option<UserToken> = None;
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
EnqueueMocapnetError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
|
||||
|
||||
// ==================== USER SESSION ==================== //
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
EnqueueMocapnetError::ServerError
|
||||
})?;
|
||||
|
||||
if let Some(user_session) = maybe_user_session.as_ref() {
|
||||
maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token));
|
||||
}
|
||||
|
||||
// TODO: Plan should handle "first anonymous use" and "investor" cases.
|
||||
let plan = get_correct_plan_for_session(
|
||||
server_state.server_environment,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
// ==================== DEBUG MODE + ROUTING TAG ==================== //
|
||||
|
||||
let is_debug_request =
|
||||
get_request_header_optional(&http_request, DEBUG_HEADER_NAME)
|
||||
.is_some();
|
||||
|
||||
let maybe_routing_tag=
|
||||
get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME)
|
||||
.map(|routing_tag| routing_tag.trim().to_string());
|
||||
|
||||
// ==================== RATE LIMIT ==================== //
|
||||
|
||||
let rate_limiter = match maybe_user_session {
|
||||
None => &server_state.redis_rate_limiters.logged_out,
|
||||
Some(ref user) => {
|
||||
if user.role.is_banned {
|
||||
return Err(EnqueueMocapnetError::NotAuthorized);
|
||||
}
|
||||
&server_state.redis_rate_limiters.logged_in
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
|
||||
return Err(EnqueueMocapnetError::RateLimited);
|
||||
}
|
||||
|
||||
// ==================== HANDLE IDEMPOTENCY ==================== //
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(EnqueueMocapnetError::BadInput(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
EnqueueMocapnetError::BadInput("invalid idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
// ==================== LOOK UP MODEL INFO ==================== //
|
||||
|
||||
// TODO(bt): CHECK DATABASE FOR TOKENS!
|
||||
|
||||
let video_source: MocapVideoSource = request.video_source
|
||||
.as_ref()
|
||||
.map(|token| MocapVideoSource::media_file_token(token.as_str()))
|
||||
.ok_or_else(|| EnqueueMocapnetError::BadInput("video source not fully specified".to_string()))?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_user_preferred_visibility : Option<Visibility> = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|user_session: &UserSessionExtended| user_session.preferences.preferred_tts_result_visibility); // TODO: New setting for web-vc
|
||||
|
||||
let set_visibility = request.creator_set_visibility
|
||||
.or(maybe_user_preferred_visibility)
|
||||
.unwrap_or(Visibility::Public);
|
||||
|
||||
let inference_args = MocapArgs {
|
||||
maybe_video_source: Some(video_source),
|
||||
maybe_ik1: Some(0.001f32),
|
||||
maybe_ik2: Some(99i32),
|
||||
maybe_ik3: Some(99i32),
|
||||
maybe_smoothing1: Some(60f32),
|
||||
maybe_smoothing2: Some(10f32),
|
||||
maybe_size1: Some(1920i32),
|
||||
maybe_size2: Some(1080i32),
|
||||
};
|
||||
|
||||
info!("Creating mocapnet job record...");
|
||||
|
||||
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
job_type: InferenceJobType::MocapNet,
|
||||
maybe_product_category: None, // This is not a product anymore
|
||||
inference_category: InferenceCategory::Mocap,
|
||||
maybe_model_type: Some(InferenceModelType::MocapNet), // NB: Model is static during inference
|
||||
maybe_model_token: None, // NB: Model is static during inference
|
||||
maybe_input_source_token: None, // TODO: Introduce a second foreign key ?
|
||||
maybe_input_source_token_type: None, // TODO: Introduce a second foreign key ?
|
||||
maybe_download_url: None,
|
||||
maybe_cover_image_media_file_token: None,
|
||||
maybe_raw_inference_text: None, // No text
|
||||
maybe_max_duration_seconds: None,
|
||||
maybe_inference_args: Some(GenericInferenceArgs {
|
||||
inference_category: Some(InferenceCategoryAbbreviated::Mocap),
|
||||
args: Some(PolymorphicInferenceArgs::Mc(inference_args)),
|
||||
}),
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: set_visibility,
|
||||
priority_level,
|
||||
requires_keepalive: plan.mocapnet_requires_frontend_keepalive(),
|
||||
is_debug_request,
|
||||
maybe_routing_tag: maybe_routing_tag.as_deref(),
|
||||
mysql_pool: &server_state.mysql_pool, // TODO(bt,2024-02-04): Reuse connection
|
||||
}).await;
|
||||
|
||||
let job_token = match query_result {
|
||||
Ok((job_token, _id)) => job_token,
|
||||
Err(err) => {
|
||||
warn!("New generic inference job creation DB error: {:?}", err);
|
||||
if err.had_duplicate_idempotency_token() {
|
||||
return Err(EnqueueMocapnetError::BadInput("Duplicate idempotency token".to_string()));
|
||||
}
|
||||
return Err(EnqueueMocapnetError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let response: EnqueueMocapnetSuccessResponse = EnqueueMocapnetSuccessResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| EnqueueMocapnetError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod enqueue_mocapnet;
|
||||
@@ -5,11 +5,7 @@ pub mod conversion;
|
||||
pub mod engine;
|
||||
pub mod events;
|
||||
pub mod flags;
|
||||
pub mod image_gen;
|
||||
pub mod investor_demo;
|
||||
pub mod leaderboard;
|
||||
pub mod media_uploads;
|
||||
pub mod mocap;
|
||||
pub mod moderation;
|
||||
pub mod stubs;
|
||||
pub mod vocoders;
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
use log::warn;
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use artcraft_router::api::router_provider::RouterProvider;
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Request-scoped identity for debug log inserts, so deep pipeline code can
|
||||
/// log without threading four separate arguments.
|
||||
pub struct GenerationDebugLogContext<'a> {
|
||||
pub event_token: &'a DebugLogEventToken,
|
||||
pub user_token: &'a UserToken,
|
||||
pub ip_address: &'a str,
|
||||
pub request_url: &'a str,
|
||||
}
|
||||
|
||||
/// Best-effort info-level debug log of an inbound generation HTTP request.
|
||||
/// Never fails the request — insert errors are logged and swallowed.
|
||||
pub async fn insert_generation_request_debug_log<'e, 'c: 'e, E>(
|
||||
debug_log_event_token: &'e DebugLogEventToken,
|
||||
user_token: &'e UserToken,
|
||||
ip_address: &'e str,
|
||||
request_url: &'e str,
|
||||
request_json: &'e str,
|
||||
mysql_executor: E,
|
||||
) where
|
||||
E: 'c + Executor<'c, Database = MySql>,
|
||||
{
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(debug_log_event_token),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
debug_log_type: DebugLogType::HttpRequest,
|
||||
maybe_log_level: Some(DebugLogLevel::Info),
|
||||
maybe_ip_address: Some(ip_address),
|
||||
maybe_url: Some(request_url),
|
||||
message: request_json,
|
||||
mysql_executor,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert HTTP request debug log: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort error-level debug log of a generation backend failure.
|
||||
/// Never fails the request — insert errors are logged and swallowed.
|
||||
pub async fn insert_generation_failure_debug_log<'e, 'c: 'e, E>(
|
||||
debug_log_event_token: &'e DebugLogEventToken,
|
||||
user_token: &'e UserToken,
|
||||
ip_address: &'e str,
|
||||
request_url: &'e str,
|
||||
error_message: &'e str,
|
||||
mysql_executor: E,
|
||||
) where
|
||||
E: 'c + Executor<'c, Database = MySql>,
|
||||
{
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(debug_log_event_token),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
debug_log_type: DebugLogType::BackendFailure,
|
||||
maybe_log_level: Some(DebugLogLevel::Error),
|
||||
maybe_ip_address: Some(ip_address),
|
||||
maybe_url: Some(request_url),
|
||||
message: error_message,
|
||||
mysql_executor,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert generation failure debug log: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
/// The debug log type for an outbound request to the given provider, or
|
||||
/// `None` for providers we don't capture.
|
||||
pub fn provider_request_debug_log_type(provider: RouterProvider) -> Option<DebugLogType> {
|
||||
match provider {
|
||||
RouterProvider::Fal => Some(DebugLogType::FalRequest),
|
||||
RouterProvider::GrokApi => Some(DebugLogType::GrokApiRequest),
|
||||
RouterProvider::Seedance2Pro => Some(DebugLogType::KinoviRequest),
|
||||
// First-party: the HttpRequest log already captures the user request.
|
||||
RouterProvider::Artcraft => None,
|
||||
// No DebugLogType variant yet; add one if GmiCloud routing is revived.
|
||||
RouterProvider::GmiCloud => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort info-level debug log of an outbound provider generation
|
||||
/// request. MUST be inserted during the pre-request DB phase — on the same
|
||||
/// open connection as the session/billing/HTTP-request writes, BEFORE that
|
||||
/// connection is released and the external provider call is made — so the
|
||||
/// outbound payload is captured even when the upload/enqueue fails, without
|
||||
/// ever touching the pool.
|
||||
/// Never fails the request — insert errors are logged and swallowed.
|
||||
pub async fn insert_provider_request_debug_log<'e, 'c: 'e, E>(
|
||||
context: &'e GenerationDebugLogContext<'e>,
|
||||
debug_log_type: DebugLogType,
|
||||
outbound_request_debug: &'e str,
|
||||
mysql_executor: E,
|
||||
) where
|
||||
E: 'c + Executor<'c, Database = MySql>,
|
||||
{
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(context.event_token),
|
||||
maybe_creator_user_token: Some(context.user_token),
|
||||
debug_log_type,
|
||||
maybe_log_level: Some(DebugLogLevel::Info),
|
||||
maybe_ip_address: Some(context.ip_address),
|
||||
maybe_url: Some(context.request_url),
|
||||
message: outbound_request_debug,
|
||||
mysql_executor,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert {} debug log: {:?}", debug_log_type, err);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod test_synthetic_failure_reason;
|
||||
pub mod generation_debug_logs;
|
||||
pub mod job_failure_test;
|
||||
pub mod map_seedance2pro_web_errors;
|
||||
pub mod payments_error_test;
|
||||
mod test_synthetic_failure_reason;
|
||||
+35
-9
@@ -3,6 +3,9 @@ use std::sync::Arc;
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
|
||||
insert_generation_failure_debug_log, insert_generation_request_debug_log,
|
||||
};
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
@@ -38,6 +41,7 @@ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, g
|
||||
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
|
||||
use sqlx::Acquire;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Seedance 1.0 Lite Image to Video
|
||||
@@ -84,6 +88,21 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
|
||||
|
||||
let debug_log_event_token = DebugLogEventToken::generate();
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
let request_url = http_request.uri().to_string();
|
||||
|
||||
insert_generation_request_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&serde_json::to_string(&*request).unwrap_or_default(),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
|
||||
let start_frame_media_file_token = match &request.media_file_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
@@ -208,12 +227,21 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
|
||||
api_key: &server_state.inference_providers.fal.api_key,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_seedance_1_lite_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_seedance_1_lite_image_to_video_webhook: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
let fal_result = match enqueue_seedance_1_lite_image_to_video_webhook(args).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!("Error calling enqueue_seedance_1_lite_image_to_video_webhook: {:?}", err);
|
||||
insert_generation_failure_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&format!("Seedance 1.0 Lite generation failed: {:?}", err),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
return Err(CommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
@@ -223,8 +251,6 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
@@ -294,7 +320,7 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+35
-9
@@ -3,6 +3,9 @@ use std::sync::Arc;
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
|
||||
insert_generation_failure_debug_log, insert_generation_request_debug_log,
|
||||
};
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
@@ -35,6 +38,7 @@ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, g
|
||||
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
|
||||
use sqlx::Acquire;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Seedance 1.0 Pro Image to Video
|
||||
@@ -81,6 +85,21 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
|
||||
|
||||
let debug_log_event_token = DebugLogEventToken::generate();
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
let request_url = http_request.uri().to_string();
|
||||
|
||||
insert_generation_request_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&serde_json::to_string(&*request).unwrap_or_default(),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
|
||||
let start_frame_media_file_token = match &request.media_file_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
@@ -182,12 +201,21 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
|
||||
api_key: &server_state.inference_providers.fal.api_key,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_seedance_1_pro_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_seedance_1_pro_image_to_video_webhook: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
let fal_result = match enqueue_seedance_1_pro_image_to_video_webhook(args).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!("Error calling enqueue_seedance_1_pro_image_to_video_webhook: {:?}", err);
|
||||
insert_generation_failure_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&format!("Seedance 1.0 Pro generation failed: {:?}", err),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
return Err(CommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
@@ -197,8 +225,6 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
@@ -268,7 +294,7 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+50
-15
@@ -3,6 +3,9 @@ use std::sync::Arc;
|
||||
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
|
||||
insert_generation_failure_debug_log, insert_generation_request_debug_log,
|
||||
};
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
|
||||
@@ -30,6 +33,7 @@ use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_it
|
||||
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
|
||||
use sqlx::Acquire;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
|
||||
|
||||
/// Seedance 1.5 Pro Multi-Function (text and image to video)
|
||||
@@ -78,6 +82,21 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
|
||||
|
||||
let debug_log_event_token = DebugLogEventToken::generate();
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
let request_url = http_request.uri().to_string();
|
||||
|
||||
insert_generation_request_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&serde_json::to_string(&*request).unwrap_or_default(),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
@@ -177,12 +196,21 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
|
||||
api_key: &server_state.inference_providers.fal.api_key,
|
||||
};
|
||||
|
||||
fal_result = enqueue_seedance_1p5_pro_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_seedance_1p5_pro_image_to_video_webhook: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
fal_result = match enqueue_seedance_1p5_pro_image_to_video_webhook(args).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!("Error calling enqueue_seedance_1p5_pro_image_to_video_webhook: {:?}", err);
|
||||
insert_generation_failure_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&format!("Seedance 1.5 Pro generation failed: {:?}", err),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
return Err(CommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
info!("text-to-video case");
|
||||
@@ -217,12 +245,21 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
|
||||
&mut mysql_connection,
|
||||
).await?;
|
||||
|
||||
fal_result = enqueue_seedance_1p5_pro_text_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_seedance_1p5_pro_text_to_video_webhook: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
fal_result = match enqueue_seedance_1p5_pro_text_to_video_webhook(args).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!("Error calling enqueue_seedance_1p5_pro_text_to_video_webhook: {:?}", err);
|
||||
insert_generation_failure_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&format!("Seedance 1.5 Pro generation failed: {:?}", err),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
return Err(CommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
@@ -233,8 +270,6 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
@@ -342,7 +377,7 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+99
-18
@@ -5,6 +5,12 @@ use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
|
||||
use crate::http_server::endpoints::generate::common::map_seedance2pro_web_errors::map_seedance2pro_error_to_web_error;
|
||||
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
|
||||
insert_generation_failure_debug_log, insert_generation_request_debug_log,
|
||||
};
|
||||
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
@@ -49,6 +55,9 @@ use sqlx::MySql;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use url::Url;
|
||||
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url, ExtractExtensions};
|
||||
|
||||
@@ -60,6 +69,14 @@ struct SeedanceGenerationResult {
|
||||
generation_mode: CommonGenerationMode,
|
||||
}
|
||||
|
||||
/// Request-scoped context for writing `debug_logs` rows.
|
||||
struct DebugLogContext<'a> {
|
||||
event_token: &'a DebugLogEventToken,
|
||||
user_token: &'a UserToken,
|
||||
ip_address: &'a str,
|
||||
request_url: &'a str,
|
||||
}
|
||||
|
||||
// ======================== Handler ========================
|
||||
|
||||
/// Seedance 2.0 Multi-Function video generation (text-to-video, keyframe, and reference).
|
||||
@@ -108,6 +125,28 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
|
||||
let user_token = &user_session.user_token;
|
||||
|
||||
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
|
||||
|
||||
let debug_log_event_token = DebugLogEventToken::generate();
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
let request_url = http_request.uri().to_string();
|
||||
|
||||
insert_generation_request_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&serde_json::to_string(&*request).unwrap_or_default(),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
|
||||
let debug_log_context = DebugLogContext {
|
||||
event_token: &debug_log_event_token,
|
||||
user_token,
|
||||
ip_address: &ip_address,
|
||||
request_url: &request_url,
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
@@ -203,6 +242,8 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
batch_count,
|
||||
duration_seconds,
|
||||
kinovi_character_ids.clone(),
|
||||
&debug_log_context,
|
||||
&mut mysql_connection,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
@@ -226,6 +267,8 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
batch_count,
|
||||
duration_seconds,
|
||||
kinovi_character_ids.clone(),
|
||||
&debug_log_context,
|
||||
&mut mysql_connection,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
@@ -248,6 +291,14 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
}
|
||||
}
|
||||
|
||||
insert_generation_failure_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&format!("Seedance 2.0 generation failed: {:?}", err),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -270,6 +321,8 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
batch_count,
|
||||
duration_seconds,
|
||||
kinovi_character_ids,
|
||||
&debug_log_context,
|
||||
&mut mysql_connection,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
@@ -289,6 +342,14 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
}
|
||||
}
|
||||
|
||||
insert_generation_failure_debug_log(
|
||||
&debug_log_event_token,
|
||||
user_token,
|
||||
&ip_address,
|
||||
&request_url,
|
||||
&format!("Seedance 2.0 generation failed: {:?}", err),
|
||||
&mut *mysql_connection,
|
||||
).await;
|
||||
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -305,8 +366,6 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
|
||||
// --- DB writes in a transaction ---
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
@@ -438,7 +497,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
creator_set_visibility: Visibility::Public,
|
||||
maybe_platform_type: get_request_platform_type(&http_request),
|
||||
maybe_cost_estimates: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
@@ -543,6 +602,8 @@ async fn upload_and_generate(
|
||||
batch_count: KinoviBatchCount,
|
||||
duration_seconds: u8,
|
||||
kinovi_character_ids: Option<Vec<String>>,
|
||||
debug_log_context: &DebugLogContext<'_>,
|
||||
mysql_connection: &mut sqlx::pool::PoolConnection<MySql>,
|
||||
) -> Result<SeedanceGenerationResult, CommonWebError> {
|
||||
|
||||
// --- Upload files to seedance2pro CDN ---
|
||||
@@ -605,24 +666,44 @@ async fn upload_and_generate(
|
||||
|
||||
let prompt = request.prompt.clone().unwrap_or_else(|| "".to_string());
|
||||
|
||||
let kinovi_request = KinoviGenerateVideoRequest {
|
||||
model_type: KinoviModelType::Seedance2Pro,
|
||||
prompt,
|
||||
aspect_ratio,
|
||||
output_resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: kinovi_character_ids,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
|
||||
// ==================== DEBUG LOG: KINOVI REQUEST ==================== //
|
||||
// Logged BEFORE the send so the outbound payload is captured even when
|
||||
// the generation call fails.
|
||||
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(debug_log_context.event_token),
|
||||
maybe_creator_user_token: Some(debug_log_context.user_token),
|
||||
debug_log_type: DebugLogType::KinoviRequest,
|
||||
maybe_log_level: Some(DebugLogLevel::Info),
|
||||
maybe_ip_address: Some(debug_log_context.ip_address),
|
||||
maybe_url: Some(debug_log_context.request_url),
|
||||
message: &format!("{:#?}", kinovi_request),
|
||||
mysql_executor: &mut **mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert Kinovi request debug log: {:?}", err);
|
||||
}
|
||||
|
||||
let video_gen_args = GenerateVideoArgs {
|
||||
session,
|
||||
host_override: None,
|
||||
request: KinoviGenerateVideoRequest {
|
||||
model_type: KinoviModelType::Seedance2Pro,
|
||||
prompt,
|
||||
aspect_ratio,
|
||||
output_resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: kinovi_character_ids,
|
||||
use_face_blur_hack: None,
|
||||
},
|
||||
request: kinovi_request,
|
||||
};
|
||||
|
||||
let gen_response = generate_video(video_gen_args).await
|
||||
|
||||
@@ -22,7 +22,6 @@ pub mod omni_gen;
|
||||
pub mod prompts;
|
||||
pub mod service;
|
||||
pub mod stats;
|
||||
pub mod studio_gen2;
|
||||
pub mod subscriptions;
|
||||
pub mod tags;
|
||||
pub mod trending;
|
||||
@@ -33,9 +32,6 @@ pub mod user_referral_codes;
|
||||
pub mod user_wallets;
|
||||
pub mod users;
|
||||
pub mod video_info;
|
||||
pub mod voice_clone_requests;
|
||||
pub mod voice_conversion;
|
||||
pub mod voice_designer;
|
||||
pub mod web_referrals;
|
||||
pub mod webhooks;
|
||||
pub mod weights;
|
||||
|
||||
+2
@@ -1 +1,3 @@
|
||||
pub mod moderation_list_all_debug_logs_handler;
|
||||
pub mod moderation_list_debug_logs_for_token_handler;
|
||||
pub mod moderation_list_debug_logs_for_user_handler;
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::ModerationDebugLogUser;
|
||||
use artcraft_api_defs::moderation::debug_logs::moderation_list_all_debug_logs::{
|
||||
ModerationListAllDebugLogsEntry, ModerationListAllDebugLogsQueryParams,
|
||||
ModerationListAllDebugLogsSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::debug_logs::list_all_debug_logs::{
|
||||
list_all_debug_logs, ListAllDebugLogsArgs,
|
||||
};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::user_lookup::user_session::require_moderator::require_moderator;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/debug_logs/list_all",
|
||||
params(
|
||||
ModerationListAllDebugLogsQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ModerationListAllDebugLogsSuccessResponse),
|
||||
(status = 400, description = "Bad request (invalid severity)"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_list_all_debug_logs_handler(
|
||||
http_request: HttpRequest,
|
||||
query: web::Query<ModerationListAllDebugLogsQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModerationListAllDebugLogsSuccessResponse>, CommonWebError> {
|
||||
let maybe_log_levels = query.parsed_severities().map_err(|err| {
|
||||
CommonWebError::BadInputWithSimpleMessage(format!("invalid severities parameter: {}", err))
|
||||
})?;
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
|
||||
|
||||
let result = list_all_debug_logs(ListAllDebugLogsArgs {
|
||||
maybe_log_levels,
|
||||
maybe_id_cursor: query.cursor,
|
||||
limit: query.limit,
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await.map_err(|err| {
|
||||
warn!("Error listing all debug logs: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let debug_logs: Vec<ModerationListAllDebugLogsEntry> = result.debug_logs.into_iter().map(|row| {
|
||||
// The join only yields user fields when the creator user exists.
|
||||
let maybe_user = match (row.maybe_creator_user_token.clone(), row.maybe_user_display_name, row.maybe_user_username, row.maybe_user_gravatar_hash) {
|
||||
(Some(user_token), Some(display_name), Some(username), Some(gravatar_hash)) => {
|
||||
Some(ModerationDebugLogUser {
|
||||
user_token,
|
||||
display_name,
|
||||
username,
|
||||
gravatar_hash,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ModerationListAllDebugLogsEntry {
|
||||
id: row.id,
|
||||
event_token: row.event_token,
|
||||
debug_log_type: row.debug_log_type,
|
||||
maybe_log_level: row.maybe_log_level,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
maybe_ip_address: row.maybe_ip_address,
|
||||
maybe_url: row.maybe_url,
|
||||
message: row.message,
|
||||
created_at: row.created_at,
|
||||
maybe_user,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ModerationListAllDebugLogsSuccessResponse {
|
||||
success: true,
|
||||
debug_logs,
|
||||
next_cursor: result.next_cursor,
|
||||
}))
|
||||
}
|
||||
+32
-50
@@ -2,80 +2,45 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::{ModerationDebugLogEntry, ModerationDebugLogUser};
|
||||
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_token::{
|
||||
ModerationListDebugLogsForTokenPathInfo, ModerationListDebugLogsForTokenQueryParams,
|
||||
ModerationListDebugLogsForTokenSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::debug_logs::list_debug_logs_for_token::{
|
||||
list_debug_logs_for_token, ListDebugLogsForTokenArgs,
|
||||
};
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::user_lookup::user_session::require_moderator::require_moderator;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
// ── Path params ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListDebugLogsPathInfo {
|
||||
pub token: DebugLogEventToken,
|
||||
}
|
||||
|
||||
// ── Query params ──
|
||||
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
pub struct ListDebugLogsQueryParams {
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListDebugLogsSuccessResponse {
|
||||
pub success: bool,
|
||||
pub debug_logs: Vec<DebugLogEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DebugLogEntry {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/debug_logs/list/{token}",
|
||||
params(
|
||||
("token" = String, Path, description = "Debug log event token"),
|
||||
ListDebugLogsQueryParams,
|
||||
ModerationListDebugLogsForTokenQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListDebugLogsSuccessResponse),
|
||||
(status = 200, description = "Success", body = ModerationListDebugLogsForTokenSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_list_debug_logs_for_token_handler(
|
||||
http_request: HttpRequest,
|
||||
path: web::Path<ListDebugLogsPathInfo>,
|
||||
query: web::Query<ListDebugLogsQueryParams>,
|
||||
path: web::Path<ModerationListDebugLogsForTokenPathInfo>,
|
||||
query: web::Query<ModerationListDebugLogsForTokenQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListDebugLogsSuccessResponse>, CommonWebError> {
|
||||
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
|
||||
|
||||
) -> Result<Json<ModerationListDebugLogsForTokenSuccessResponse>, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
|
||||
|
||||
let rows = list_debug_logs_for_token(ListDebugLogsForTokenArgs {
|
||||
event_token: &path.token,
|
||||
limit: query.limit,
|
||||
@@ -86,18 +51,35 @@ pub async fn moderation_list_debug_logs_for_token_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let debug_logs: Vec<DebugLogEntry> = rows.into_iter().map(|row| {
|
||||
DebugLogEntry {
|
||||
let debug_logs: Vec<ModerationDebugLogEntry> = rows.into_iter().map(|row| {
|
||||
// The join only yields user fields when the creator user exists.
|
||||
let maybe_user = match (row.maybe_creator_user_token.clone(), row.maybe_user_display_name, row.maybe_user_username, row.maybe_user_gravatar_hash) {
|
||||
(Some(user_token), Some(display_name), Some(username), Some(gravatar_hash)) => {
|
||||
Some(ModerationDebugLogUser {
|
||||
user_token,
|
||||
display_name,
|
||||
username,
|
||||
gravatar_hash,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ModerationDebugLogEntry {
|
||||
id: row.id,
|
||||
event_token: row.event_token,
|
||||
debug_log_type: row.debug_log_type,
|
||||
maybe_log_level: row.maybe_log_level,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
maybe_ip_address: row.maybe_ip_address,
|
||||
maybe_url: row.maybe_url,
|
||||
message: row.message,
|
||||
created_at: row.created_at,
|
||||
maybe_user,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListDebugLogsSuccessResponse {
|
||||
Ok(Json(ModerationListDebugLogsForTokenSuccessResponse {
|
||||
success: true,
|
||||
debug_logs,
|
||||
}))
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::{ModerationDebugLogEntry, ModerationDebugLogUser};
|
||||
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_user::{
|
||||
ModerationListDebugLogsForUserPathInfo, ModerationListDebugLogsForUserQueryParams,
|
||||
ModerationListDebugLogsForUserSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::debug_logs::list_debug_logs_for_user::{
|
||||
list_debug_logs_for_user, ListDebugLogsForUserArgs,
|
||||
};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::user_lookup::user_session::require_moderator::require_moderator;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/debug_logs/user_list/{user_token}",
|
||||
params(
|
||||
("user_token" = String, Path, description = "User token"),
|
||||
ModerationListDebugLogsForUserQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ModerationListDebugLogsForUserSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_list_debug_logs_for_user_handler(
|
||||
http_request: HttpRequest,
|
||||
path: web::Path<ModerationListDebugLogsForUserPathInfo>,
|
||||
query: web::Query<ModerationListDebugLogsForUserQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModerationListDebugLogsForUserSuccessResponse>, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
|
||||
|
||||
let result = list_debug_logs_for_user(ListDebugLogsForUserArgs {
|
||||
user_token: &path.user_token,
|
||||
maybe_id_cursor: query.cursor,
|
||||
limit: query.limit,
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await.map_err(|err| {
|
||||
warn!("Error listing debug logs for user {}: {:?}", path.user_token, err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let debug_logs: Vec<ModerationDebugLogEntry> = result.debug_logs.into_iter().map(|row| {
|
||||
// The join only yields user fields when the creator user exists.
|
||||
let maybe_user = match (row.maybe_creator_user_token.clone(), row.maybe_user_display_name, row.maybe_user_username, row.maybe_user_gravatar_hash) {
|
||||
(Some(user_token), Some(display_name), Some(username), Some(gravatar_hash)) => {
|
||||
Some(ModerationDebugLogUser {
|
||||
user_token,
|
||||
display_name,
|
||||
username,
|
||||
gravatar_hash,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ModerationDebugLogEntry {
|
||||
id: row.id,
|
||||
event_token: row.event_token,
|
||||
debug_log_type: row.debug_log_type,
|
||||
maybe_log_level: row.maybe_log_level,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
maybe_ip_address: row.maybe_ip_address,
|
||||
maybe_url: row.maybe_url,
|
||||
message: row.message,
|
||||
created_at: row.created_at,
|
||||
maybe_user,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ModerationListDebugLogsForUserSuccessResponse {
|
||||
success: true,
|
||||
debug_logs,
|
||||
next_cursor: result.next_cursor,
|
||||
}))
|
||||
}
|
||||
@@ -14,6 +14,7 @@ pub mod user_stripe_data;
|
||||
pub mod wallet_ledger_entries;
|
||||
pub mod wallets;
|
||||
pub mod user_daily_spends;
|
||||
pub mod top_spenders;
|
||||
pub mod user_spend_events;
|
||||
pub mod user_spend_summaries;
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod moderator_list_top_spenders_handler;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user