diff --git a/crates/api_clients/fal_client/src/webhook_api/parse_webhook_inner_payload.rs b/crates/api_clients/fal_client/src/webhook_api/hydrate_webhook_contents.rs similarity index 64% rename from crates/api_clients/fal_client/src/webhook_api/parse_webhook_inner_payload.rs rename to crates/api_clients/fal_client/src/webhook_api/hydrate_webhook_contents.rs index 85554dab35..1bfc5992bd 100644 --- a/crates/api_clients/fal_client/src/webhook_api/parse_webhook_inner_payload.rs +++ b/crates/api_clients/fal_client/src/webhook_api/hydrate_webhook_contents.rs @@ -1,36 +1,44 @@ use serde_json::Value; -use crate::webhook_api::payload::webhook_error_type::WebhookErrorType; -use crate::webhook_api::payload::webhook_inner_payload::{ErrorData, PayloadErrorData, SuccessData, WebhookInnerPayload}; -use crate::webhook_api::payload::webhook_payload::{WebhookPayload, WebhookStatus}; +use crate::webhook_api::raw::webhook_error_type::WebhookErrorType; +use crate::webhook_api::hydrated::hydrated_webhook_contents::{ErrorData, PayloadErrorData, WebhookSuccessData, HydratedWebhookContents}; +use crate::webhook_api::raw::raw_webhook_payload::{RawWebhookPayload, RawWebhookStatus}; +use crate::webhook_api::hydrated::success_case_extractors::extract_contents_from_payload; /// Parse the inner payload of a FAL webhook into one of three cases. /// /// 1. If status is ERROR, parse out the first `detail` entry's `msg` and `type`. /// 2. If status is OK but there's no payload and there is a `payload_error`, return PayloadError. /// 3. Otherwise, return Success with the payload. -pub fn parse_webhook_inner_payload(webhook: &WebhookPayload) -> WebhookInnerPayload { +pub fn hydrate_webhook_contents(webhook: &RawWebhookPayload) -> HydratedWebhookContents { match webhook.status { - WebhookStatus::Error => { + RawWebhookStatus::Error => { let (message, error_type) = extract_error_first_detail(&webhook.payload); - WebhookInnerPayload::Error(ErrorData { + HydratedWebhookContents::Error(ErrorData { message, error_type, }) } - WebhookStatus::Ok => { - // Check for payload_error case: status=OK but no payload, has payload_error. + RawWebhookStatus::Ok => { + // Check for `payload_error` case: status=OK but no payload, has payload_error. if webhook.payload.is_none() { if let Some(ref payload_error) = webhook.payload_error { - return WebhookInnerPayload::PayloadError(PayloadErrorData { + return HydratedWebhookContents::PayloadError(PayloadErrorData { payload_error: payload_error.clone(), }); } } - WebhookInnerPayload::Success(SuccessData { - payload: webhook.payload.clone().unwrap_or(Value::Null), + let value = webhook.payload + .clone() + .unwrap_or(Value::Null); + + let extracted_contents = extract_contents_from_payload(&value); + + HydratedWebhookContents::Success(WebhookSuccessData { + payload: value, + extracted_contents, }) } } @@ -67,7 +75,7 @@ fn extract_error_first_detail(payload: &Option) -> (Option, Optio mod tests { use super::*; - fn load_test_webhook(filename: &str) -> WebhookPayload { + fn load_test_webhook(filename: &str) -> RawWebhookPayload { let path = format!("test_data/webhooks/{}", filename); let json = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)); @@ -78,10 +86,10 @@ mod tests { #[test] fn gpt_image_1p5_content_policy_violation() { let webhook = load_test_webhook("gpt_image_1p5_fail_content_policy.json"); - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::Error(data) => { + HydratedWebhookContents::Error(data) => { assert_eq!( data.error_type, Some(WebhookErrorType::ContentPolicyViolation), @@ -91,17 +99,17 @@ mod tests { Some("The content could not be processed because it contained material flagged by a content checker."), ); } - other => panic!("Expected WebhookInnerPayload::Error, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::Error, got {:?}", other), } } #[test] fn gpt_image_invalid_api_key() { let webhook = load_test_webhook("gpt_image_fail_invalid_api_key.json"); - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::Error(data) => { + HydratedWebhookContents::Error(data) => { assert_eq!( data.error_type, Some(WebhookErrorType::InvalidApiKey), @@ -111,17 +119,17 @@ mod tests { Some("Invalid API key"), ); } - other => panic!("Expected WebhookInnerPayload::Error, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::Error, got {:?}", other), } } #[test] fn kling_1p6_pro_file_too_large() { let webhook = load_test_webhook("kling_1p6_pro_file_too_large_error.json"); - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::Error(data) => { + HydratedWebhookContents::Error(data) => { assert_eq!( data.error_type, Some(WebhookErrorType::FileTooLarge), @@ -131,17 +139,17 @@ mod tests { Some("File size exceeds the maximum allowed size of 10485760 bytes. Please upload a smaller file."), ); } - other => panic!("Expected WebhookInnerPayload::Error, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::Error, got {:?}", other), } } #[test] fn kling_3p0_pro_content_policy_violation() { let webhook = load_test_webhook("kling_3p0_pro_fail_content_policy.json"); - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::Error(data) => { + HydratedWebhookContents::Error(data) => { assert_eq!( data.error_type, Some(WebhookErrorType::ContentPolicyViolation), @@ -151,17 +159,17 @@ mod tests { Some("The content could not be processed because it contained material flagged by a content checker."), ); } - other => panic!("Expected WebhookInnerPayload::Error, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::Error, got {:?}", other), } } #[test] fn nano_banana_pro_no_media_generated() { let webhook = load_test_webhook("nano_banana_pro_fail_no_media_generated.json"); - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::Error(data) => { + HydratedWebhookContents::Error(data) => { assert_eq!( data.error_type, Some(WebhookErrorType::NoMediaGenerated), @@ -170,71 +178,71 @@ mod tests { data.message.as_deref().unwrap().starts_with("The model did not generate"), ); } - other => panic!("Expected WebhookInnerPayload::Error, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::Error, got {:?}", other), } } #[test] fn payload_error_case() { - let webhook = WebhookPayload { + let webhook = RawWebhookPayload { request_id: "test-123".to_string(), gateway_request_id: "test-123".to_string(), - status: WebhookStatus::Ok, + status: RawWebhookStatus::Ok, error: None, payload: None, payload_error: Some("encoding error occurred".to_string()), }; - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::PayloadError(data) => { + HydratedWebhookContents::PayloadError(data) => { assert_eq!(data.payload_error, "encoding error occurred"); } - other => panic!("Expected WebhookInnerPayload::PayloadError, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::PayloadError, got {:?}", other), } } #[test] fn success_case() { - let webhook = WebhookPayload { + let webhook = RawWebhookPayload { request_id: "test-456".to_string(), gateway_request_id: "test-456".to_string(), - status: WebhookStatus::Ok, + status: RawWebhookStatus::Ok, error: None, payload: Some(serde_json::json!({"images": [{"url": "https://example.com/img.png"}]})), payload_error: None, }; - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::Success(data) => { + HydratedWebhookContents::Success(data) => { assert!(data.payload.get("images").is_some()); } - other => panic!("Expected WebhookInnerPayload::Success, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::Success, got {:?}", other), } } #[test] fn error_with_no_payload() { - let webhook = WebhookPayload { + let webhook = RawWebhookPayload { request_id: "test-789".to_string(), gateway_request_id: "test-789".to_string(), - status: WebhookStatus::Error, + status: RawWebhookStatus::Error, error: Some("Internal server error".to_string()), payload: None, payload_error: None, }; - let result = parse_webhook_inner_payload(&webhook); + let result = hydrate_webhook_contents(&webhook); match result { - WebhookInnerPayload::Error(data) => { + HydratedWebhookContents::Error(data) => { assert_eq!(data.message, None); assert_eq!(data.error_type, None); } - other => panic!("Expected WebhookInnerPayload::Error, got {:?}", other), + other => panic!("Expected HydratedWebhookContents::Error, got {:?}", other), } } } diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/hydrated_webhook_contents.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/hydrated_webhook_contents.rs new file mode 100644 index 0000000000..840bc4e2df --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/hydrated_webhook_contents.rs @@ -0,0 +1,127 @@ +use serde::Deserialize; +use serde_json::Value; + +use crate::webhook_api::raw::webhook_error_type::WebhookErrorType; + +/// The parsed inner payload of a FAL webhook. +#[derive(Debug)] +pub enum HydratedWebhookContents { + /// The webhook reported success and has a payload. + Success(WebhookSuccessData), + + /// The webhook reported an error (status=ERROR) with optional detail info. + Error(ErrorData), + + /// The webhook reported as "success" but (1) had no payload and (2) had a payload_error. + /// In rare instances of an "OK" response, there may be an error on Fal's end with encoding + /// the payload. If that happens, the "payload_error" field may be set, and this enum variant + /// represents that failure case. + PayloadError(PayloadErrorData), +} + +#[derive(Debug)] +pub struct WebhookSuccessData { + /// The success data is polymorphic, so we're returning a JSON `Value` for now. + /// This will allow for downstream handlers to parse the payload as needed. + /// This is the entire raw success payload. + pub payload: Value, + + /// If there are any extracted sub-payload contents, such as "images" or "video", + /// then they are included here. This may not be fully inclusive of future + /// payload types. + pub extracted_contents: Option, +} + +#[derive(Debug)] +pub struct ExtractedContents { + /// Parsed from `payload.image` (single image result). + pub image: Option, + + /// Parsed from `payload.images` (batch image results). + pub images: Option>, + + /// Parsed from `payload.video`. + pub video: Option, + + /// Parsed from `payload.model_glb`. + pub model_glb: Option, + + /// Parsed from `payload.model_mesh`. + pub model_mesh: Option, + + /// Parsed from `payload.thumbnail`. + pub thumbnail: Option, +} + +/// Data under `payload.image`: +#[derive(Debug, Deserialize)] +pub struct ImageData { + pub url: Option, + pub content_type: Option, + pub file_name: Option, + pub file_size: Option, + pub height: Option, + pub width: Option, +} + +/// Data under `payload.images` (a list of these): +#[derive(Debug, Deserialize)] +pub struct ImagesData { + pub url: Option, + pub content_type: Option, + pub file_name: Option, + pub file_size: Option, + pub height: Option, + pub width: Option, +} + +/// Data under `payload.video`: +#[derive(Debug, Deserialize)] +pub struct VideoData { + pub url: Option, + pub content_type: Option, + pub file_name: Option, + pub file_size: Option, +} + +/// Data under `payload.model_glb` (there may be other sibling keys too) +#[derive(Debug, Deserialize)] +pub struct ModelGlbData { + pub content_type: Option, + pub file_name: Option, + pub file_size: Option, + pub url: Option, +} + +/// Data under `payload.model_mesh` (there may be other sibling keys too) +#[derive(Debug, Deserialize)] +pub struct ModelMeshData { + pub content_type: Option, + pub file_name: Option, + pub file_size: Option, + pub url: Option, +} + +/// Data under `payload.thumbnail` (there may be other sibling keys too) +/// Frequently seen together with `model_glb`. +#[derive(Debug, Deserialize)] +pub struct ThumbnailData { + pub content_type: Option, + pub file_name: Option, + pub file_size: Option, + pub url: Option, +} + +#[derive(Debug)] +pub struct ErrorData { + /// The first human-readable message from `payload.detail[].msg`, if any. + pub message: Option, + + /// The first machine-readable error type from `payload.detail[].type`, if any. + pub error_type: Option, +} + +#[derive(Debug)] +pub struct PayloadErrorData { + pub payload_error: String, +} diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/mod.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/mod.rs new file mode 100644 index 0000000000..0a599d4a1e --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/mod.rs @@ -0,0 +1,2 @@ +pub mod hydrated_webhook_contents; +pub mod success_case_extractors; diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_image_payload.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_image_payload.rs new file mode 100644 index 0000000000..2c3486da77 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_image_payload.rs @@ -0,0 +1,100 @@ +use serde_json::{Map, Value}; + +use crate::webhook_api::hydrated::hydrated_webhook_contents::ImageData; + +/// Extract and deserialize the `image` key from a webhook success payload. +pub (crate) fn extract_image(obj: &Map) -> Option { + let value = obj.get("image")?; + serde_json::from_value(value.clone()).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::webhook_api::hydrate_webhook_contents::hydrate_webhook_contents; + use crate::webhook_api::hydrated::hydrated_webhook_contents::HydratedWebhookContents; + use crate::webhook_api::raw::raw_webhook_payload::{RawWebhookPayload, RawWebhookStatus}; + + #[test] + fn payload_without_known_keys_has_none_extracted_contents() { + let webhook = RawWebhookPayload { + request_id: "test-no-keys".to_string(), + gateway_request_id: "test-no-keys".to_string(), + status: RawWebhookStatus::Ok, + error: None, + payload: Some(serde_json::json!({"some_other_key": "value"})), + payload_error: None, + }; + + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + assert!(data.extracted_contents.is_none()); + } + + #[test] + fn null_payload_has_none_extracted_contents() { + let webhook = RawWebhookPayload { + request_id: "test-null".to_string(), + gateway_request_id: "test-null".to_string(), + status: RawWebhookStatus::Ok, + error: None, + payload: None, + payload_error: None, + }; + + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + assert!(data.extracted_contents.is_none()); + } + + #[test] + fn synthetic_image_payload() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "image": { + "url": "https://cdn.example.com/single.png", + "content_type": "image/png", + "file_name": "single.png", + "file_size": 98765, + "width": 512, + "height": 512 + } + }"#).unwrap(); + + let image = extract_image(&obj).expect("should extract image"); + assert_eq!(image.url.as_deref(), Some("https://cdn.example.com/single.png")); + assert_eq!(image.content_type.as_deref(), Some("image/png")); + assert_eq!(image.file_name.as_deref(), Some("single.png")); + assert_eq!(image.file_size, Some(98765)); + assert_eq!(image.width, Some(512)); + assert_eq!(image.height, Some(512)); + } + + #[test] + fn synthetic_image_url_only() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "image": {"url": "https://cdn.example.com/img.jpg"} + }"#).unwrap(); + + let image = extract_image(&obj).expect("should extract image"); + assert_eq!(image.url.as_deref(), Some("https://cdn.example.com/img.jpg")); + assert!(image.content_type.is_none()); + assert!(image.file_size.is_none()); + } + + #[test] + fn missing_image_key_returns_none() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "images": [{"url": "https://example.com/img.png"}] + }"#).unwrap(); + + assert!(extract_image(&obj).is_none()); + } +} diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_images_payload.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_images_payload.rs new file mode 100644 index 0000000000..7293c99259 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_images_payload.rs @@ -0,0 +1,96 @@ +use serde_json::{Map, Value}; + +use crate::webhook_api::hydrated::hydrated_webhook_contents::ImagesData; + +/// Extract and deserialize the `images` key from a webhook success payload. +pub (crate) fn extract_images(obj: &Map) -> Option> { + let value = obj.get("images")?; + serde_json::from_value(value.clone()).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::webhook_api::hydrate_webhook_contents::hydrate_webhook_contents; + use crate::webhook_api::hydrated::hydrated_webhook_contents::HydratedWebhookContents; + use crate::webhook_api::raw::raw_webhook_payload::RawWebhookPayload; + + fn load_test_webhook(filename: &str) -> RawWebhookPayload { + let path = format!("test_data/webhooks/{}", filename); + let json = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)); + serde_json::from_str(&json) + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e)) + } + + #[test] + fn images_payload_from_test_file() { + let webhook = load_test_webhook("success/images_payload_1.json"); + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + let contents = data.extracted_contents + .expect("extracted_contents should be Some"); + + let images = contents.images.expect("images should be Some"); + assert_eq!(images.len(), 2); + + assert_eq!(images[0].url.as_deref(), Some("https://v3b.fal.media/files/b/01234567/name1.png")); + assert_eq!(images[0].content_type.as_deref(), Some("image/png")); + assert_eq!(images[0].file_name.as_deref(), Some("name1.png")); + assert_eq!(images[0].file_size, Some(11500002)); + + assert_eq!(images[1].url.as_deref(), Some("https://v3b.fal.media/files/b/01234567/name2.png")); + assert_eq!(images[1].file_size, Some(13499999)); + + assert!(contents.image.is_none()); + assert!(contents.video.is_none()); + assert!(contents.model_glb.is_none()); + assert!(contents.model_mesh.is_none()); + } + + #[test] + fn synthetic_images_payload() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "images": [ + {"url": "https://example.com/a.png", "content_type": "image/png", "width": 1024, "height": 768}, + {"url": "https://example.com/b.jpg", "content_type": "image/jpeg", "file_size": 54321} + ] + }"#).unwrap(); + + let images = extract_images(&obj).expect("should extract images"); + assert_eq!(images.len(), 2); + + assert_eq!(images[0].url.as_deref(), Some("https://example.com/a.png")); + assert_eq!(images[0].width, Some(1024)); + assert_eq!(images[0].height, Some(768)); + assert!(images[0].file_size.is_none()); + + assert_eq!(images[1].url.as_deref(), Some("https://example.com/b.jpg")); + assert_eq!(images[1].content_type.as_deref(), Some("image/jpeg")); + assert_eq!(images[1].file_size, Some(54321)); + assert!(images[1].width.is_none()); + } + + #[test] + fn empty_images_array_returns_empty_vec() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "images": [] + }"#).unwrap(); + + let images = extract_images(&obj).expect("should extract empty images"); + assert!(images.is_empty()); + } + + #[test] + fn missing_images_key_returns_none() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "video": {"url": "https://example.com/v.mp4"} + }"#).unwrap(); + + assert!(extract_images(&obj).is_none()); + } +} diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_model_glb_payload.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_model_glb_payload.rs new file mode 100644 index 0000000000..24dffc9ea1 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_model_glb_payload.rs @@ -0,0 +1,91 @@ +use serde_json::{Map, Value}; + +use crate::webhook_api::hydrated::hydrated_webhook_contents::ModelGlbData; + +/// Extract and deserialize the `model_glb` key from a webhook success payload. +pub (crate) fn extract_model_glb(obj: &Map) -> Option { + let value = obj.get("model_glb")?; + serde_json::from_value(value.clone()).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::webhook_api::hydrate_webhook_contents::hydrate_webhook_contents; + use crate::webhook_api::hydrated::hydrated_webhook_contents::HydratedWebhookContents; + use crate::webhook_api::raw::raw_webhook_payload::RawWebhookPayload; + + fn load_test_webhook(filename: &str) -> RawWebhookPayload { + let path = format!("test_data/webhooks/{}", filename); + let json = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)); + serde_json::from_str(&json) + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e)) + } + + #[test] + fn model_glb_from_test_file() { + let webhook = load_test_webhook("success/model_glb_urls_and_thumbnail_1.json"); + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + let contents = data.extracted_contents + .expect("extracted_contents should be Some"); + + let glb = contents.model_glb.expect("model_glb should be Some"); + assert_eq!(glb.url.as_deref(), Some("https://v3b.fal.media/files/b/0a95eb37/f7UaAhyfcx0BAnYy0GpFM_model.glb")); + assert_eq!(glb.content_type.as_deref(), Some("model/gltf-binary")); + assert_eq!(glb.file_name.as_deref(), Some("model.glb")); + assert_eq!(glb.file_size, Some(66284788)); + + // This payload also has a thumbnail. + assert!(contents.thumbnail.is_some()); + + // No image/images/video/mesh in this payload. + assert!(contents.image.is_none()); + assert!(contents.images.is_none()); + assert!(contents.video.is_none()); + assert!(contents.model_mesh.is_none()); + } + + #[test] + fn synthetic_model_glb_payload() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "model_glb": { + "url": "https://cdn.example.com/model.glb", + "content_type": "model/gltf-binary", + "file_name": "output.glb", + "file_size": 5432100 + } + }"#).unwrap(); + + let glb = extract_model_glb(&obj).expect("should extract model_glb"); + assert_eq!(glb.url.as_deref(), Some("https://cdn.example.com/model.glb")); + assert_eq!(glb.content_type.as_deref(), Some("model/gltf-binary")); + assert_eq!(glb.file_name.as_deref(), Some("output.glb")); + assert_eq!(glb.file_size, Some(5432100)); + } + + #[test] + fn model_glb_url_only() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "model_glb": {"url": "https://cdn.example.com/m.glb"} + }"#).unwrap(); + + let glb = extract_model_glb(&obj).expect("should extract model_glb"); + assert_eq!(glb.url.as_deref(), Some("https://cdn.example.com/m.glb")); + assert!(glb.content_type.is_none()); + } + + #[test] + fn missing_model_glb_key_returns_none() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "video": {"url": "https://example.com/v.mp4"} + }"#).unwrap(); + + assert!(extract_model_glb(&obj).is_none()); + } +} diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_model_mesh_payload.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_model_mesh_payload.rs new file mode 100644 index 0000000000..e30bf1d031 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_model_mesh_payload.rs @@ -0,0 +1,89 @@ +use serde_json::{Map, Value}; + +use crate::webhook_api::hydrated::hydrated_webhook_contents::ModelMeshData; + +/// Extract and deserialize the `model_mesh` key from a webhook success payload. +pub (crate) fn extract_model_mesh(obj: &Map) -> Option { + let value = obj.get("model_mesh")?; + serde_json::from_value(value.clone()).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::webhook_api::hydrate_webhook_contents::hydrate_webhook_contents; + use crate::webhook_api::hydrated::hydrated_webhook_contents::HydratedWebhookContents; + use crate::webhook_api::raw::raw_webhook_payload::RawWebhookPayload; + + fn load_test_webhook(filename: &str) -> RawWebhookPayload { + let path = format!("test_data/webhooks/{}", filename); + let json = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)); + serde_json::from_str(&json) + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e)) + } + + #[test] + fn model_mesh_from_test_file() { + let webhook = load_test_webhook("success/model_mesh_payload_1.json"); + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + let contents = data.extracted_contents + .expect("extracted_contents should be Some"); + + let mesh = contents.model_mesh.expect("model_mesh should be Some"); + assert_eq!(mesh.url.as_deref(), Some("https://v3b.fal.media/files/b/0a95eb2d/XCpH7fk-tZVXr1XMUr7DY_mesh-1775972273-765284.glb")); + assert_eq!(mesh.content_type.as_deref(), Some("application/octet-stream")); + assert_eq!(mesh.file_name.as_deref(), Some("mesh-1775972273-765284.glb")); + assert_eq!(mesh.file_size, Some(3886928)); + + // No other content types in this payload. + assert!(contents.image.is_none()); + assert!(contents.images.is_none()); + assert!(contents.video.is_none()); + assert!(contents.model_glb.is_none()); + assert!(contents.thumbnail.is_none()); + } + + #[test] + fn synthetic_model_mesh_payload() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "model_mesh": { + "url": "https://cdn.example.com/mesh.obj", + "content_type": "model/obj", + "file_name": "output.obj", + "file_size": 1234567 + } + }"#).unwrap(); + + let mesh = extract_model_mesh(&obj).expect("should extract model_mesh"); + assert_eq!(mesh.url.as_deref(), Some("https://cdn.example.com/mesh.obj")); + assert_eq!(mesh.content_type.as_deref(), Some("model/obj")); + assert_eq!(mesh.file_name.as_deref(), Some("output.obj")); + assert_eq!(mesh.file_size, Some(1234567)); + } + + #[test] + fn model_mesh_url_only() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "model_mesh": {"url": "https://cdn.example.com/m.obj"} + }"#).unwrap(); + + let mesh = extract_model_mesh(&obj).expect("should extract model_mesh"); + assert_eq!(mesh.url.as_deref(), Some("https://cdn.example.com/m.obj")); + assert!(mesh.content_type.is_none()); + } + + #[test] + fn missing_model_mesh_key_returns_none() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "image": {"url": "https://example.com/img.png"} + }"#).unwrap(); + + assert!(extract_model_mesh(&obj).is_none()); + } +} diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_thumbnail_payload.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_thumbnail_payload.rs new file mode 100644 index 0000000000..c3241732be --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_thumbnail_payload.rs @@ -0,0 +1,86 @@ +use serde_json::{Map, Value}; + +use crate::webhook_api::hydrated::hydrated_webhook_contents::ThumbnailData; + +/// Extract and deserialize the `thumbnail` key from a webhook success payload. +pub(crate) fn extract_thumbnail(obj: &Map) -> Option { + let value = obj.get("thumbnail")?; + serde_json::from_value(value.clone()).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::webhook_api::hydrate_webhook_contents::hydrate_webhook_contents; + use crate::webhook_api::hydrated::hydrated_webhook_contents::HydratedWebhookContents; + use crate::webhook_api::raw::raw_webhook_payload::RawWebhookPayload; + + fn load_test_webhook(filename: &str) -> RawWebhookPayload { + let path = format!("test_data/webhooks/{}", filename); + let json = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)); + serde_json::from_str(&json) + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e)) + } + + #[test] + fn thumbnail_from_model_glb_test_file() { + let webhook = load_test_webhook("success/model_glb_urls_and_thumbnail_1.json"); + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + let contents = data.extracted_contents + .expect("extracted_contents should be Some"); + + let thumbnail = contents.thumbnail.expect("thumbnail should be Some"); + assert_eq!(thumbnail.url.as_deref(), Some("https://v3b.fal.media/files/b/0a95eb38/yvwGJWZV9TkWZuZvjxZNj_preview.png")); + assert_eq!(thumbnail.content_type.as_deref(), Some("image/png")); + assert_eq!(thumbnail.file_name.as_deref(), Some("preview.png")); + assert_eq!(thumbnail.file_size, Some(93436)); + } + + #[test] + fn synthetic_thumbnail_payload() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "thumbnail": { + "url": "https://cdn.example.com/thumb.png", + "content_type": "image/png", + "file_name": "thumb.png", + "file_size": 12345 + } + }"#).unwrap(); + + let thumb = extract_thumbnail(&obj).expect("should extract thumbnail"); + assert_eq!(thumb.url.as_deref(), Some("https://cdn.example.com/thumb.png")); + assert_eq!(thumb.content_type.as_deref(), Some("image/png")); + assert_eq!(thumb.file_name.as_deref(), Some("thumb.png")); + assert_eq!(thumb.file_size, Some(12345)); + } + + #[test] + fn missing_thumbnail_key_returns_none() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "model_glb": {"url": "https://example.com/model.glb"} + }"#).unwrap(); + + assert!(extract_thumbnail(&obj).is_none()); + } + + #[test] + fn model_mesh_payload_has_no_thumbnail() { + let webhook = load_test_webhook("success/model_mesh_payload_1.json"); + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + let contents = data.extracted_contents + .expect("extracted_contents should be Some"); + + assert!(contents.thumbnail.is_none()); + } +} diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_video_payload.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_video_payload.rs new file mode 100644 index 0000000000..8ebee81269 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/extract_video_payload.rs @@ -0,0 +1,90 @@ +use serde_json::{Map, Value}; + +use crate::webhook_api::hydrated::hydrated_webhook_contents::VideoData; + +/// Extract and deserialize the `video` key from a webhook success payload. +pub (crate) fn extract_video(obj: &Map) -> Option { + let value = obj.get("video")?; + serde_json::from_value(value.clone()).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::webhook_api::hydrate_webhook_contents::hydrate_webhook_contents; + use crate::webhook_api::hydrated::hydrated_webhook_contents::HydratedWebhookContents; + use crate::webhook_api::raw::raw_webhook_payload::RawWebhookPayload; + + fn load_test_webhook(filename: &str) -> RawWebhookPayload { + let path = format!("test_data/webhooks/{}", filename); + let json = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)); + serde_json::from_str(&json) + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e)) + } + + #[test] + fn video_payload_from_test_file() { + let webhook = load_test_webhook("success/video_payload_1.json"); + let result = hydrate_webhook_contents(&webhook); + + let HydratedWebhookContents::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + let contents = data.extracted_contents + .expect("extracted_contents should be Some"); + + let video = contents.video.expect("video should be Some"); + + assert_eq!(video.url.as_deref(), Some("https://v3b.fal.media/files/b/0abcdef0/AB-CDE_123456789abcde_output.mp4")); + assert_eq!(video.content_type.as_deref(), Some("video/mp4")); + assert_eq!(video.file_name.as_deref(), Some("output.mp4")); + assert_eq!(video.file_size, Some(6226845)); + + assert!(contents.image.is_none()); + assert!(contents.images.is_none()); + assert!(contents.model_glb.is_none()); + assert!(contents.model_mesh.is_none()); + } + + #[test] + fn synthetic_video_payload() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "video": { + "url": "https://cdn.example.com/output.mp4", + "content_type": "video/mp4", + "file_name": "clip.mp4", + "file_size": 12345678 + } + }"#).unwrap(); + + let video = extract_video(&obj).expect("should extract video"); + assert_eq!(video.url.as_deref(), Some("https://cdn.example.com/output.mp4")); + assert_eq!(video.content_type.as_deref(), Some("video/mp4")); + assert_eq!(video.file_name.as_deref(), Some("clip.mp4")); + assert_eq!(video.file_size, Some(12345678)); + } + + #[test] + fn synthetic_video_url_only() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "video": {"url": "https://cdn.example.com/v.mp4"} + }"#).unwrap(); + + let video = extract_video(&obj).expect("should extract video"); + assert_eq!(video.url.as_deref(), Some("https://cdn.example.com/v.mp4")); + assert!(video.content_type.is_none()); + assert!(video.file_name.is_none()); + assert!(video.file_size.is_none()); + } + + #[test] + fn missing_video_key_returns_none() { + let obj: serde_json::Map = serde_json::from_str(r#"{ + "images": [{"url": "https://example.com/img.png"}] + }"#).unwrap(); + + assert!(extract_video(&obj).is_none()); + } +} diff --git a/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/mod.rs b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/mod.rs new file mode 100644 index 0000000000..cd40e6e06d --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/hydrated/success_case_extractors/mod.rs @@ -0,0 +1,43 @@ +pub mod extract_image_payload; +pub mod extract_images_payload; +pub mod extract_video_payload; +pub mod extract_model_glb_payload; +pub mod extract_model_mesh_payload; +pub mod extract_thumbnail_payload; + +use serde_json::Value; + +use crate::webhook_api::hydrated::hydrated_webhook_contents::ExtractedContents; + +/// Try to extract known content keys from a success payload. +/// +/// Checks the payload (as a JSON object) for any of: `image`, `images`, +/// `video`, `model_glb`, `model_mesh`. If at least one is found, returns +/// `Some(ExtractedContents)` with copies of the relevant values. Multiple +/// keys can be populated simultaneously. Returns `None` if the payload is +/// not an object or none of the known keys are present. +pub fn extract_contents_from_payload(payload: &Value) -> Option { + let obj = payload.as_object()?; + + let image = extract_image_payload::extract_image(obj); + let images = extract_images_payload::extract_images(obj); + let video = extract_video_payload::extract_video(obj); + let model_glb = extract_model_glb_payload::extract_model_glb(obj); + let model_mesh = extract_model_mesh_payload::extract_model_mesh(obj); + let thumbnail = extract_thumbnail_payload::extract_thumbnail(obj); + + if image.is_none() && images.is_none() && video.is_none() + && model_glb.is_none() && model_mesh.is_none() && thumbnail.is_none() + { + return None; + } + + Some(ExtractedContents { + image, + images, + video, + model_glb, + model_mesh, + thumbnail, + }) +} diff --git a/crates/api_clients/fal_client/src/webhook_api/mod.rs b/crates/api_clients/fal_client/src/webhook_api/mod.rs index 9bb83b48ba..eaaaac6df5 100644 --- a/crates/api_clients/fal_client/src/webhook_api/mod.rs +++ b/crates/api_clients/fal_client/src/webhook_api/mod.rs @@ -1,3 +1,4 @@ -pub mod parse_webhook_inner_payload; -pub mod parse_webhook_payload; -pub mod payload; \ No newline at end of file +pub mod raw; +pub mod hydrated; +pub mod hydrate_webhook_contents; +pub mod parse_raw_webhook_payload; diff --git a/crates/api_clients/fal_client/src/webhook_api/parse_raw_webhook_payload.rs b/crates/api_clients/fal_client/src/webhook_api/parse_raw_webhook_payload.rs new file mode 100644 index 0000000000..6172bc82e5 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/parse_raw_webhook_payload.rs @@ -0,0 +1,5 @@ +use crate::webhook_api::raw::raw_webhook_payload::RawWebhookPayload; + +pub fn parse_raw_webhook_payload(json: &str) -> Result { + serde_json::from_str(json) +} diff --git a/crates/api_clients/fal_client/src/webhook_api/parse_webhook_payload.rs b/crates/api_clients/fal_client/src/webhook_api/parse_webhook_payload.rs deleted file mode 100644 index 38c270fc0c..0000000000 --- a/crates/api_clients/fal_client/src/webhook_api/parse_webhook_payload.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::webhook_api::payload::webhook_payload::WebhookPayload; - -pub fn parse_webhook_payload(json: &str) -> Result { - serde_json::from_str(json) -} diff --git a/crates/api_clients/fal_client/src/webhook_api/payload/mod.rs b/crates/api_clients/fal_client/src/webhook_api/payload/mod.rs deleted file mode 100644 index 93638d13fd..0000000000 --- a/crates/api_clients/fal_client/src/webhook_api/payload/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod webhook_inner_payload; -pub mod webhook_payload; -pub mod webhook_error_type; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/webhook_api/payload/webhook_inner_payload.rs b/crates/api_clients/fal_client/src/webhook_api/payload/webhook_inner_payload.rs deleted file mode 100644 index ccc43f93f4..0000000000 --- a/crates/api_clients/fal_client/src/webhook_api/payload/webhook_inner_payload.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde_json::Value; - -use crate::webhook_api::payload::webhook_error_type::WebhookErrorType; - -/// The parsed inner payload of a FAL webhook. -#[derive(Debug)] -pub enum WebhookInnerPayload { - /// The webhook reported success and has a payload. - Success(SuccessData), - /// The webhook reported an error (status=ERROR) with optional detail info. - Error(ErrorData), - /// The webhook reported success but had no payload and instead had a payload_error. - PayloadError(PayloadErrorData), -} - -#[derive(Debug)] -pub struct SuccessData { - pub payload: Value, -} - -#[derive(Debug)] -pub struct ErrorData { - /// The first human-readable message from `payload.detail[].msg`, if any. - pub message: Option, - /// The first machine-readable error type from `payload.detail[].type`, if any. - pub error_type: Option, -} - -#[derive(Debug)] -pub struct PayloadErrorData { - pub payload_error: String, -} diff --git a/crates/api_clients/fal_client/src/webhook_api/raw/mod.rs b/crates/api_clients/fal_client/src/webhook_api/raw/mod.rs new file mode 100644 index 0000000000..f141a2c3be --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/raw/mod.rs @@ -0,0 +1,2 @@ +pub mod webhook_error_type; +pub mod raw_webhook_payload; diff --git a/crates/api_clients/fal_client/src/webhook_api/payload/webhook_payload.rs b/crates/api_clients/fal_client/src/webhook_api/raw/raw_webhook_payload.rs similarity index 93% rename from crates/api_clients/fal_client/src/webhook_api/payload/webhook_payload.rs rename to crates/api_clients/fal_client/src/webhook_api/raw/raw_webhook_payload.rs index 961003c5d4..fedf703ab0 100644 --- a/crates/api_clients/fal_client/src/webhook_api/payload/webhook_payload.rs +++ b/crates/api_clients/fal_client/src/webhook_api/raw/raw_webhook_payload.rs @@ -3,7 +3,7 @@ use serde_json::Value; /// The value FAL sends to webhook endpoints. #[derive(Deserialize, Debug, Clone)] -pub struct WebhookPayload { +pub struct RawWebhookPayload { /// Value originally used in the queue API pub request_id: String, @@ -13,7 +13,7 @@ pub struct WebhookPayload { pub gateway_request_id: String, /// Status of the request: OK or ERROR. - pub status: WebhookStatus, + pub status: RawWebhookStatus, /// When an error happens, the status will be ERROR. /// The error property will contain a message and payload will contain the error details. @@ -32,7 +32,7 @@ pub struct WebhookPayload { } #[derive(Deserialize, Debug, Clone, Copy)] -pub enum WebhookStatus { +pub enum RawWebhookStatus { /// Success case #[serde(alias = "OK")] Ok, diff --git a/crates/api_clients/fal_client/src/webhook_api/payload/webhook_error_type.rs b/crates/api_clients/fal_client/src/webhook_api/raw/webhook_error_type.rs similarity index 100% rename from crates/api_clients/fal_client/src/webhook_api/payload/webhook_error_type.rs rename to crates/api_clients/fal_client/src/webhook_api/raw/webhook_error_type.rs diff --git a/crates/api_clients/fal_client/test_data/webhooks/success/images_payload_1.json b/crates/api_clients/fal_client/test_data/webhooks/success/images_payload_1.json new file mode 100644 index 0000000000..3d4fdea8f7 --- /dev/null +++ b/crates/api_clients/fal_client/test_data/webhooks/success/images_payload_1.json @@ -0,0 +1,26 @@ +{ + "error": null, + "gateway_request_id": "11111111-2222-3333-4444-555555555555", + "payload": { + "images": [ + { + "content_type": "image/png", + "file_name": "name1.png", + "file_size": 11500002, + "height": null, + "url": "https://v3b.fal.media/files/b/01234567/name1.png", + "width": null + }, + { + "content_type": "image/png", + "file_name": "name2.png", + "file_size": 13499999, + "height": null, + "url": "https://v3b.fal.media/files/b/01234567/name2.png", + "width": null + } + ] + }, + "request_id": "11111111-2222-3333-4444-555555555555", + "status": "OK" +} \ No newline at end of file diff --git a/crates/api_clients/fal_client/test_data/webhooks/success/model_glb_urls_and_thumbnail_1.json b/crates/api_clients/fal_client/test_data/webhooks/success/model_glb_urls_and_thumbnail_1.json new file mode 100644 index 0000000000..f3e0287c27 --- /dev/null +++ b/crates/api_clients/fal_client/test_data/webhooks/success/model_glb_urls_and_thumbnail_1.json @@ -0,0 +1,37 @@ +{ + "error": null, + "gateway_request_id": "019d8031-ede2-7932-a18f-9b2865908c0e", + "payload": { + "model_glb": { + "content_type": "model/gltf-binary", + "file_name": "model.glb", + "file_size": 66284788, + "url": "https://v3b.fal.media/files/b/0a95eb37/f7UaAhyfcx0BAnYy0GpFM_model.glb" + }, + "model_urls": { + "fbx": null, + "glb": { + "content_type": "model/gltf-binary", + "file_name": "model.glb", + "file_size": 66284788, + "url": "https://v3b.fal.media/files/b/0a95eb37/f7UaAhyfcx0BAnYy0GpFM_model.glb" + }, + "obj": { + "content_type": "text/plain", + "file_name": "model.obj", + "file_size": 55806533, + "url": "https://v3b.fal.media/files/b/0a95eb38/vfrPPtdTXNEYh_ExY-adN_model.obj" + }, + "usdz": null + }, + "seed": null, + "thumbnail": { + "content_type": "image/png", + "file_name": "preview.png", + "file_size": 93436, + "url": "https://v3b.fal.media/files/b/0a95eb38/yvwGJWZV9TkWZuZvjxZNj_preview.png" + } + }, + "request_id": "019d8031-ede2-7932-a18f-9b2865908c0e", + "status": "OK" +} \ No newline at end of file diff --git a/crates/api_clients/fal_client/test_data/webhooks/success/model_mesh_payload_1.json b/crates/api_clients/fal_client/test_data/webhooks/success/model_mesh_payload_1.json new file mode 100644 index 0000000000..5cb7f66a82 --- /dev/null +++ b/crates/api_clients/fal_client/test_data/webhooks/success/model_mesh_payload_1.json @@ -0,0 +1,15 @@ +{ + "error": null, + "gateway_request_id": "019d8032-38cb-7161-8dd8-0b4adf10fa72", + "payload": { + "model_mesh": { + "content_type": "application/octet-stream", + "file_name": "mesh-1775972273-765284.glb", + "file_size": 3886928, + "url": "https://v3b.fal.media/files/b/0a95eb2d/XCpH7fk-tZVXr1XMUr7DY_mesh-1775972273-765284.glb" + }, + "seed": 765284 + }, + "request_id": "019d8032-38cb-7161-8dd8-0b4adf10fa72", + "status": "OK" +} \ No newline at end of file diff --git a/crates/api_clients/fal_client/test_data/webhooks/success/video_payload_1.json b/crates/api_clients/fal_client/test_data/webhooks/success/video_payload_1.json new file mode 100644 index 0000000000..413f59db96 --- /dev/null +++ b/crates/api_clients/fal_client/test_data/webhooks/success/video_payload_1.json @@ -0,0 +1,14 @@ +{ + "error": null, + "gateway_request_id": "01234560-1230-1230-1230-122233344455", + "payload": { + "video": { + "content_type": "video/mp4", + "file_name": "output.mp4", + "file_size": 6226845, + "url": "https://v3b.fal.media/files/b/0abcdef0/AB-CDE_123456789abcde_output.mp4" + } + }, + "request_id": "01234560-1230-1230-1230-122233344455", + "status": "OK" +} \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/billing_fakeyou/list_active_user_subscriptions_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/billing_fakeyou/list_active_user_subscriptions_handler.rs index 7cf3fcdac9..1fa071fb15 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/billing_fakeyou/list_active_user_subscriptions_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/billing_fakeyou/list_active_user_subscriptions_handler.rs @@ -44,7 +44,7 @@ pub async fn list_active_user_subscriptions_handler( .await .map_err(|err| { warn!("Error looking up user: {:?}", err); - AdvancedCommonWebError::from_error + AdvancedCommonWebError::from_error(err) })?; let user_metadata = match maybe_user_metadata { diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs index 61a8b43a4f..e306b263e4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs @@ -7,9 +7,9 @@ use crate::state::server_state::ServerState; use actix_web::web::Bytes; use actix_web::web::Json; use actix_web::{web, HttpRequest}; -use fal_client::webhook_api::parse_webhook_inner_payload::parse_webhook_inner_payload; -use fal_client::webhook_api::parse_webhook_payload::parse_webhook_payload; -use fal_client::webhook_api::payload::webhook_inner_payload::WebhookInnerPayload; +use fal_client::webhook_api::hydrate_webhook_contents::hydrate_webhook_contents; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::HydratedWebhookContents; +use fal_client::webhook_api::parse_raw_webhook_payload::parse_raw_webhook_payload; use http_server_common::response::response_success_helpers::SimpleGenericJsonSuccess; use log::{error, info, warn}; use pager::notification::notification_details_builder::NotificationDetailsBuilder; @@ -36,8 +36,10 @@ pub async fn fal_webhook_handler( info!("Received FAL webhook body: {}", raw_body); - // Step 2: Parse into WebhookPayload. - let webhook_payload = parse_webhook_payload(&raw_body) + println!("\n\nReceived FAL webhook body: {}\n\n", raw_body); + + // Step 2: Parse into RawWebhookPayload. + let webhook_payload = parse_raw_webhook_payload(&raw_body) .map_err(|err| { error!("FAL webhook: could not parse webhook payload: {:?}", err); enqueue_parse_error_alert(&server_state, &http_request, "JSON parse failed", &err, Some(&raw_body)); @@ -50,15 +52,15 @@ pub async fn fal_webhook_handler( info!("FAL webhook request_id: {} (status: {:?})", request_id, webhook_payload.status); - // Step 3: Parse the inner payload. - let inner_payload = parse_webhook_inner_payload(&webhook_payload); + // Step 3: Hydrate the webhook contents. + let hydrated_contents = hydrate_webhook_contents(&webhook_payload); // Step 4 & 5: Branch on the inner payload type. - let result = match inner_payload { - WebhookInnerPayload::Success(success_data) => { - handle_successful_fal_webhook(&server_state, request_id, &success_data.payload).await + let result = match hydrated_contents { + HydratedWebhookContents::Success(success_data) => { + handle_successful_fal_webhook(&server_state, request_id, &success_data).await } - WebhookInnerPayload::Error(error_data) => { + HydratedWebhookContents::Error(error_data) => { handle_failed_fal_webhook( &server_state, request_id, @@ -66,7 +68,7 @@ pub async fn fal_webhook_handler( webhook_payload.error.as_deref(), ).await } - WebhookInnerPayload::PayloadError(payload_error_data) => { + HydratedWebhookContents::PayloadError(payload_error_data) => { warn!( "FAL webhook payload_error for request_id {}: {}", request_id, payload_error_data.payload_error, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_failure/handle_failed_fal_webhook.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_failure/handle_failed_fal_webhook.rs index bcbaaffafb..3fbde6cf79 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_failure/handle_failed_fal_webhook.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_failure/handle_failed_fal_webhook.rs @@ -2,8 +2,8 @@ use crate::http_server::common_responses::advanced_common_web_error::AdvancedCom use crate::state::server_state::ServerState; use actix_web::web::Json; use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory; -use fal_client::webhook_api::payload::webhook_error_type::WebhookErrorType; -use fal_client::webhook_api::payload::webhook_inner_payload::ErrorData; +use fal_client::webhook_api::raw::webhook_error_type::WebhookErrorType; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ErrorData; use http_server_common::response::response_success_helpers::SimpleGenericJsonSuccess; use log::{error, info, warn}; use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/handle_successful_fal_webhook.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/handle_successful_fal_webhook.rs index 1f65511b49..9885797b66 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/handle_successful_fal_webhook.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/handle_successful_fal_webhook.rs @@ -1,9 +1,9 @@ use actix_web::web::Json; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::WebhookSuccessData; use http_server_common::response::response_success_helpers::SimpleGenericJsonSuccess; use log::{info, warn}; use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id; use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::{mark_fal_generic_inference_job_successfully_done, MarkJobArgs}; -use serde_json::Value; use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; use crate::state::server_state::ServerState; @@ -17,7 +17,7 @@ use super::process_video_payload::process_video_payload; pub async fn handle_successful_fal_webhook( server_state: &ServerState, request_id: &str, - payload: &Value, + success_data: &WebhookSuccessData, ) -> Result, AdvancedCommonWebError> { let db_result = get_inference_job_by_fal_id( @@ -42,25 +42,46 @@ pub async fn handle_successful_fal_webhook( let mut maybe_media_token = None; let mut maybe_batch_token = None; - if let Some(payload_obj) = payload.as_object() { - if payload_obj.contains_key("image") { + if let Some(ref extracted) = success_data.extracted_contents { + if let Some(ref image_data) = extracted.image { info!("Handling image payload for request_id {} / job {:?}", request_id, job.job_token); - let token = process_image_payload(payload_obj, &job, server_state).await?; - maybe_media_token = Some(token); - } else if payload_obj.contains_key("images") { - (maybe_media_token, maybe_batch_token) = process_images_payload(payload_obj, &job, server_state).await?; - } else if payload_obj.contains_key("video") { + let token = process_image_payload(image_data, &job, server_state).await?; + if maybe_media_token.is_none() { + maybe_media_token = Some(token); + } + } + + if let Some(ref images_data) = extracted.images { + info!("Handling images payload for request_id {} / job {:?}", request_id, job.job_token); + let (media_token, batch_token) = process_images_payload(images_data, &job, server_state).await?; + if maybe_media_token.is_none() { + maybe_media_token = media_token; + } + if maybe_batch_token.is_none() { + maybe_batch_token = batch_token; + } + } + + if let Some(ref video_data) = extracted.video { info!("Handling video payload for request_id {} / job {:?}", request_id, job.job_token); - let token = process_video_payload(payload_obj, &job, server_state).await?; - maybe_media_token = Some(token); - } else if payload_obj.contains_key("model_glb") { + let token = process_video_payload(video_data, &job, server_state).await?; + if maybe_media_token.is_none() { + maybe_media_token = Some(token); + } + } + + if let Some(ref model_glb_data) = extracted.model_glb { info!("Handling model_glb payload for request_id {} / job {:?}", request_id, job.job_token); - let token = process_model_glb_payload(payload_obj, &job, server_state).await?; - maybe_media_token = Some(token); - } else if payload_obj.contains_key("model_mesh") { + let token = process_model_glb_payload(model_glb_data, extracted.thumbnail.as_ref(), &job, server_state).await?; + if maybe_media_token.is_none() { + maybe_media_token = Some(token); + } + } else if let Some(ref model_mesh_data) = extracted.model_mesh { info!("Handling model_mesh payload for request_id {} / job {:?}", request_id, job.job_token); - let token = process_model_mesh_payload(payload_obj, &job, server_state).await?; - maybe_media_token = Some(token); + let token = process_model_mesh_payload(model_mesh_data, &job, server_state).await?; + if maybe_media_token.is_none() { + maybe_media_token = Some(token); + } } } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_image_payload.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_image_payload.rs index 08a163d02e..3e520e6553 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_image_payload.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_image_payload.rs @@ -6,6 +6,7 @@ use enums::by_table::media_files::media_file_class::MediaFileClass; use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; use enums::by_table::media_files::media_file_type::MediaFileType; use errors::AnyhowResult; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ImageData; use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes; use images::image_info::image_info::ImageInfo; @@ -14,34 +15,16 @@ use mimetypes::mimetype_info::file_extension::FileExtension; use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails; use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; -use serde_json::{Map, Value}; use tokens::tokens::media_files::MediaFileToken; const PREFIX : Option<&str> = Some("artcraft_"); -#[derive(Deserialize, Debug)] -pub struct FalWebhookImage { - pub content_type: Option, - pub file_name: Option, - pub file_size: Option, - pub height: Option, - pub width: Option, - pub url: Option, -} - pub async fn process_image_payload( - payload: &Map, + image_data: &ImageData, job: &FalJobDetails, server_state: &ServerState, ) -> AnyhowResult { - let image_value = payload.get("image") - .ok_or_else(|| anyhow!("no `image` key in payload"))?; - - info!("Fal Image Payload: {:?}", image_value); - - let image: FalWebhookImage = serde_json::from_value(image_value.clone())?; - - let image_url = image.url + let image_url = image_data.url .as_deref() .ok_or_else(|| anyhow!("no `url` in image payload"))?; @@ -56,7 +39,6 @@ pub async fn process_image_payload( let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes) .ok_or_else(|| anyhow!("Failed to get mimetype info"))?; - info!("File type: {}, extension: {:?}", mimetype_info.mime_type(), mimetype_info.file_extension()); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_images_payload.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_images_payload.rs index 081e762a53..8e3c34ad58 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_images_payload.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_images_payload.rs @@ -6,6 +6,7 @@ use enums::by_table::media_files::media_file_class::MediaFileClass; use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; use enums::by_table::media_files::media_file_type::MediaFileType; use errors::AnyhowResult; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ImagesData; use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes; use images::image_info::image_info::ImageInfo; @@ -14,76 +15,54 @@ use mimetypes::mimetype_info::file_extension::FileExtension; use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails; use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; -use serde_json::{Map, Value}; use tokens::tokens::batch_generations::BatchGenerationToken; use tokens::tokens::media_files::MediaFileToken; const PREFIX : Option<&str> = Some("artcraft_"); -#[derive(Deserialize, Debug)] -pub struct FalWebhookImage { - pub content_type: Option, - pub file_name: Option, - pub file_size: Option, - pub height: Option, - pub width: Option, - pub url: Option, -} - pub async fn process_images_payload( - payload: &Map, + images_data: &[ImagesData], job: &FalJobDetails, server_state: &ServerState, ) -> AnyhowResult<(Option, Option)> { - let images_value = payload.get("images") - .ok_or_else(|| anyhow!("no `images` key in payload"))?; - - info!("Fal Images Payload: {:?}", images_value); - - let images: Vec = serde_json::from_value(images_value.clone())?; - let mut maybe_media_token = None; // NB: We are not going to create `batch_generations` table records. We don't need them. // The foreign key in `media_files` is enough to look up the rest of the batch. let mut maybe_batch_token = None; - if images.len() > 1 { + if images_data.len() > 1 { maybe_batch_token = Some(BatchGenerationToken::generate()); } - for image in images { - let media_token = upload_image(job, server_state, &image, maybe_batch_token.as_ref()).await?; - + for image in images_data { + let media_token = upload_image(job, server_state, image, maybe_batch_token.as_ref()).await?; + if maybe_media_token.is_none() { maybe_media_token = Some(media_token); // Set the first media token } } - + Ok((maybe_media_token, maybe_batch_token)) } async fn upload_image( job: &FalJobDetails, server_state: &ServerState, - image: &FalWebhookImage, + image: &ImagesData, maybe_batch_token: Option<&BatchGenerationToken>, ) -> AnyhowResult { let image_url = image.url .as_deref() .ok_or_else(|| anyhow!("no `url` in image payload"))?; - //let mime_type = image.content_type - // .as_deref() - // .ok_or_else(|| anyhow!("no `content_type` in image payload"))?; - let file_bytes = http_download_url_to_bytes(image_url) .await .map_err(|e| anyhow!("Failed to download image: {:?}", e))?; let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes) - .ok_or_else(|| anyhow!("Failed to get mimetype info"))?; + .ok_or_else(|| anyhow!("Failed to get mimetype info (1)"))?; info!("File type: {}, extension: {:?}", mimetype_info.mime_type(), @@ -96,7 +75,7 @@ async fn upload_image( let file_bytes = webp_bytes_to_png_bytes(&file_bytes)?; let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes) - .ok_or_else(|| anyhow!("Failed to get mimetype info"))?; + .ok_or_else(|| anyhow!("Failed to get mimetype info (2)"))?; info!("Updated file type: {}, extension: {:?}", mimetype_info.mime_type(), @@ -127,7 +106,7 @@ async fn upload_image( async fn upload_image_bytes( job: &FalJobDetails, server_state: &ServerState, - image: &FalWebhookImage, + image: &ImagesData, file_bytes: &[u8], mimetype_info: MimetypeInfo, maybe_batch_token: Option<&BatchGenerationToken>, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_glb_payload.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_glb_payload.rs index 755d01cb1e..083a2622d4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_glb_payload.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_glb_payload.rs @@ -7,12 +7,12 @@ use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCat use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; use enums::by_table::media_files::media_file_type::MediaFileType; use errors::AnyhowResult; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::{ModelGlbData, ThumbnailData}; use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; use log::{info, warn}; use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails; use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; -use serde_json::{Map, Value}; use mysql_queries::queries::media_files::edit::set_media_file_cover_image::{set_media_file_cover_image, UpdateArgs}; use tokens::tokens::media_files::MediaFileToken; @@ -55,34 +55,15 @@ const PREFIX : Option<&str> = Some("artcraft_"); */ -#[derive(Deserialize, Debug)] -pub struct FalMediaPayload { - pub content_type: Option, - pub file_name: Option, - pub file_size: Option, - pub url: Option, -} - pub async fn process_model_glb_payload( - payload: &Map, + model_glb_data: &ModelGlbData, + maybe_thumbnail_data: Option<&ThumbnailData>, job: &FalJobDetails, server_state: &ServerState, ) -> AnyhowResult { - - let model_glb_value = payload.get("model_glb") - .ok_or_else(|| anyhow!("no `model_mesh` key in payload"))?; - - info!("Fal Model Glb Payload: {:?}", model_glb_value); - - let mesh: FalMediaPayload = serde_json::from_value(model_glb_value.clone())?; - - let mesh_url = mesh.url + let mesh_url = model_glb_data.url .as_deref() - .ok_or_else(|| anyhow!("no `url` in image payload"))?; - - //let mime_type = mesh.content_type - // .as_deref() - // .ok_or_else(|| anyhow!("no `content_type` in mesh payload"))?; + .ok_or_else(|| anyhow!("no `url` in model glb payload"))?; let file_bytes = http_download_url_to_bytes(mesh_url) .await @@ -135,7 +116,7 @@ pub async fn process_model_glb_payload( info!("Glb media file uploaded with token: {}", media_token); let result = try_to_attach_thumbnail( - payload, + maybe_thumbnail_data, job, server_state, &media_token, @@ -150,33 +131,30 @@ pub async fn process_model_glb_payload( } async fn try_to_attach_thumbnail( - payload: &Map, + maybe_thumbnail_data: Option<&ThumbnailData>, job: &FalJobDetails, server_state: &ServerState, glb_media_token: &MediaFileToken, ) -> AnyhowResult<()> { + let thumbnail_data = maybe_thumbnail_data + .ok_or_else(|| anyhow!("no thumbnail data in extracted contents"))?; - let thumbnail_value = payload.get("thumbnail") - .ok_or_else(|| anyhow!("no `thumbnail` key in payload"))?; + info!("Fal Thumbnail Data: {:?}", thumbnail_data); - info!("Fal Thumbnail Payload: {:?}", thumbnail_value); - - let thumbnail : FalMediaPayload = serde_json::from_value(thumbnail_value.clone())?; - - let mesh_url = thumbnail.url + let thumbnail_url = thumbnail_data.url .as_deref() - .ok_or_else(|| anyhow!("no `url` in image payload"))?; + .ok_or_else(|| anyhow!("no `url` in thumbnail payload"))?; - let file_bytes = http_download_url_to_bytes(mesh_url) + let file_bytes = http_download_url_to_bytes(thumbnail_url) .await - .map_err(|e| anyhow!("Failed to download image: {:?}", e))?; + .map_err(|e| anyhow!("Failed to download thumbnail image: {:?}", e))?; let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes) .ok_or_else(|| anyhow!("Failed to get mimetype info"))?; let mime_type = mimetype_info.mime_type(); - info!("Mime type of image: {}", mime_type); + info!("Mime type of thumbnail image: {}", mime_type); let media_file_type = MediaFileType::try_from_mime_type(mime_type) .ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_mesh_payload.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_mesh_payload.rs index 4bee953471..cd6c541552 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_mesh_payload.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_model_mesh_payload.rs @@ -7,44 +7,24 @@ use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCat use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; use enums::by_table::media_files::media_file_type::MediaFileType; use errors::AnyhowResult; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ModelMeshData; use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; use log::info; use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails; use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; -use serde_json::{Map, Value}; use tokens::tokens::media_files::MediaFileToken; const PREFIX : Option<&str> = Some("artcraft_"); -#[derive(Deserialize, Debug)] -pub struct FalWebhookModelMesh { - pub content_type: Option, - pub file_name: Option, - pub file_size: Option, - pub url: Option, -} - pub async fn process_model_mesh_payload( - payload: &Map, + model_mesh_data: &ModelMeshData, job: &FalJobDetails, server_state: &ServerState, ) -> AnyhowResult { - - let model_mesh_value = payload.get("model_mesh") - .ok_or_else(|| anyhow!("no `model_mesh` key in payload"))?; - - info!("Fal Model Mesh Payload: {:?}", model_mesh_value); - - let mesh: FalWebhookModelMesh = serde_json::from_value(model_mesh_value.clone())?; - - let mesh_url = mesh.url + let mesh_url = model_mesh_data.url .as_deref() - .ok_or_else(|| anyhow!("no `url` in image payload"))?; - - //let mime_type = mesh.content_type - // .as_deref() - // .ok_or_else(|| anyhow!("no `content_type` in mesh payload"))?; + .ok_or_else(|| anyhow!("no `url` in model mesh payload"))?; let file_bytes = http_download_url_to_bytes(mesh_url) .await diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_video_payload.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_video_payload.rs index 124f7ee20c..d273879ffd 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_video_payload.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/process_success/process_video_payload.rs @@ -6,13 +6,13 @@ use enums::by_table::media_files::media_file_class::MediaFileClass; use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; use enums::by_table::media_files::media_file_type::MediaFileType; use errors::AnyhowResult; +use fal_client::webhook_api::hydrated::hydrated_webhook_contents::VideoData; use filesys::path_to_string::path_to_string; use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; use log::{error, info}; use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails; use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; -use serde_json::{Map, Value}; use std::io::Write; use tempfile::NamedTempFile; use thumbnail_generator::task_client::thumbnail_task::{ThumbnailTaskBuilder, ThumbnailTaskInputMimeType}; @@ -21,36 +21,14 @@ use ffmpeg_utils::ffprobe::ffprobe_get_info::ffprobe_get_info; const PREFIX : Option<&str> = Some("artcraft_"); -#[derive(Deserialize, Debug)] -pub struct FalWebhookVideo { - pub content_type: Option, - pub file_name: Option, - pub file_size: Option, - pub height: Option, // NB: Might not be on videos - pub width: Option, // NB: Might not be on videos - pub url: Option, -} - pub async fn process_video_payload( - payload: &Map, + video_data: &VideoData, job: &FalJobDetails, server_state: &ServerState, ) -> AnyhowResult { - - let video_value = payload.get("video") - .ok_or_else(|| anyhow!("no `image` key in payload"))?; - - info!("Fal Video Payload: {:?}", video_value); - - let video: FalWebhookVideo = serde_json::from_value(video_value.clone())?; - - let video_url = video.url + let video_url = video_data.url .as_deref() .ok_or_else(|| anyhow!("no `url` in video payload"))?; - - //let mime_type = image.content_type - // .as_deref() - // .ok_or_else(|| anyhow!("no `content_type` in image payload"))?; let file_bytes = http_download_url_to_bytes(video_url) .await