mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 11:19:29 -04:00
Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53bd51c91f | ||
|
|
4f2b63ac31 | ||
|
|
658b15d6aa | ||
|
|
60461e9208 | ||
|
|
a8b4541000 | ||
|
|
7cc1c0209f | ||
|
|
43c82e0a82 | ||
|
|
ac0e95c265 | ||
|
|
885fc0a983 | ||
|
|
97215d3a3e | ||
|
|
68912622d9 | ||
|
|
03ab6b8f11 | ||
|
|
314dd37ce8 | ||
|
|
74571c9f47 | ||
|
|
e70df9a72f | ||
|
|
9982d49f99 | ||
|
|
fbc2eaceb4 | ||
|
|
28c0703b60 | ||
|
|
bbe3a837de | ||
|
|
dd8b6338a6 | ||
|
|
8e4ad8b3a1 | ||
|
|
98969a660e | ||
|
|
d71ab61760 | ||
|
|
0c6e742fe1 | ||
|
|
2dd6dc9d50 | ||
|
|
2800c84699 | ||
|
|
9ea44e4aa6 | ||
|
|
57e4887cbf | ||
|
|
7661bf158a | ||
|
|
67a509dff4 | ||
|
|
ca3794d646 | ||
|
|
c833f119a5 | ||
|
|
5b82b722b1 | ||
|
|
c4298cc02b | ||
|
|
9a9b9bd12d | ||
|
|
5944aa2b4d | ||
|
|
c8533c8081 | ||
|
|
c5e65d6e0f | ||
|
|
72f308f8ba | ||
|
|
e4d137e56d | ||
|
|
e09c2e6682 | ||
|
|
009c9832ca | ||
|
|
2394375e11 | ||
|
|
7885df257f | ||
|
|
94e4c3fc01 | ||
|
|
6ee54bdcb9 | ||
|
|
a31138535e | ||
|
|
8899b3dcb5 | ||
|
|
69d9b2013a | ||
|
|
ff56a96816 | ||
|
|
c5e1843b91 | ||
|
|
9a433ff42a | ||
|
|
1469b4fcff | ||
|
|
b1965a6019 | ||
|
|
29b1468e1a | ||
|
|
1008b6f69a | ||
|
|
f797d6f769 | ||
|
|
3cb2c31aa7 | ||
|
|
6f64086834 | ||
|
|
170ee9b069 | ||
|
|
75ab5887f7 | ||
|
|
8e67654b9d | ||
|
|
76172e3956 | ||
|
|
affc19abc5 | ||
|
|
15a04d5f52 | ||
|
|
49a5f67dc8 | ||
|
|
0db259b64a | ||
|
|
363e3fee37 | ||
|
|
5cead36b18 | ||
|
|
f435e9fb7e | ||
|
|
58ebe1f836 |
@@ -233,8 +233,8 @@ jobs:
|
||||
with:
|
||||
projectPath: ./crates/desktop/artcraft
|
||||
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
|
||||
releaseName: "Artcraft v__VERSION__"
|
||||
releaseBody: "See the assets to download this version and install."
|
||||
releaseName: "ArtCraft v__VERSION__"
|
||||
releaseBody: "See the assets below to download this version and install."
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
@@ -99,9 +99,9 @@ jobs:
|
||||
SQLX_OFFLINE: true
|
||||
with:
|
||||
projectPath: ./crates/desktop/artcraft
|
||||
tagName: artcraft-windows-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
|
||||
releaseName: "Artcraft Windows v__VERSION__"
|
||||
releaseBody: "Windows build - See the assets to download this version and install."
|
||||
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
|
||||
releaseName: "ArtCraft v__VERSION__"
|
||||
releaseBody: "Production build of ArtCraft. See the assets below to download and install."
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
@@ -32,6 +32,9 @@ script/production_database_secret_backup.sh
|
||||
script/production_database_secret_connect.sh
|
||||
script/socat_dev_database_connect.sh
|
||||
|
||||
# Python virtual envs (named by convention)
|
||||
venv/
|
||||
|
||||
# Scratch data
|
||||
scratch/
|
||||
test_data/temp/
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n password_hash = ?,\n ip_address_last_update = ?,\n is_without_password = false,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "03761e50f94f2805f1c5c64f598a23af8d708e514a01c6090570b2e0bfabfe2c"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_confirmed = true,\n password_hash = ?,\n ip_address_last_update = ?,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "05de463f1c408793d64fb011b1732e3c1b3cf485076b3e1a4a50d16683b99e46"
|
||||
}
|
||||
+94
-50
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_confirmed,\n users.email_gravatar_hash,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ",
|
||||
"query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_gravatar_hash,\n \n users.email_confirmed,\n users.email_confirmed_by_google,\n users.email_is_synthetic,\n users.is_without_password,\n users.username_is_not_customized,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -45,16 +45,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email_confirmed",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "email_gravatar_hash",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
@@ -64,28 +54,38 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_stripe_customer_id",
|
||||
"ordinal": 5,
|
||||
"name": "email_confirmed",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email_confirmed_by_google",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_loyalty_program_key",
|
||||
"name": "email_is_synthetic",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "disable_gravatar",
|
||||
"name": "is_without_password",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
@@ -95,6 +95,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "username_is_not_customized",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_stripe_customer_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_loyalty_program_key",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "disable_gravatar",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "auto_play_audio_preference",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -104,7 +144,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 14,
|
||||
"name": "auto_play_video_preference",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -114,7 +154,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 15,
|
||||
"name": "preferred_tts_result_visibility: enums::common::visibility::Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
@@ -124,7 +164,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 16,
|
||||
"name": "preferred_w2l_result_visibility: enums::common::visibility::Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
@@ -134,7 +174,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 17,
|
||||
"name": "user_role_slug",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -144,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 18,
|
||||
"name": "is_banned",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -154,7 +194,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 19,
|
||||
"name": "can_access_studio",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -164,7 +204,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 20,
|
||||
"name": "maybe_feature_flags",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -174,7 +214,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 21,
|
||||
"name": "can_use_tts",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -184,7 +224,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 22,
|
||||
"name": "can_use_w2l",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -194,7 +234,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 23,
|
||||
"name": "can_delete_own_tts_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -204,7 +244,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 24,
|
||||
"name": "can_delete_own_w2l_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -214,7 +254,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 25,
|
||||
"name": "can_delete_own_account",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -224,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 26,
|
||||
"name": "can_upload_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -234,7 +274,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"ordinal": 27,
|
||||
"name": "can_upload_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -244,7 +284,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"ordinal": 28,
|
||||
"name": "can_delete_own_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -254,7 +294,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"ordinal": 29,
|
||||
"name": "can_delete_own_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -264,7 +304,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"ordinal": 30,
|
||||
"name": "can_approve_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -274,7 +314,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"ordinal": 31,
|
||||
"name": "can_edit_other_users_profiles",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -284,7 +324,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 32,
|
||||
"name": "can_edit_other_users_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -294,7 +334,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 33,
|
||||
"name": "can_edit_other_users_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -304,7 +344,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"ordinal": 34,
|
||||
"name": "can_delete_other_users_tts_models",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -314,7 +354,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"ordinal": 35,
|
||||
"name": "can_delete_other_users_tts_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -324,7 +364,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"ordinal": 36,
|
||||
"name": "can_delete_other_users_w2l_templates",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -334,7 +374,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 33,
|
||||
"ordinal": 37,
|
||||
"name": "can_delete_other_users_w2l_results",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -344,7 +384,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 34,
|
||||
"ordinal": 38,
|
||||
"name": "can_ban_users",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -354,7 +394,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 35,
|
||||
"ordinal": 39,
|
||||
"name": "can_delete_users",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -374,6 +414,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
@@ -406,5 +450,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1c32bdb79ed2bc1b8988bbb6ea3f2a1db4ed4552d53cb4f31ff4b08de01691f7"
|
||||
"hash": "3ca99449c071cb0a8d90a735e1a767a94be224a02e0d16e81997d569af8d4cbb"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO users\nSET\n token = ?,\n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 19
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "453f0386cfe34e533501ba29dc2022fc449266f9858f16886bde9a0df6b82026"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_address = ?,\n email_confirmed = false,\n email_confirmed_by_google = false,\n email_is_synthetic = false,\n email_gravatar_hash = ?,\n ip_address_last_update = ?,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4737c44e7b1c73b21a245dd9d1f96aecfc4c614ed19ac4b590a26809c98fee03"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 22
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_address = ?,\n email_gravatar_hash = ?,\n email_confirmed = false,\n email_confirmed_by_google = false,\n email_is_synthetic = false,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6fa90a16784c7b7e3436877c2a751bf0b60054640ebe69cb5f33938238def0fe"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n email_confirmed = true,\n email_is_synthetic = false,\n is_without_password = false,\n is_temporary = false,\n password_hash = ?,\n ip_address_last_update = ?,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9cd3a581f6857e5c1db68a103f47eba9714ee4b5eefc8ba02fc49a4b3a19310e"
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n email_address,\n email_is_synthetic\nFROM users\nWHERE token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email_is_synthetic",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c2dd5120b34d62153772acc94f97c3b7a22b7c53216721e54882c0019d2467e6"
|
||||
}
|
||||
Generated
+927
-104
File diff suppressed because it is too large
Load Diff
+26
-2
@@ -36,6 +36,7 @@ members = [
|
||||
"crates/desktop/artcraft",
|
||||
|
||||
# Shared libraries
|
||||
"crates/lib/actix_artcraft",
|
||||
"crates/lib/actix_cors_configs",
|
||||
"crates/lib/actix_helpers",
|
||||
"crates/lib/bootstrap",
|
||||
@@ -80,7 +81,6 @@ members = [
|
||||
"crates/lib/migration",
|
||||
"crates/lib/password",
|
||||
"crates/lib/server_environment",
|
||||
"crates/lib/shared_service_components",
|
||||
"crates/lib/sqlx_mysql_helpers",
|
||||
"crates/lib/storyteller_root",
|
||||
"crates/lib/subprocess_common",
|
||||
@@ -90,6 +90,7 @@ members = [
|
||||
"crates/lib/url_config",
|
||||
"crates/lib/url_utils",
|
||||
"crates/lib/user_input_common",
|
||||
"crates/lib/users",
|
||||
"crates/lib/web_scrapers",
|
||||
#"crates/lib/api_clients/fakeyou_client",
|
||||
#"crates/lib/machine_learning/ml_models",
|
||||
@@ -135,6 +136,23 @@ members = [
|
||||
"crates/vendor/fal-rs/generate",
|
||||
"crates/vendor/limitation",
|
||||
"crates/vendor/tauri-plugin-http",
|
||||
"crates/vendor/Symphonia/symphonia",
|
||||
"crates/vendor/Symphonia/symphonia-bundle-flac",
|
||||
"crates/vendor/Symphonia/symphonia-bundle-mp3",
|
||||
"crates/vendor/Symphonia/symphonia-codec-aac",
|
||||
"crates/vendor/Symphonia/symphonia-codec-alac",
|
||||
"crates/vendor/Symphonia/symphonia-codec-opus",
|
||||
"crates/vendor/Symphonia/symphonia-codec-pcm",
|
||||
"crates/vendor/Symphonia/symphonia-codec-vorbis",
|
||||
"crates/vendor/Symphonia/symphonia-codec-wavpack",
|
||||
"crates/vendor/Symphonia/symphonia-core",
|
||||
"crates/vendor/Symphonia/symphonia-format-isomp4",
|
||||
"crates/vendor/Symphonia/symphonia-format-mkv",
|
||||
"crates/vendor/Symphonia/symphonia-format-ogg",
|
||||
"crates/vendor/Symphonia/symphonia-format-wav",
|
||||
"crates/vendor/Symphonia/symphonia-metadata",
|
||||
"crates/vendor/Symphonia/symphonia-play",
|
||||
"crates/vendor/Symphonia/symphonia-utils-xiph",
|
||||
# NB(bt,2025-05-25): Too many failures on Mac. Disable for now.
|
||||
#"crates/vendor/candle/candle-core",
|
||||
#"crates/vendor/candle/candle-datasets",
|
||||
@@ -186,13 +204,17 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
|
||||
# Internal libs
|
||||
# Setting paths here ensures we don't need to do so in every crate's Cargo.toml
|
||||
# It makes refactoring much easier.
|
||||
actix_artcraft = { path = "crates/lib/actix_artcraft" }
|
||||
actix_helpers = { path = "crates/lib/actix_helpers" }
|
||||
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
|
||||
build_metadata = { path = "crates/lib/build_metadata" }
|
||||
cloud_storage = { path = "crates/api_clients/cloud_storage" }
|
||||
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
|
||||
cloudflare_mitigation = { path = "crates/lib/cloudflare_mitigation" }
|
||||
collections = { path = "crates/lib/data/collections" }
|
||||
component_traits = { path = "crates/service/plugins/component_traits" }
|
||||
cookie_store = { path = "crates/lib/cookie_store" }
|
||||
cookies = { path = "crates/lib/cookies" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
enums_public = { path = "crates/schema/public/enums_public" }
|
||||
@@ -219,6 +241,7 @@ test_data = { path = "crates/lib/test_data" }
|
||||
testing = { path = "crates/lib/testing" }
|
||||
tokens = { path = "crates/schema/public/tokens" }
|
||||
url_utils = { path = "crates/lib/url_utils" }
|
||||
users = { path = "crates/lib/users" }
|
||||
web_base64 = { path = "crates/lib/data/web_base64" }
|
||||
world_labs_client = { path = "crates/api_clients/world_labs_client" }
|
||||
|
||||
@@ -247,12 +270,13 @@ futures = { version = "0.3.31" }
|
||||
hex-literal = "1.0.0"
|
||||
indexmap = "2.9.0"
|
||||
log = "0.4.27"
|
||||
md-5 = "0.10.6" # insecure but used for gravatar
|
||||
once_cell = "1.21.3"
|
||||
r2d2 = "0.8.10"
|
||||
rand = "0.9.1"
|
||||
redis = { version = "1.0.2", features = ["r2d2", "tokio-rustls-comp", "tokio-comp", "cluster-async", "aio"] }
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls" ] }
|
||||
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls", "gzip" ] }
|
||||
reqwest-websocket = { version = "0.5.0" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_derive = "1.0.219"
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
<p align="center">
|
||||
<video src="https://github.com/user-attachments/assets/b4e24c27-d87d-4fd1-8599-dc0d0b8af48d" width="100%" autoplay="true" loop controls>
|
||||
</p>
|
||||
|
||||
<p align="center">The IDE for artists.</p>
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/artcraft"><img alt="Discord" src="https://img.shields.io/discord/1359579021108842617?style=for-the-badge&label=discord&color=ffffff&logo=discord&logoColor=ffffff" /></a>
|
||||
<a href="https://www.youtube.com/@OfficialArtCraftStudios"><img alt="YouTube" src="https://img.shields.io/youtube/channel/subscribers/UCdjY4VG0ntoGwFsKZO4sVWA?style=for-the-badge&logo=YouTube" /></a>
|
||||
<a href="https://x.com/intent/follow?screen_name=get_artcraft"><img alt="X" src="https://img.shields.io/twitter/follow/get_artcraft?style=for-the-badge&label=follow&logo=x&logoColor=ffffff&color=ffffff" /></a>
|
||||
<a href="https://www.linkedin.com/company/artcraft-ai"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin--0A66C2?style=for-the-badge"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
ArtCraft
|
||||
========
|
||||
ArtCraft is the IDE for interactive AI image and video creation.
|
||||
@@ -66,10 +80,3 @@ those providers, for example: OpenArt, FreePik, etc.
|
||||
- [license](./LICENSE.md)
|
||||
- [roadmap](./ROADMAP.md)
|
||||
|
||||
## Social
|
||||
|
||||
[](https://discord.gg/artcraft)
|
||||
[](https://www.youtube.com/@OfficialArtCraftStudios)
|
||||
[](https://www.linkedin.com/company/artcraft-ai)
|
||||
[](https://x.com/get_artcraft)
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN
|
||||
was_eagerly_provisioned;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN
|
||||
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN
|
||||
is_temporary;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN
|
||||
is_temporary BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN
|
||||
email_is_synthetic;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN
|
||||
email_is_synthetic BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -12,6 +12,14 @@ CREATE TABLE users (
|
||||
-- Visible "primary key"
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- This is TRUE for accounts that were provisioned automatically (eg. eager stripe
|
||||
-- checkout flow), and who haven't completed onboarding (eg. finishing with billing).
|
||||
-- Once the user finishes setup, this is set to FALSE.
|
||||
-- Existing users and normal signup flow always sets this to FALSE.
|
||||
-- If enough time has elapsed for an incomplete `is_temporary` user, such user records
|
||||
-- might be possible to delete as we assume the user abandoned sign up long ago.
|
||||
is_temporary BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- Username is a lookup key; display_name allows the user to add custom case.
|
||||
username VARCHAR(20) NOT NULL,
|
||||
display_name VARCHAR(20) NOT NULL,
|
||||
@@ -30,6 +38,8 @@ CREATE TABLE users (
|
||||
-- Set by the user at sign up:
|
||||
-- - When email + password signup, this is the email provided.
|
||||
-- - When Google SSO, this is the email provided by Google.
|
||||
-- - When "eager" checkout flow, this is synthetically generated and
|
||||
-- updated by the user later.
|
||||
email_address VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Email address is confirmed by us.
|
||||
@@ -39,6 +49,11 @@ CREATE TABLE users (
|
||||
-- Email address is confirmed by Google.
|
||||
email_confirmed_by_google BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- We created a user account provisionally, and the email address is not yet real.
|
||||
-- This happens during "eager" checkout flow before users create accounts - we create
|
||||
-- a user account for them with a fake email address (since this is a unique key).
|
||||
email_is_synthetic BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- The role assigned to the user confers permissions.
|
||||
user_role_slug VARCHAR(16) NOT NULL,
|
||||
|
||||
@@ -86,6 +101,7 @@ CREATE TABLE users (
|
||||
password_version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
-- Users signing up from SSO do not have a password at time of account creation.
|
||||
-- Users signing up with eager stripe checkout flow do not have a password at time of creation
|
||||
is_without_password BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== ABUSE TRACKING ==========
|
||||
@@ -191,10 +207,19 @@ CREATE TABLE users (
|
||||
maybe_source VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- How users created their account
|
||||
-- Initially this will be "email_password" or "google_sign_in"
|
||||
-- Older users do not have a value and are assumed to be "email_sign_in"
|
||||
-- Older users do not have a value and are assumed to be "email_password"
|
||||
-- Values:
|
||||
-- - "email_password"
|
||||
-- - "google_sign_in"
|
||||
-- - "stripe_checkout" (eager stripe checkout account provisioning)
|
||||
maybe_signup_method VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Account was provisioned automatically (eg. stripe eager checkout flow)
|
||||
-- This will remain true even if the user finishes account setup, email change,
|
||||
-- password setup, etc. (It's a permanent label on the account for later
|
||||
-- statistical evaluation.)
|
||||
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== MODERATION DETAILS ==========
|
||||
|
||||
-- Different than deleted.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
Dev Setup
|
||||
=========
|
||||
|
||||
# ArtCraft
|
||||
|
||||
ArtCraft is a Rust / Tauri app.
|
||||
|
||||
To set up the ArtCraft development environment, install the following:
|
||||
|
||||
1. [Install Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html).
|
||||
2. [Install npm](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm). (Node version `v24.13.0` works at time of writing.)
|
||||
3. [Install nx](https://nx.dev/docs/getting-started/installation). (Nx version `v22.4.5` works at time of writing.)
|
||||
4. [Install Tauri CLI](https://v2.tauri.app/reference/cli/). (Version `tauri-cli 2.10.0` works at time of writing.)
|
||||
|
||||
An easy way to get started with running the app in development is to run the two commands (in separate terminals):
|
||||
|
||||
**Mac and Linux Development**
|
||||
|
||||
```bash
|
||||
# Run the frontend dev server
|
||||
./script/artcraft/unix_frontend_dev.sh
|
||||
|
||||
# Run the Tauri Rust application
|
||||
./script/artcraft/unix_rust_dev.sh
|
||||
```
|
||||
|
||||
**Windows Development**
|
||||
|
||||
```powershell
|
||||
# Run the frontend dev server
|
||||
.\script\artcraft\windows_frontend_dev.ps1
|
||||
|
||||
# Run the Tauri Rust application
|
||||
.\script\artcraft\windows_rust_dev.ps1
|
||||
```
|
||||
|
||||
|
||||
# ArtCraft Server
|
||||
|
||||
ArtCraft's server is a Rust / Actix app called `storyteller-web`.
|
||||
|
||||
You don't need to run this to develop the ArtCraft application, but it can be useful to spin up
|
||||
a development instance for adding new server functions or as your own private local copy.
|
||||
|
||||
See [dev_setup_server.md](./dev_setup_server.md) for instructions.
|
||||
@@ -0,0 +1,105 @@
|
||||
# ArtCraft Server
|
||||
|
||||
ArtCraft's server is a Rust / Actix app called `storyteller-web`.
|
||||
|
||||
You don't need to run this to develop the ArtCraft application, but it can be useful to spin up
|
||||
a development instance for adding new server functions or as your own private local copy.
|
||||
|
||||
`storyteller-web` can function entirely within the development environment, and ArtCraft can be
|
||||
pointed to a local instance of the server.
|
||||
|
||||
**TODO**: Concisely describe setting up server components. Note: we have
|
||||
previous docs in `old/` that may still be relevant, though they're perhaps
|
||||
slightly out of date.
|
||||
|
||||
## Server Setup (Mac)
|
||||
|
||||
### Install and Migrate MySQL Database
|
||||
|
||||
```bash
|
||||
# For now, the tooling doesn't support MySQL 9.6 as it was just released in January 2026.
|
||||
# This will probably change in the future to no longer need to specify the 8.* series.
|
||||
brew install mysql@8.4
|
||||
|
||||
# Try to connect
|
||||
mysql -uroot
|
||||
|
||||
# If connection fails, start service, then try again.
|
||||
brew services start mysql
|
||||
mysql -uroot
|
||||
|
||||
# If that fails, reboot and try connecting again.
|
||||
```
|
||||
|
||||
Create the `storyteller` database table. Connect to the database:
|
||||
|
||||
```bash
|
||||
sudo mysql -u root
|
||||
|
||||
# If it needs a password, use argument -p and when prompted use "root" as the typical default password
|
||||
```
|
||||
|
||||
Then run this:
|
||||
|
||||
```mysql
|
||||
use mysql;
|
||||
CREATE DATABASE storyteller;
|
||||
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
|
||||
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
|
||||
```
|
||||
|
||||
Then verify access with `./script/dev_mysql_connect.sh`
|
||||
|
||||
```bash
|
||||
cargo install diesel_cli \
|
||||
--no-default-features \
|
||||
--features mysql,sqlite
|
||||
```
|
||||
|
||||
If this fails,
|
||||
|
||||
```bash
|
||||
brew install zstd
|
||||
brew install openssl
|
||||
```
|
||||
|
||||
Possibly,
|
||||
|
||||
```bash
|
||||
export RUSTFLAGS="-L$(brew --prefix zstd)/lib -L$(brew --prefix openssl)/lib"
|
||||
```
|
||||
|
||||
In older Macs, there [have been some issues with Diesel CLI](https://github.com/diesel-rs/diesel/issues/2605)
|
||||
that require a few extra dependencies to be installed:
|
||||
|
||||
```bash
|
||||
# If you're on Mac and the above command didn't work, run the following and then retry:
|
||||
brew install libpq
|
||||
```
|
||||
|
||||
You're ready to run the migrations:
|
||||
|
||||
```bash
|
||||
diesel migration run
|
||||
```
|
||||
|
||||
You might get a scary message about `"Encountered unknown type for Mysql: enum"` -- you can safely ignore this
|
||||
error if you see it in isolation. It doesn't impact the migrations whatsoever.
|
||||
|
||||
Finally, you'll need the sqlx CLI tool to run codegen.
|
||||
You likely won't need this now, but if you change any queries, this will be necessary:
|
||||
|
||||
```bash
|
||||
cargo install sqlx-cli --features rustls,mysql,sqlite
|
||||
```
|
||||
|
||||
### Install Redis (Mac)
|
||||
|
||||
```bash
|
||||
# Install Redis
|
||||
brew install redis
|
||||
|
||||
# Start the service:
|
||||
brew services start redis
|
||||
```
|
||||
|
||||
@@ -7,4 +7,5 @@ pub mod media_file;
|
||||
pub mod prompts;
|
||||
pub mod stripe_artcraft;
|
||||
pub mod subscriptions;
|
||||
pub mod users;
|
||||
pub mod utils;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const CREATE_NEW_USER_ACCOUNT_AND_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/user_signup/subscription_checkout";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest {
|
||||
/// The (non-Stripe) internal identifier for the product or subscription.
|
||||
/// This will be translated into a Stripe identifier.
|
||||
pub plan: Option<ArtcraftSubscriptionSlug>,
|
||||
|
||||
pub cadence: Option<PlanBillingCadence>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
|
||||
pub enum PlanBillingCadence {
|
||||
#[serde(rename = "monthly")]
|
||||
Monthly,
|
||||
|
||||
#[serde(rename = "yearly")]
|
||||
Yearly,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// The checkout session URL.
|
||||
pub stripe_checkout_redirect_url: String,
|
||||
|
||||
/// If a user account was created, these are the details.
|
||||
pub generated_user: Option<UserDetails>,
|
||||
|
||||
/// If a session was created, these are the details.
|
||||
pub session: Option<SessionDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserDetails {
|
||||
/// The generated username.
|
||||
pub username: String,
|
||||
|
||||
/// The generated display name.
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetails {
|
||||
/// A signed session that can be sent as a header, bypassing cookies.
|
||||
/// This is useful for API clients that don't support cookies or Google
|
||||
/// browsers killing cross-domain cookies.
|
||||
pub signed_session: String,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod create_create_new_user_account_and_subscription_checkout;
|
||||
pub mod create_credits_pack_checkout;
|
||||
pub mod create_subscription_checkout;
|
||||
pub mod customer_portal_cancel_plan;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ChangePasswordRequest {
|
||||
pub password: String,
|
||||
pub password_confirmation: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ChangePasswordResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditEmailRequest {
|
||||
pub email_address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditEmailResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditUsernameRequest {
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EditUsernameResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod change_password;
|
||||
pub mod edit_email;
|
||||
pub mod edit_username;
|
||||
pub mod session_info;
|
||||
@@ -0,0 +1,99 @@
|
||||
use crate::common::responses::user_details_light::UserDetailsLight;
|
||||
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
|
||||
use serde_derive::Serialize;
|
||||
use std::collections::BTreeSet;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Copy, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FakeYouPlan {
|
||||
Free,
|
||||
Basic,
|
||||
Standard,
|
||||
Pro,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Copy, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StorytellerStreamPlan {
|
||||
Free,
|
||||
Basic,
|
||||
Standard,
|
||||
Pro,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionUserInfo {
|
||||
pub core_info: UserDetailsLight,
|
||||
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_gravatar_hash: String,
|
||||
|
||||
pub onboarding: SessionOnboardingState,
|
||||
|
||||
// TODO(bt,2024-03-30): Remove legacy feature flag
|
||||
#[deprecated(note = "DO NOT USE. Use `maybe_feature_flags` instead! The flag you're looking for is `studio`.")]
|
||||
pub can_access_studio: bool,
|
||||
|
||||
/// Collection of feature / rollout flags
|
||||
/// This is the proper place to detect if a user has access to some rollout (non-paywall) feature.
|
||||
/// NB: The BTreeSet maintains order so React doesn't introduce re-render state bugs when order changes
|
||||
pub maybe_feature_flags: BTreeSet<UserFeatureFlag>,
|
||||
|
||||
// Premium plans:
|
||||
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
|
||||
pub fakeyou_plan: FakeYouPlan,
|
||||
|
||||
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
|
||||
pub storyteller_stream_plan: StorytellerStreamPlan,
|
||||
|
||||
// Usage permissions:
|
||||
pub can_use_tts: bool,
|
||||
pub can_use_w2l: bool,
|
||||
pub can_delete_own_tts_results: bool,
|
||||
pub can_delete_own_w2l_results: bool,
|
||||
pub can_delete_own_account: bool,
|
||||
|
||||
// Contribution permissions:
|
||||
pub can_upload_tts_models: bool,
|
||||
pub can_upload_w2l_templates: bool,
|
||||
pub can_delete_own_tts_models: bool,
|
||||
pub can_delete_own_w2l_templates: bool,
|
||||
|
||||
// Moderation permissions:
|
||||
pub can_approve_w2l_templates: bool,
|
||||
pub can_edit_other_users_profiles: bool,
|
||||
pub can_edit_other_users_tts_models: bool,
|
||||
pub can_edit_other_users_w2l_templates: bool,
|
||||
pub can_delete_other_users_tts_models: bool,
|
||||
pub can_delete_other_users_tts_results: bool,
|
||||
pub can_delete_other_users_w2l_templates: bool,
|
||||
pub can_delete_other_users_w2l_results: bool,
|
||||
pub can_ban_users: bool,
|
||||
pub can_delete_users: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionOnboardingState {
|
||||
/// If true, the user hasn't set their email.
|
||||
pub email_not_set: bool,
|
||||
|
||||
/// If true, the user hasn't confirmed their email.
|
||||
pub email_not_confirmed: bool,
|
||||
|
||||
/// If true, the user hasn't set their password.
|
||||
pub password_not_set: bool,
|
||||
|
||||
/// If true, the user hasn't set their username (the username they have is random).
|
||||
pub username_not_customized: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SessionInfoSuccessResponse {
|
||||
pub success: bool,
|
||||
pub logged_in: bool,
|
||||
pub user: Option<SessionUserInfo>,
|
||||
}
|
||||
+18
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_edit_image::{bytedance_seedream_v4_edit_image, BytedanceSeedreamV4EditImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -53,6 +54,23 @@ pub enum EnqueueBytedanceSeedreamV4EditImageSize {
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4EditImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.03 per image.
|
||||
let unit_cost = 3;
|
||||
let cost = match self.num_images {
|
||||
None => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::One) => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two) => unit_cost * 2,
|
||||
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Three) => unit_cost * 3,
|
||||
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Four) => unit_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4_edit_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4EditImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+18
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_edit_image::{bytedance_seedream_v4p5_edit_image, BytedanceSeedreamV4p5EditImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -52,6 +53,23 @@ pub enum EnqueueBytedanceSeedreamV4p5EditImageSize {
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.04 per image.
|
||||
let unit_cost = 4;
|
||||
let cost = match self.num_images {
|
||||
None => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::One) => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two) => unit_cost * 2,
|
||||
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three) => unit_cost * 3,
|
||||
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four) => unit_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4p5_edit_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+15
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::gemini_25_flash_image::edit::gemini_25_flash_image_edit;
|
||||
use fal::prelude::fal_ai::gemini_25_flash_image::edit::Gemini25FlashImageEditInput;
|
||||
use fal::webhook::WebhookResponse;
|
||||
@@ -49,6 +50,20 @@ pub enum Gemini25FlashEditAspectRatio {
|
||||
NineBySixteen, // NB: No NineByTwentyOne ?
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for Gemini25FlashEditArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
|
||||
match self.num_images {
|
||||
Gemini25FlashEditNumImages::One => 4,
|
||||
Gemini25FlashEditNumImages::Two => 8,
|
||||
Gemini25FlashEditNumImages::Three => 12,
|
||||
Gemini25FlashEditNumImages::Four => 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
|
||||
args: Gemini25FlashEditArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+26
@@ -2,6 +2,7 @@ use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::gpt_image_1::edit_image::byok::{gpt_edit_image, GptEditImageRequest};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -44,6 +45,31 @@ pub enum GptEditImageNumImages{
|
||||
Four,
|
||||
}
|
||||
|
||||
|
||||
// NB: These are BYOK, so they're not Fal's prices
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for GptEditImageByokArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Can't find details, so using this: https://www.reddit.com/r/OpenAI/comments/1krfwa1/pricing_gpt_image_1_model/
|
||||
// Prompts are billed similarly to other GPT models. Image outputs cost approximately $0.01 (low), $0.04 (medium),
|
||||
// and $0.17 (high) for square images.
|
||||
// We're likely losing money on this, but that's okay. Will adjust in the future to be fair to users and us.
|
||||
let base_cost = match self.quality {
|
||||
GptEditImageQuality::Auto => 17,
|
||||
GptEditImageQuality::Low => 1,
|
||||
GptEditImageQuality::Medium => 4,
|
||||
GptEditImageQuality::High => 17,
|
||||
};
|
||||
let cost = match self.num_images {
|
||||
GptEditImageNumImages::One => base_cost,
|
||||
GptEditImageNumImages::Two => base_cost * 2,
|
||||
GptEditImageNumImages::Three => base_cost * 3,
|
||||
GptEditImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_gpt_image_1_edit_image_webhook<V: IntoUrl>(
|
||||
args: GptEditImageByokArgs<'_, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+29
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::gpt_image::gpt_image_1p5_image_edit::{gpt_image_1p5_image_edit, GptImage1p5ImageEditInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -69,6 +70,34 @@ pub enum EnqueueGptImage1p5EditImageOutputFormat {
|
||||
Webp,
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1p5EditImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost different amounts based on the number of images, quality, and size.
|
||||
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
|
||||
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
|
||||
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
|
||||
let use_quality = self.quality.unwrap_or(EnqueueGptImage1p5EditImageQuality::Medium);
|
||||
let use_size = self.image_size.unwrap_or(EnqueueGptImage1p5EditImageSize::Square);
|
||||
let base_cost = match (use_quality, use_size) {
|
||||
(EnqueueGptImage1p5EditImageQuality::Low, EnqueueGptImage1p5EditImageSize::Square) => 1,
|
||||
(EnqueueGptImage1p5EditImageQuality::Low, _) => 1,
|
||||
(EnqueueGptImage1p5EditImageQuality::Medium, EnqueueGptImage1p5EditImageSize::Square) => 3,
|
||||
(EnqueueGptImage1p5EditImageQuality::Medium, _) => 5,
|
||||
(EnqueueGptImage1p5EditImageQuality::High, EnqueueGptImage1p5EditImageSize::Square) => 13,
|
||||
(EnqueueGptImage1p5EditImageQuality::High, _) => 20,
|
||||
};
|
||||
let cost = match self.num_images {
|
||||
EnqueueGptImage1p5EditImageNumImages::One => base_cost,
|
||||
EnqueueGptImage1p5EditImageNumImages::Two => base_cost * 2,
|
||||
EnqueueGptImage1p5EditImageNumImages::Three => base_cost * 3,
|
||||
EnqueueGptImage1p5EditImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
|
||||
args: EnqueueGptImage1p5EditImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+25
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_image_edit::{nano_banana_pro_image_edit, NanoBananaProImageEditInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -56,6 +57,30 @@ pub enum EnqueueNanoBananaProEditImageAspectRatio {
|
||||
NineBySixteen, // NB: No NineByTwentyOne ?
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBananaProEditImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.15 per image.
|
||||
// For $1.00, you can run this model 7 times.
|
||||
// 4K outputs will be charged at double the standard rate.
|
||||
// If web search is used, an additional $0.015 will be charged.
|
||||
// Note: Pricing may change in the future.
|
||||
let cost = match self.resolution {
|
||||
None => 15,
|
||||
Some(EnqueueNanoBananaProEditImageResolution::OneK) => 15,
|
||||
Some(EnqueueNanoBananaProEditImageResolution::TwoK) => 15,
|
||||
Some(EnqueueNanoBananaProEditImageResolution::FourK) => 30,
|
||||
};
|
||||
let cost = match self.num_images {
|
||||
EnqueueNanoBananaProEditImageNumImages::One => cost,
|
||||
EnqueueNanoBananaProEditImageNumImages::Two => cost * 2,
|
||||
EnqueueNanoBananaProEditImageNumImages::Three => cost * 3,
|
||||
EnqueueNanoBananaProEditImageNumImages::Four => cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
|
||||
args: EnqueueNanoBananaProEditImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+18
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::qwen_image_edit::{qwen_image_edit, QwenImageEditInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -45,6 +46,23 @@ pub enum QwenImageEditSize {
|
||||
//Custom { width: u32, height: u32 }, // TODO
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl, R: IntoUrl> FalRequestCostCalculator for QwenImageEditArgs<'_, U, R> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.03 per megapixel. -- Ugh, not calculating that
|
||||
let base_cost = 3;
|
||||
let cost = match self.num_images {
|
||||
None => base_cost,
|
||||
Some(QwenImageEditNumImages::One) => base_cost,
|
||||
Some(QwenImageEditNumImages::Two) => base_cost * 2,
|
||||
Some(QwenImageEditNumImages::Three) => base_cost * 3,
|
||||
Some(QwenImageEditNumImages::Four) => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
|
||||
args: QwenImageEditArgs<'_, U, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+17
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::flux_pro::v1_1::{v1_1, FluxProPlusTextToImageInput, ImageSizeProperty};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -33,6 +34,22 @@ pub enum FluxPro11NumImages {
|
||||
Four,
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for FluxPro11Args<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.04 per megapixel. -- NB: What? Not going to calculate that.
|
||||
let base_cost = 6;
|
||||
let cost = match self.num_images {
|
||||
FluxPro11NumImages::One => base_cost,
|
||||
FluxPro11NumImages::Two => base_cost * 2,
|
||||
FluxPro11NumImages::Three => base_cost * 3,
|
||||
FluxPro11NumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_flux_pro_11_text_to_image_webhook<U: IntoUrl>(
|
||||
args: FluxPro11Args<'_, U>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+17
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::flux_pro::v1_1_ultra::{v1_1_ultra, AspectRatioProperty, FluxProUltraTextToImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -35,6 +36,22 @@ pub enum FluxPro11UltraNumImages {
|
||||
Four,
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for FluxPro11UltraArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.06 per image.
|
||||
let base_cost = 6;
|
||||
let cost = match self.num_images {
|
||||
FluxPro11UltraNumImages::One => base_cost,
|
||||
FluxPro11UltraNumImages::Two => base_cost * 2,
|
||||
FluxPro11UltraNumImages::Three => base_cost * 3,
|
||||
FluxPro11UltraNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
|
||||
args: FluxPro11UltraArgs<'_, U>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+26
@@ -2,6 +2,7 @@ use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::prelude::fal_ai::gpt_image_1::edit_image::byok::{gpt_text_to_image, GptTextToImageRequest};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -43,6 +44,31 @@ pub enum GptTextToImageNumImages{
|
||||
Four,
|
||||
}
|
||||
|
||||
|
||||
// NB: These are BYOK, so they're not Fal's prices
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for GptTextToImageByokArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Can't find details, so using this: https://www.reddit.com/r/OpenAI/comments/1krfwa1/pricing_gpt_image_1_model/
|
||||
// Prompts are billed similarly to other GPT models. Image outputs cost approximately $0.01 (low), $0.04 (medium),
|
||||
// and $0.17 (high) for square images.
|
||||
// We're likely losing money on this, but that's okay. Will adjust in the future to be fair to users and us.
|
||||
let base_cost = match self.quality {
|
||||
GptTextToImageQuality::Auto => 17,
|
||||
GptTextToImageQuality::Low => 1,
|
||||
GptTextToImageQuality::Medium => 4,
|
||||
GptTextToImageQuality::High => 17,
|
||||
};
|
||||
let cost = match self.num_images {
|
||||
GptTextToImageNumImages::One => base_cost,
|
||||
GptTextToImageNumImages::Two => base_cost * 2,
|
||||
GptTextToImageNumImages::Three => base_cost * 3,
|
||||
GptTextToImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_gpt_image_1_text_to_image_webhook<V: IntoUrl>(
|
||||
args: GptTextToImageByokArgs<'_, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+19
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::BytedanceSeedreamV4TextToImageInput;
|
||||
use fal::prelude::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::bytedance_seedream_v4_text_to_image;
|
||||
use fal::webhook::WebhookResponse;
|
||||
@@ -53,6 +54,24 @@ pub enum EnqueueBytedanceSeedreamV4TextToImageSize {
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4TextToImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Copied from edit image version
|
||||
// Your request will cost $0.03 per image.
|
||||
let unit_cost = 3;
|
||||
let cost = match self.num_images {
|
||||
None => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::One) => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two) => unit_cost * 2,
|
||||
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Three) => unit_cost * 3,
|
||||
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Four) => unit_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4TextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+20
@@ -4,6 +4,8 @@ use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_text_to_image::{bytedance_seedream_v4p5_text_to_image, BytedanceSeedreamV4p5TextToImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageNumImages};
|
||||
|
||||
pub struct EnqueueBytedanceSeedreamV4p5TextToImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
@@ -51,6 +53,24 @@ pub enum EnqueueBytedanceSeedreamV4p5TextToImageSize {
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// NB: Copied from edit image costs
|
||||
// Your request will cost $0.04 per image.
|
||||
let unit_cost = 4;
|
||||
let cost = match self.num_images {
|
||||
None => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One) => unit_cost,
|
||||
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two) => unit_cost * 2,
|
||||
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Three) => unit_cost * 3,
|
||||
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four) => unit_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+17
@@ -5,6 +5,8 @@ use fal::prelude::fal_ai::gemini_25_flash_image::text_to_image::gemini_25_flash_
|
||||
use fal::prelude::fal_ai::gemini_25_flash_image::text_to_image::Gemini25FlashTextToImageInput;
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use crate::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{Gemini25FlashEditArgs, Gemini25FlashEditNumImages};
|
||||
|
||||
pub struct Gemini25FlashTextToImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
@@ -46,6 +48,21 @@ pub enum Gemini25FlashTextToImageAspectRatio {
|
||||
NineBySixteen, // NB: No NineByTwentyOne ?
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for Gemini25FlashTextToImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// NB: This was copied from the image-to-image case.
|
||||
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
|
||||
match self.num_images {
|
||||
Gemini25FlashTextToImageNumImages::One => 4,
|
||||
Gemini25FlashTextToImageNumImages::Two => 8,
|
||||
Gemini25FlashTextToImageNumImages::Three => 12,
|
||||
Gemini25FlashTextToImageNumImages::Four => 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_gemini_25_flash_text_to_image_webhook<R: IntoUrl>(
|
||||
args: Gemini25FlashTextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+29
@@ -1,6 +1,7 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::endpoints::fal_ai::gpt_image::gpt_image_1p5_text_to_image::{gpt_image_1p5_text_to_image, GptImage1p5TextToImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
@@ -61,6 +62,34 @@ pub enum EnqueueGptImage1p5TextToImageOutputFormat {
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1p5TextToImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// NB: copied from edit image case - should be correct, but not verified.
|
||||
// Your request will cost different amounts based on the number of images, quality, and size.
|
||||
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
|
||||
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
|
||||
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
|
||||
let use_quality = self.quality.unwrap_or(EnqueueGptImage1p5TextToImageQuality::Medium);
|
||||
let use_size = self.image_size.unwrap_or(EnqueueGptImage1p5TextToImageSize::Square);
|
||||
let base_cost = match (use_quality, use_size) {
|
||||
(EnqueueGptImage1p5TextToImageQuality::Low, EnqueueGptImage1p5TextToImageSize::Square) => 1,
|
||||
(EnqueueGptImage1p5TextToImageQuality::Low, _) => 1,
|
||||
(EnqueueGptImage1p5TextToImageQuality::Medium, EnqueueGptImage1p5TextToImageSize::Square) => 3,
|
||||
(EnqueueGptImage1p5TextToImageQuality::Medium, _) => 5,
|
||||
(EnqueueGptImage1p5TextToImageQuality::High, EnqueueGptImage1p5TextToImageSize::Square) => 13,
|
||||
(EnqueueGptImage1p5TextToImageQuality::High, _) => 20,
|
||||
};
|
||||
let cost = match self.num_images {
|
||||
EnqueueGptImage1p5TextToImageNumImages::One => base_cost,
|
||||
EnqueueGptImage1p5TextToImageNumImages::Two => base_cost * 2,
|
||||
EnqueueGptImage1p5TextToImageNumImages::Three => base_cost * 3,
|
||||
EnqueueGptImage1p5TextToImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueGptImage1p5TextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
+27
@@ -4,6 +4,8 @@ use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_text_to_image::{nano_banana_pro_text_to_image, NanoBananaProTextToImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use crate::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
|
||||
|
||||
pub struct EnqueueNanoBananaProTextToImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
@@ -53,6 +55,31 @@ pub enum EnqueueNanoBananaProTextToImageAspectRatio {
|
||||
NineBySixteen, // NB: No NineByTwentyOne ?
|
||||
}
|
||||
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBananaProTextToImageArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.15 per image.
|
||||
// For $1.00, you can run this model 7 times.
|
||||
// 4K outputs will be charged at double the standard rate.
|
||||
// If web search is used, an additional $0.015 will be charged.
|
||||
// Note: Pricing may change in the future.
|
||||
let cost = match self.resolution {
|
||||
None => 15,
|
||||
Some(EnqueueNanoBananaProTextToImageResolution::OneK) => 15,
|
||||
Some(EnqueueNanoBananaProTextToImageResolution::TwoK) => 15,
|
||||
Some(EnqueueNanoBananaProTextToImageResolution::FourK) => 30,
|
||||
};
|
||||
let cost = match self.num_images {
|
||||
EnqueueNanoBananaProTextToImageNumImages::One => cost,
|
||||
EnqueueNanoBananaProTextToImageNumImages::Two => cost * 2,
|
||||
EnqueueNanoBananaProTextToImageNumImages::Three => cost * 3,
|
||||
EnqueueNanoBananaProTextToImageNumImages::Four => cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueNanoBananaProTextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "artcraft"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
description = "Artcraft"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
@@ -54,6 +54,7 @@ bytes = "1.10.1"
|
||||
chrono.workspace = true
|
||||
directories = "6.0.0"
|
||||
dirs = "6.0.0"
|
||||
discord-rich-presence = "1.1.0"
|
||||
futures = "0.3"
|
||||
image = "0.25.5"
|
||||
indexmap.workspace = true
|
||||
@@ -77,10 +78,6 @@ url.workspace = true
|
||||
|
||||
# NB: We need to vendor this in so that we can access the cookies inside via the private API
|
||||
tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
|
||||
|
||||
# NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error
|
||||
# types and can't do threading. We need to wait for a new version to unpin from Git.
|
||||
discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" }
|
||||
tauri-plugin-opener = "2"
|
||||
|
||||
# # Transparent title bar on MacOS
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// This is a comprehensive list of common aspect ratios.
|
||||
/// This is a comprehensive list of common aspect ratios you can specify when enqueuing a generation.
|
||||
/// Not every model will support every aspect ratio.
|
||||
/// In the case a model doesn't support the aspect ratio, pick the nearest option.
|
||||
/// In the case a model doesn't support the aspect ratio, gracefully pick the nearest option.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonAspectRatio {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod aspect_ratio;
|
||||
pub mod aspect_ratio;
|
||||
pub mod resolution;
|
||||
@@ -0,0 +1,17 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// This is a comprehensive list of common resolutions you can specify when enqueuing a generation.
|
||||
/// Not every model will support every resolution
|
||||
/// In the case a model doesn't support the resolution, gracefully pick the nearest option.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonResolution {
|
||||
/// Models: Nano Banana Pro
|
||||
OneK,
|
||||
/// Models: Nano Banana Pro
|
||||
TwoK,
|
||||
/// Models: (None)
|
||||
ThreeK,
|
||||
/// Models: Nano Banana Pro
|
||||
FourK,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod resolution_to_artcraft_nano_banana_pro;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenImageResolution;
|
||||
|
||||
pub fn resolution_to_artcraft_nano_banana_pro(resolution: CommonResolution) -> NanoBananaProMultiFunctionImageGenImageResolution {
|
||||
match resolution {
|
||||
CommonResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
|
||||
CommonResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
|
||||
// NB: ThreeK doesn't exist in Nano Banana Pro, use FourK as nearest higher resolution
|
||||
CommonResolution::ThreeK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
|
||||
CommonResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod common_resolution;
|
||||
pub mod convert;
|
||||
+1
-2
@@ -62,8 +62,7 @@ pub async fn handle_artcraft_gpt_image_1p5_text_to_image(
|
||||
prompt: request.prompt.clone(),
|
||||
image_size: aspect_ratio,
|
||||
num_images,
|
||||
// Not provided for text-to-image
|
||||
image_media_tokens: None,
|
||||
image_media_tokens: request.image_media_tokens.clone(),
|
||||
mask_image_token: None,
|
||||
input_fidelity: None,
|
||||
// Not provided
|
||||
|
||||
+21
-8
@@ -1,5 +1,7 @@
|
||||
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro;
|
||||
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
|
||||
use crate::core::api_adapters::resolution::convert::resolution_to_artcraft_nano_banana_pro::resolution_to_artcraft_nano_banana_pro;
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageResolution, EditImageSize};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
@@ -61,18 +63,12 @@ pub async fn handle_artcraft_nano_banana_pro_text_to_image(
|
||||
};
|
||||
|
||||
let aspect_ratio = get_aspect_ratio(request);
|
||||
|
||||
let resolution = request.image_resolution
|
||||
.map(|image_resolution| match image_resolution {
|
||||
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
|
||||
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
|
||||
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
|
||||
});
|
||||
let resolution = get_resolution(request);
|
||||
|
||||
let request = NanoBananaProMultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt.clone(),
|
||||
image_media_tokens: None,
|
||||
image_media_tokens: request.image_media_tokens.clone(),
|
||||
num_images,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
@@ -126,3 +122,20 @@ fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<NanoBananaPro
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_resolution(request: &EnqueueTextToImageRequest) -> Option<NanoBananaProMultiFunctionImageGenImageResolution> {
|
||||
if let Some(common_resolution) = request.common_resolution {
|
||||
return Some(resolution_to_artcraft_nano_banana_pro(common_resolution));
|
||||
}
|
||||
|
||||
if let Some(resolution) = request.image_resolution {
|
||||
// Deprecated resolution
|
||||
return Some(match resolution {
|
||||
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
|
||||
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
|
||||
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
|
||||
})
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
+13
-7
@@ -1,3 +1,5 @@
|
||||
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
|
||||
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
@@ -16,6 +18,8 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
|
||||
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
@@ -34,9 +38,6 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use tauri::{AppHandle, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
|
||||
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
|
||||
|
||||
/// This is used in the Tauri command bridge.
|
||||
/// Don't change the serializations without coordinating with the frontend.
|
||||
@@ -88,15 +89,20 @@ pub struct EnqueueTextToImageRequest {
|
||||
/// Text prompt for the image generation. Required.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Aspect ratio.
|
||||
#[deprecated(note="use common_aspect_ratio")]
|
||||
pub aspect_ratio: Option<TextToImageSize>,
|
||||
|
||||
/// New field for aspect ratio.
|
||||
/// Not all models support each of these aspect ratios, but we can choose or interpolate sensibly.
|
||||
pub common_aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// New field for resolution.
|
||||
/// Not all models support each of these resolutions, but we can choose or interpolate sensibly.
|
||||
pub common_resolution: Option<CommonResolution>,
|
||||
|
||||
/// Aspect ratio.
|
||||
#[deprecated(note="use common_aspect_ratio")]
|
||||
pub aspect_ratio: Option<TextToImageSize>,
|
||||
|
||||
/// Aspect ratio.
|
||||
#[deprecated(note="use common_resolution")]
|
||||
pub image_resolution: Option<TextToImageResolution>,
|
||||
|
||||
/// The number of images to generate.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
/// The version of the ArtCraft desktop app reported in the UI and via Analytics.
|
||||
/// This should match the CI builds.
|
||||
pub const ARTCRAFT_VERSION: &str = "0.5.0";
|
||||
pub const ARTCRAFT_VERSION: &str = "0.6.0";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# For things that are too application-logic specific to live in generic actix helpers.
|
||||
[package]
|
||||
name = "actix_artcraft"
|
||||
edition = "2024"
|
||||
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 = "actix_artcraft"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
actix_helpers.workspace = true
|
||||
cookies.workspace = true
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
http_headers.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
actix-http.workspace = true
|
||||
actix-service.workspace = true
|
||||
actix-web.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
# (None yet)
|
||||
@@ -0,0 +1,21 @@
|
||||
//! actix_artcraft - for actix helpers that need to be shared between crates,
|
||||
//! but are too application-specific to be in general actix helper crates
|
||||
//!
|
||||
|
||||
// Never allow these
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
|
||||
|
||||
// Okay to toggle
|
||||
#![forbid(unreachable_patterns)]
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
// Always allow
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod requests;
|
||||
pub mod sessions;
|
||||
+13
-84
@@ -3,14 +3,7 @@ use actix_web::HttpRequest;
|
||||
use enums::by_table::users::user_signup_source::UserSignupSource;
|
||||
use log::warn;
|
||||
|
||||
const ARTCRAFT : &str = "artcraft";
|
||||
const FAKEYOU : &str = "fakeyou";
|
||||
const STORYTELLER : &str = "storyteller";
|
||||
|
||||
/// This is for the users table `maybe_source` field which is populated during account creation.
|
||||
/// While we could back this with an enum, there may be a motivation to use this VARCHAR(255) field
|
||||
/// for more robust payloads and user tracking in the future.
|
||||
pub fn get_request_signup_source(http_request: &HttpRequest) -> Option<&'static str> {
|
||||
pub fn get_request_signup_source_enum(http_request: &HttpRequest) -> Option<UserSignupSource> {
|
||||
// NB: "Origin" vs "Referrer"
|
||||
//
|
||||
// Basically:
|
||||
@@ -27,12 +20,14 @@ pub fn get_request_signup_source(http_request: &HttpRequest) -> Option<&'static
|
||||
match maybe_origin {
|
||||
Ok(Some(uri)) => {
|
||||
if let Some(host) = uri.host() {
|
||||
if host.contains(ARTCRAFT) {
|
||||
return Some(ARTCRAFT);
|
||||
} else if host.contains(STORYTELLER) {
|
||||
return Some(STORYTELLER);
|
||||
} else if host.contains(FAKEYOU) {
|
||||
return Some(FAKEYOU);
|
||||
if host.contains("getartcraft.com") {
|
||||
return Some(UserSignupSource::ArtCraftGetWeb);
|
||||
} else if host.contains("artcraft.ai") {
|
||||
return Some(UserSignupSource::ArtCraftAiWeb);
|
||||
} else if host.contains("storyteller") {
|
||||
return Some(UserSignupSource::Storyteller);
|
||||
} else if host.contains("fakeyou") {
|
||||
return Some(UserSignupSource::FakeYou);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,79 +45,13 @@ pub fn get_request_signup_source(http_request: &HttpRequest) -> Option<&'static
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_request_signup_source_enum(http_request: &HttpRequest) -> Option<UserSignupSource> {
|
||||
get_request_signup_source(http_request)
|
||||
.and_then(|source_str| UserSignupSource::from_str(source_str).ok())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::test::TestRequest;
|
||||
|
||||
mod origin_header_str {
|
||||
use super::*;
|
||||
use crate::http_server::requests::get_request_signup_source::get_request_signup_source;
|
||||
|
||||
#[test]
|
||||
fn artcraft_dot_ai() {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://artcraft.ai"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source(&request), Some("artcraft"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_artcraft_dot_com() {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://getartcraft.com"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source(&request), Some("artcraft"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_dot_get_artcraft_dot_com() {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://api.getartcraft.com"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source(&request), Some("artcraft"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fakeyou_dot_com() {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://fakeyou.com"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source(&request), Some("fakeyou"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_dot_fakeyou_dot_com() {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://api.fakeyou.com"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source(&request), Some("fakeyou"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storyteller_dot_ai() {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://storyteller.ai"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source(&request), Some("storyteller"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_dot_storyteller_dot_ai() {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://api.storyteller.ai"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source(&request), Some("storyteller"));
|
||||
}
|
||||
}
|
||||
|
||||
mod origin_header_enum {
|
||||
use super::*;
|
||||
use crate::http_server::requests::get_request_signup_source::get_request_signup_source_enum;
|
||||
use crate::requests::get_request_signup_source_enum::get_request_signup_source_enum;
|
||||
use enums::by_table::users::user_signup_source::UserSignupSource;
|
||||
|
||||
#[test]
|
||||
@@ -130,7 +59,7 @@ mod tests {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://artcraft.ai"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
|
||||
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraftAiWeb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -138,7 +67,7 @@ mod tests {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://getartcraft.com"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
|
||||
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraftGetWeb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -146,7 +75,7 @@ mod tests {
|
||||
let request = TestRequest::get()
|
||||
.insert_header(("origin", "https://api.getartcraft.com"))
|
||||
.to_http_request();
|
||||
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
|
||||
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraftGetWeb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -0,0 +1 @@
|
||||
pub mod get_request_signup_source_enum;
|
||||
+3
-4
@@ -9,13 +9,12 @@ use actix_web::HttpRequest;
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
|
||||
use crate::sessions::http_user_session_payload::HttpUserSessionPayload;
|
||||
use crate::sessions::payload_signer::HttpUserSessionPayloadSigner;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::session::http::http_user_session_payload::HttpUserSessionPayload;
|
||||
use crate::http_server::session::http::payload_signer::HttpUserSessionPayloadSigner;
|
||||
|
||||
/// Name of the HTTP cookie that carries the session payload
|
||||
const SESSION_COOKIE_NAME : &str = "session";
|
||||
|
||||
@@ -139,7 +138,7 @@ mod tests {
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
|
||||
use crate::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
|
||||
#[test]
|
||||
fn test_create_cookie_payload() {
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
pub mod http_user_session_manager;
|
||||
pub mod http_user_session_payload;
|
||||
pub mod payload_signer;
|
||||
pub mod payload_signer;
|
||||
+2
-4
@@ -1,12 +1,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::sessions::http_user_session_payload::HttpUserSessionPayload;
|
||||
use cookies::jwt_signer::JwtSigner;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::session::http::http_user_session_payload::HttpUserSessionPayload;
|
||||
|
||||
/**
|
||||
* Session payload version history
|
||||
*
|
||||
@@ -57,11 +56,10 @@ impl HttpUserSessionPayloadSigner {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::sessions::payload_signer::HttpUserSessionPayloadSigner;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::session::http::payload_signer::HttpUserSessionPayloadSigner;
|
||||
|
||||
#[test]
|
||||
fn test_encode() {
|
||||
let signer = HttpUserSessionPayloadSigner::new("fake_secret").unwrap();
|
||||
@@ -29,7 +29,8 @@ log = { workspace = true }
|
||||
#symphonia = { version = "0.5.2", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "vorbis", "wav"] }
|
||||
# Opus support still hasn't landed in Symphonia, so we have to use an older version (~0.5) off-master.
|
||||
# Opus is the browser's default (only) recording codec: https://blog.addpipe.com/audio-constraints-getusermedia/
|
||||
symphonia = { git = "https://github.com/pdeljanov/Symphonia.git", rev = "e1a70095c4409478c1ded499fb8dd4afa540fb90", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "opus", "vorbis", "wav"] }
|
||||
# symphonia = { git = "https://github.com/pdeljanov/Symphonia.git", rev = "e1a70095c4409478c1ded499fb8dd4afa540fb90", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "opus", "vorbis", "wav"] }
|
||||
symphonia = { path = "../../../vendor/Symphonia/symphonia", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "opus", "vorbis", "wav"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/dictionary_children_terms.txt");
|
||||
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_children_terms.txt");
|
||||
|
||||
pub (crate) fn contains_children_keyword(prompt_tokens: &[String]) -> bool {
|
||||
static TERM_DICTIONARY : Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const TERMS: &str = include_str!("../../../../../../includes/binary_includes/dictionary_potential_minor_terms.txt");
|
||||
const TERMS: &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_potential_minor_terms.txt");
|
||||
|
||||
pub (crate) fn contains_potential_minor_keyword(prompt_tokens: &[String]) -> bool {
|
||||
static TERM_DICTIONARY: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/dictionary_racist_terms.txt");
|
||||
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_racist_terms.txt");
|
||||
|
||||
pub (crate) fn contains_racist_keyword(prompt_tokens: &[String]) -> bool {
|
||||
static TERM_DICTIONARY : Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/dictionary_sex_terms.txt");
|
||||
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_sex_terms.txt");
|
||||
pub (crate) fn contains_sex_keyword(prompt_tokens: &[String]) -> bool {
|
||||
static TERM_DICTIONARY : Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
TERMS.lines()
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
[package]
|
||||
name = "shared_service_components"
|
||||
edition = "2021"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "shared_service_components"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
openai_sora_client = { workspace = true }
|
||||
errors = { workspace = true }
|
||||
|
||||
# External
|
||||
anyhow = { workspace = true }
|
||||
log = { workspace = true }
|
||||
r2d2_redis = "0.14.0"
|
||||
reqwest = { workspace = true, features = ["multipart", "gzip"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
@@ -1,4 +0,0 @@
|
||||
//! This library contains code shared between `storyteller-web` and jobs.
|
||||
|
||||
pub mod sora_redis_credentials;
|
||||
pub mod sora_redis_sentinel_refresh;
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
use crate::sora_redis_credentials::keys::{BEARER_SUBKEY, COOKIE_SUBKEY, SENTINEL_SUBKEY, SORA_SECRET_REDIS_KEY};
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use openai_sora_client::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn get_sora_credentials_from_redis(
|
||||
redis: &mut PooledConnection<RedisConnectionManager>
|
||||
) -> AnyhowResult<SoraCredentialSet> {
|
||||
|
||||
let values : HashMap<String, String> = redis.hgetall(SORA_SECRET_REDIS_KEY)
|
||||
.map_err(|e| anyhow!("Failed to get Sora credentials from Redis: {}", e))?;
|
||||
|
||||
let bearer = values.get(BEARER_SUBKEY);
|
||||
let cookie = values.get(COOKIE_SUBKEY);
|
||||
let sentinel = values.get(SENTINEL_SUBKEY);
|
||||
|
||||
match (bearer, cookie, sentinel) {
|
||||
(Some(b), Some(c), Some(s)) => {
|
||||
let creds = SoraCredentialBuilder::new()
|
||||
.with_jwt_bearer_token(&b)
|
||||
.with_cookies(&c)
|
||||
.with_sora_sentinel(&s)
|
||||
.build()?;
|
||||
Ok(creds)
|
||||
}
|
||||
_ => Err(anyhow!("redis sora credential values not present")),
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/// Redis key
|
||||
pub (crate) const SORA_SECRET_REDIS_KEY : &str = "sora_secret";
|
||||
|
||||
/// HKEY field for the bearer token
|
||||
pub (crate) const BEARER_SUBKEY : &str = "bearer";
|
||||
|
||||
/// HKEY field for the cookie
|
||||
pub (crate) const COOKIE_SUBKEY : &str = "cookie";
|
||||
|
||||
/// HKEY field for the sentinel
|
||||
pub (crate) const SENTINEL_SUBKEY : &str = "sentinel";
|
||||
|
||||
/// Which subkey to write
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum RedisSoraCredentialSubkey {
|
||||
Bearer,
|
||||
Cookie,
|
||||
Sentinel,
|
||||
}
|
||||
|
||||
impl RedisSoraCredentialSubkey {
|
||||
pub (crate) fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
RedisSoraCredentialSubkey::Bearer => BEARER_SUBKEY,
|
||||
RedisSoraCredentialSubkey::Cookie => COOKIE_SUBKEY,
|
||||
RedisSoraCredentialSubkey::Sentinel => SENTINEL_SUBKEY,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod keys;
|
||||
pub mod set_sora_credential_field_in_redis;
|
||||
pub mod get_sora_credentials_from_redis;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use errors::AnyhowResult;
|
||||
use crate::sora_redis_credentials::keys::{RedisSoraCredentialSubkey, SORA_SECRET_REDIS_KEY};
|
||||
|
||||
pub fn set_sora_credential_field_in_redis(
|
||||
redis: &mut PooledConnection<RedisConnectionManager>,
|
||||
subkey: RedisSoraCredentialSubkey,
|
||||
value: &str,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
redis.hset(SORA_SECRET_REDIS_KEY, subkey.to_str(), value)
|
||||
.map_err(|e| anyhow!("Failed to set Sora credentials in Redis: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod refresh;
|
||||
@@ -1,19 +0,0 @@
|
||||
/// Send a POST request to env variable SORA_SENTINEL_REFRESH_URL
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use reqwest::Client;
|
||||
use std::env;
|
||||
|
||||
const SORA_SENTINEL_REFRESH_URL: &str = "http://oai-token-server/refresh_token";
|
||||
|
||||
/// Refresh the Sora sentinel
|
||||
/// The response body does _not_ contain the new sentinel
|
||||
/// We should read it again from redis or generate a new one here
|
||||
/// For now we refresh in the server handler
|
||||
pub async fn refresh_sentinel() -> AnyhowResult<String> {
|
||||
let client = Client::new();
|
||||
let url = env::var("SORA_SENTINEL_REFRESH_URL").unwrap_or(SORA_SENTINEL_REFRESH_URL.to_string());
|
||||
let response = client.post(url).send().await?;
|
||||
let body = response.text().await?;
|
||||
Ok(body)
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
#![forbid(unused_variables)]
|
||||
#![forbid(warnings)]
|
||||
|
||||
pub (crate) const BANNED_SLURS : &str = include_str!("../../../../includes/binary_includes/banned_slurs.txt");
|
||||
pub (crate) const BANNED_SLURS : &str = include_str!("../../../../includes/binary_includes/safety/banned_slurs.txt");
|
||||
|
||||
pub mod check_for_slurs;
|
||||
pub mod latin_alphabet;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "users"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "users"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
collections.workspace = true
|
||||
primitives.workspace = true
|
||||
|
||||
# External
|
||||
md-5.workspace = true
|
||||
once_cell.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# (None)
|
||||
@@ -0,0 +1,34 @@
|
||||
use md5::{Digest, Md5};
|
||||
|
||||
pub fn email_to_gravatar_hash(email_address: &str) -> String {
|
||||
let email = email_address.trim().to_lowercase();
|
||||
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(email);
|
||||
let hash = hasher.finalize();
|
||||
let gravatar_hash = format!("{:x}", hash);
|
||||
|
||||
gravatar_hash
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::email::email_to_gravatar_hash::email_to_gravatar_hash;
|
||||
|
||||
#[test]
|
||||
fn test_gravatar() {
|
||||
// Email 1
|
||||
assert_eq!(email_to_gravatar_hash("example@example.com"), "23463b99b62a72f26ed677cc556c44e8".to_string());
|
||||
assert_eq!(email_to_gravatar_hash("EXAMPLE@EXAMPLE.COM"), "23463b99b62a72f26ed677cc556c44e8".to_string());
|
||||
assert_eq!(email_to_gravatar_hash(" example@example.com \n "), "23463b99b62a72f26ed677cc556c44e8".to_string());
|
||||
|
||||
// Email 2
|
||||
assert_eq!(email_to_gravatar_hash("echelon@gmail.com"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
|
||||
assert_eq!(email_to_gravatar_hash("ECHELON@GMAIL.COM"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
|
||||
assert_eq!(email_to_gravatar_hash(" ECHELON@GMAIL.COM "), "37e7e18f19d8f654a1ace405c846a9de".to_string());
|
||||
|
||||
// Misc
|
||||
assert_eq!(email_to_gravatar_hash(""), "d41d8cd98f00b204e9800998ecf8427e".to_string());
|
||||
assert_eq!(email_to_gravatar_hash(" "), "d41d8cd98f00b204e9800998ecf8427e".to_string());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use rand::Rng;
|
||||
|
||||
const SYNTHETIC_EMAIL_PREFIX : &str = "synthetic.email";
|
||||
const SYNTHETIC_EMAIL_DOMAIN : &str = "getartcraft.com";
|
||||
|
||||
pub fn generate_random_synthetic_email() -> String {
|
||||
let random_digit = rand::thread_rng().gen_range(u32::MIN..u32::MAX);
|
||||
format!("{}.{}@{}", SYNTHETIC_EMAIL_PREFIX, random_digit, SYNTHETIC_EMAIL_DOMAIN)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::email::generate_random_synthetic_email::generate_random_synthetic_email;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_base_case_success() {
|
||||
assert!(generate_random_synthetic_email().len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_lots() {
|
||||
let mut collection = HashSet::new();
|
||||
for _ in 0..100 {
|
||||
collection.insert(generate_random_synthetic_email());
|
||||
}
|
||||
assert!(collection.len() > 50); // NB: Should be an easy bar to hit
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_format() {
|
||||
let email = generate_random_synthetic_email();
|
||||
assert!(email.contains("@"));
|
||||
assert!(email.contains("."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod email_to_gravatar_hash;
|
||||
pub mod generate_random_synthetic_email;
|
||||
pub mod validate_email_address_format;
|
||||
@@ -0,0 +1,182 @@
|
||||
|
||||
/// Basic email format validation
|
||||
pub fn validate_email_address_format(email: &str) -> Result<(), String> {
|
||||
let email = email.trim();
|
||||
|
||||
if email.is_empty() {
|
||||
return Err("email address cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if email.len() > 255 {
|
||||
return Err("email address is too long".to_string());
|
||||
}
|
||||
|
||||
// Basic format check: must contain exactly one @ with content on both sides
|
||||
let at_count = email.matches('@').count();
|
||||
if at_count != 1 {
|
||||
return Err("email address must contain exactly one @".to_string());
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = email.split('@').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("invalid email format".to_string());
|
||||
}
|
||||
|
||||
let local = parts[0];
|
||||
let domain = parts[1];
|
||||
|
||||
if local.is_empty() {
|
||||
return Err("email address must have content before @".to_string());
|
||||
}
|
||||
|
||||
if domain.is_empty() {
|
||||
return Err("email address must have a domain after @".to_string());
|
||||
}
|
||||
|
||||
// Domain must contain at least one dot
|
||||
if !domain.contains('.') {
|
||||
return Err("email domain must contain a dot".to_string());
|
||||
}
|
||||
|
||||
// Domain cannot start or end with a dot
|
||||
if domain.starts_with('.') || domain.ends_with('.') {
|
||||
return Err("email domain cannot start or end with a dot".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::validate_email_address_format;
|
||||
|
||||
// Valid emails
|
||||
#[test]
|
||||
fn test_valid_simple_email() {
|
||||
assert!(validate_email_address_format("user@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_subdomain() {
|
||||
assert!(validate_email_address_format("user@mail.example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_plus() {
|
||||
assert!(validate_email_address_format("user+tag@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_dots_in_local() {
|
||||
assert!(validate_email_address_format("first.last@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_with_numbers() {
|
||||
assert!(validate_email_address_format("user123@example123.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email_trimmed() {
|
||||
assert!(validate_email_address_format(" user@example.com ").is_ok());
|
||||
}
|
||||
|
||||
// Empty/whitespace
|
||||
#[test]
|
||||
fn test_invalid_empty() {
|
||||
let result = validate_email_address_format("");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_whitespace_only() {
|
||||
let result = validate_email_address_format(" ");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
// Length
|
||||
#[test]
|
||||
fn test_invalid_too_long() {
|
||||
let long_email = format!("{}@example.com", "a".repeat(250));
|
||||
let result = validate_email_address_format(&long_email);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("too long"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_at_max_length() {
|
||||
// 255 chars is the max
|
||||
let local_part = "a".repeat(242); // 242 + @ + example.com (11) = 254
|
||||
let email = format!("{}@example.com", local_part);
|
||||
assert_eq!(email.len(), 254);
|
||||
assert!(validate_email_address_format(&email).is_ok());
|
||||
}
|
||||
|
||||
// @ symbol
|
||||
#[test]
|
||||
fn test_invalid_no_at() {
|
||||
let result = validate_email_address_format("userexample.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("exactly one @"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_multiple_at() {
|
||||
let result = validate_email_address_format("user@@example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("exactly one @"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_multiple_at_separated() {
|
||||
let result = validate_email_address_format("user@name@example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("exactly one @"));
|
||||
}
|
||||
|
||||
// Local part
|
||||
#[test]
|
||||
fn test_invalid_no_local_part() {
|
||||
let result = validate_email_address_format("@example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("content before @"));
|
||||
}
|
||||
|
||||
// Domain part
|
||||
#[test]
|
||||
fn test_invalid_no_domain() {
|
||||
let result = validate_email_address_format("user@");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("domain after @"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_no_dot() {
|
||||
let result = validate_email_address_format("user@localhost");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("must contain a dot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_starts_with_dot() {
|
||||
let result = validate_email_address_format("user@.example.com");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cannot start or end with a dot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_ends_with_dot() {
|
||||
let result = validate_email_address_format("user@example.com.");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cannot start or end with a dot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_domain_only_dot() {
|
||||
let result = validate_email_address_format("user@.");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cannot start or end with a dot"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! users
|
||||
//!
|
||||
|
||||
// Never allow these
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
|
||||
|
||||
// Okay to toggle
|
||||
#![forbid(unreachable_patterns)]
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
// Always allow
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod email;
|
||||
pub mod username;
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
/// Username may be up to this many characters, but not more.
|
||||
pub const USERNAME_MAX_LENGTH: usize = 16;
|
||||
+6
-6
@@ -1,15 +1,15 @@
|
||||
use crate::http_server::validations::validate_username::USERNAME_MAX_LENGTH;
|
||||
use crate::username::constants::USERNAME_MAX_LENGTH;
|
||||
use collections::random_from_array::random_from_array;
|
||||
use once_cell::sync::Lazy;
|
||||
use primitives::iterators::iterate_trimmed_lines_without_comments::iterate_trimmed_lines_without_comments;
|
||||
use primitives::str::first_letter_uppercase::first_letter_uppercase;
|
||||
use rand::distr::{Distribution, StandardUniform};
|
||||
use rand::Rng;
|
||||
use std::collections::HashSet;
|
||||
use rand::distr::{Distribution, StandardUniform};
|
||||
|
||||
pub const ADJECTIVES : &str = include_str!("../../../../../../includes/binary_includes/usernames/atoms/username_adjectives.txt");
|
||||
pub const NOUNS : &str = include_str!("../../../../../../includes/binary_includes/usernames/atoms/username_nouns.txt");
|
||||
pub const NOUNS_ANIMALS: &str = include_str!("../../../../../../includes/binary_includes/usernames/atoms/username_nouns_animals.txt");
|
||||
pub const ADJECTIVES : &str = include_str!("../../../../../includes/binary_includes/usernames/atoms/username_adjectives.txt");
|
||||
pub const NOUNS : &str = include_str!("../../../../../includes/binary_includes/usernames/atoms/username_nouns.txt");
|
||||
pub const NOUNS_ANIMALS: &str = include_str!("../../../../../includes/binary_includes/usernames/atoms/username_nouns_animals.txt");
|
||||
|
||||
static ALL_NOUNS : Lazy<Vec<&'static str>> = Lazy::new(|| {
|
||||
iterate_trimmed_lines_without_comments(NOUNS.lines())
|
||||
@@ -120,7 +120,7 @@ fn maybe_random_safe_digit() -> Option<u32> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
use crate::util::generate_random_username::generate_random_username;
|
||||
use crate::username::generate_random_username::generate_random_username;
|
||||
|
||||
#[test]
|
||||
fn test_base_case_success() {
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod generate_random_username;
|
||||
pub mod constants;
|
||||
+5
@@ -64,6 +64,11 @@ pub async fn create_account_from_email_and_password(
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: args.maybe_user_token,
|
||||
|
||||
// These flags represent eagerly created users and are false for this flow
|
||||
is_temporary: false,
|
||||
email_is_synthetic: false,
|
||||
was_eagerly_provisioned: false,
|
||||
},
|
||||
Transactor::for_pool(mysql_pool),
|
||||
).await?;
|
||||
|
||||
+5
@@ -59,6 +59,11 @@ pub async fn create_account_from_google_sso(
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: None,
|
||||
|
||||
// These flags represent eagerly created users and are false for this flow
|
||||
is_temporary: false,
|
||||
email_is_synthetic: false,
|
||||
was_eagerly_provisioned: false,
|
||||
},
|
||||
transactor,
|
||||
).await?;
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// NB: Incrementally getting rid of build warnings...
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use crate::queries::users::user::create::create_account_error::CreateAccountError;
|
||||
use crate::queries::users::user::create::create_account_generic::{create_account_generic, GenericCreateAccountArgs};
|
||||
use crate::utils::transactor::Transactor;
|
||||
use enums::by_table::users::user_signup_method::UserSignupMethod;
|
||||
use enums::by_table::users::user_signup_source::UserSignupSource;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Stripe checkout sessions do not have a real email address or password.
|
||||
/// The user will create these once the checkout session is completed.
|
||||
/// The password hash field is nullable, so we can't leave it null/empty.
|
||||
const SSO_PASSWORD : &str = "*";
|
||||
|
||||
pub struct CreateAccountFromStripeCheckoutArgs<'a> {
|
||||
// NB: Username will likely be generated by our code, not the user.
|
||||
pub username: &'a str,
|
||||
pub display_name: &'a str,
|
||||
|
||||
// NB: Email will likely be generated by our code, not the user.
|
||||
pub email_address: &'a str,
|
||||
pub email_gravatar_hash: &'a str,
|
||||
|
||||
pub ip_address: &'a str,
|
||||
pub maybe_source: Option<UserSignupSource>,
|
||||
|
||||
/// Comma separated string of feature flags.
|
||||
pub maybe_feature_flags: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub async fn create_account_from_stripe_checkout(
|
||||
args: CreateAccountFromStripeCheckoutArgs<'_>,
|
||||
transactor: Transactor<'_, '_>,
|
||||
) -> Result<UserToken, CreateAccountError>
|
||||
{
|
||||
let result= create_account_generic(
|
||||
GenericCreateAccountArgs {
|
||||
maybe_signup_method: Some(UserSignupMethod::StripeCheckout),
|
||||
|
||||
ip_address: args.ip_address,
|
||||
maybe_feature_flags: args.maybe_feature_flags,
|
||||
maybe_source: args.maybe_source,
|
||||
|
||||
// These are synthetic user accounts that are being eagerly created
|
||||
// If the user abandons the onboarding flow, they can be deleted later.
|
||||
is_temporary: true,
|
||||
was_eagerly_provisioned: true,
|
||||
|
||||
// Stripe checkout users have randomly generated usernames
|
||||
username: args.username,
|
||||
display_name: args.display_name,
|
||||
username_is_generated: true,
|
||||
username_is_not_customized: true,
|
||||
|
||||
// Stripe checkout users have NO REAL EMAIL ADDRESS yet.
|
||||
// Stripe will tell us what their email address is once
|
||||
// they complete the checkout (or the user can update it,
|
||||
// and their write wins.)
|
||||
email_address: args.email_address,
|
||||
email_gravatar_hash: args.email_gravatar_hash,
|
||||
email_confirmed_by_google: false,
|
||||
email_is_synthetic: true,
|
||||
|
||||
// SSO accounts start without a password
|
||||
password_hash: SSO_PASSWORD,
|
||||
is_without_password: true,
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: None,
|
||||
},
|
||||
transactor,
|
||||
).await?;
|
||||
|
||||
Ok(result.user_token)
|
||||
}
|
||||
+37
@@ -23,10 +23,35 @@ pub struct GenericCreateAccountArgs<'a> {
|
||||
|
||||
pub email_address: &'a str,
|
||||
pub email_gravatar_hash: &'a str,
|
||||
|
||||
/// Google did email IDV and told us it's legitimate.
|
||||
pub email_confirmed_by_google: bool,
|
||||
|
||||
/// The email address is not real yet. We generated a fake/synthetic value
|
||||
/// that will hopefully be replaced later. This is for automated user creation
|
||||
/// flows, like Stripe Checkout onboarding flow.
|
||||
pub email_is_synthetic: bool,
|
||||
|
||||
pub password_hash: &'a str,
|
||||
pub is_without_password: bool,
|
||||
|
||||
/// This is initially TRUE for accounts that were provisioned automatically,
|
||||
/// without user intervention, e.g. in the eager Stripe Checkout onboarding
|
||||
/// flow. Accounts in this state have not yet been customized by users and may
|
||||
/// not in fact represent real users if the users abandon the flow. Once the
|
||||
/// user finishes setup, this will be set to false.
|
||||
///
|
||||
/// Existing and normal setup flow sets this flag to false.
|
||||
///
|
||||
/// If enough time has passed without the user interacting with the account,
|
||||
/// then it might be prudent to delete these records.
|
||||
pub is_temporary: bool,
|
||||
|
||||
/// Whether the account was created eagerly (e.g. via stripe checkout flow)
|
||||
/// without the user explicitly setting up an account, choosing an email or
|
||||
/// password, etc. (It's a technical choice to do faster checkout flow.)
|
||||
/// This is a permanent label on the account.
|
||||
pub was_eagerly_provisioned: bool,
|
||||
|
||||
pub ip_address: &'a str,
|
||||
pub maybe_source: Option<UserSignupSource>,
|
||||
@@ -66,6 +91,9 @@ pub async fn create_account_generic(
|
||||
INSERT INTO users
|
||||
SET
|
||||
token = ?,
|
||||
|
||||
is_temporary = ?,
|
||||
|
||||
username = ?,
|
||||
display_name = ?,
|
||||
|
||||
@@ -77,6 +105,9 @@ SET
|
||||
|
||||
email_confirmed = FALSE,
|
||||
email_confirmed_by_google = ?,
|
||||
email_is_synthetic = ?,
|
||||
|
||||
was_eagerly_provisioned = ?,
|
||||
|
||||
profile_markdown = ?,
|
||||
profile_rendered_html = ?,
|
||||
@@ -96,6 +127,9 @@ SET
|
||||
maybe_signup_method = ?
|
||||
"#,
|
||||
user_token.as_str(),
|
||||
|
||||
args.is_temporary,
|
||||
|
||||
args.username,
|
||||
args.display_name,
|
||||
|
||||
@@ -106,6 +140,9 @@ SET
|
||||
args.email_gravatar_hash,
|
||||
|
||||
args.email_confirmed_by_google,
|
||||
args.email_is_synthetic,
|
||||
|
||||
args.was_eagerly_provisioned,
|
||||
|
||||
INITIAL_PROFILE_MARKDOWN,
|
||||
INITIAL_PROFILE_RENDERED_HTML,
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod create_account_from_email_and_password;
|
||||
pub mod create_account_from_google_sso;
|
||||
|
||||
pub (crate) mod create_account_generic;
|
||||
pub mod create_account_from_stripe_checkout;
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use crate::helpers::boolean_converters::i8_to_bool;
|
||||
use crate::helpers::transform_optional_result::transform_optional_result;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Executor, MySql};
|
||||
use std::fmt::Display;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct MaybeUpdateEmailFromSyntheticValueArgs<'a> {
|
||||
pub user_token: &'a UserToken,
|
||||
pub email_address: &'a str,
|
||||
pub email_gravatar_hash: &'a str,
|
||||
pub mysql_connection: &'a mut PoolConnection<MySql>,
|
||||
}
|
||||
pub async fn maybe_update_email_from_synthetic_value(
|
||||
args: MaybeUpdateEmailFromSyntheticValueArgs<'_>,
|
||||
) -> AnyhowResult<()> {
|
||||
let result = sqlx::query_as!(
|
||||
UserEmailCheckRecordRaw,
|
||||
r#"
|
||||
SELECT
|
||||
email_address,
|
||||
email_is_synthetic
|
||||
FROM users
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.user_token.as_str(),
|
||||
)
|
||||
.fetch_one(&mut **args.mysql_connection)
|
||||
.await;
|
||||
|
||||
let maybe_record = transform_optional_result(result)?;
|
||||
|
||||
let record = match maybe_record {
|
||||
Some(record) => record,
|
||||
None => return Err(anyhow!("User not found for token: {}", args.user_token)),
|
||||
};
|
||||
|
||||
let record : UserEmailCheckRecord = record.into();
|
||||
|
||||
if !record.email_is_synthetic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let record_email = record.email_address.to_lowercase();
|
||||
|
||||
// NB: So we don't get the format de-synced, let's just trust the boolean flag.
|
||||
//if !record_email.contains("synthetic") {
|
||||
// return Ok(());
|
||||
//}
|
||||
|
||||
// Normally we don't have much business logic in this crate, but let's be extra careful since we don't have a change log.
|
||||
if record_email.contains("gmail")
|
||||
|| record_email.contains("aol.com")
|
||||
|| record_email.contains("apple")
|
||||
|| record_email.contains("att.net")
|
||||
|| record_email.contains("comcast")
|
||||
|| record_email.contains("earthlink")
|
||||
|| record_email.contains("hotmail")
|
||||
|| record_email.contains("icloud")
|
||||
|| record_email.contains("live.com")
|
||||
|| record_email.contains("mac.com")
|
||||
|| record_email.contains("mail.ru")
|
||||
|| record_email.contains("me.com")
|
||||
|| record_email.contains("outlook")
|
||||
|| record_email.contains("proton")
|
||||
|| record_email.contains("rocketmail")
|
||||
|| record_email.contains("sbcglobal")
|
||||
|| record_email.contains("yahoo")
|
||||
|| record_email.contains("ymail")
|
||||
|| record_email.contains("zoho")
|
||||
{
|
||||
// NB: Just to super extra careful, in case we change the synthetic email format.
|
||||
// This obviously won't catch everything.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let update_email = args.email_address.to_lowercase();
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET
|
||||
email_address = ?,
|
||||
email_gravatar_hash = ?,
|
||||
email_confirmed = false,
|
||||
email_confirmed_by_google = false,
|
||||
email_is_synthetic = false,
|
||||
version = version + 1
|
||||
WHERE
|
||||
token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
update_email,
|
||||
args.email_gravatar_hash,
|
||||
args.user_token.as_str(),
|
||||
);
|
||||
|
||||
args.mysql_connection.execute(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct UserEmailCheckRecordRaw {
|
||||
email_address: String,
|
||||
email_is_synthetic: i8,
|
||||
}
|
||||
|
||||
struct UserEmailCheckRecord {
|
||||
email_address: String,
|
||||
email_is_synthetic: bool,
|
||||
}
|
||||
|
||||
impl From<UserEmailCheckRecordRaw> for UserEmailCheckRecord {
|
||||
fn from(raw: UserEmailCheckRecordRaw) -> Self {
|
||||
Self {
|
||||
email_address: raw.email_address,
|
||||
email_is_synthetic: i8_to_bool(raw.email_is_synthetic),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user