mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 19:58:46 -04:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
091579e9bb | ||
|
|
fa7a0c3200 | ||
|
|
422eabb616 | ||
|
|
0c810cfadb | ||
|
|
7bf757fdf1 | ||
|
|
b2947f69ce | ||
|
|
c5e9cf6bcf | ||
|
|
d02ce726c6 | ||
|
|
eb8301ab43 | ||
|
|
b8895fbae9 | ||
|
|
ebbc9af5ed | ||
|
|
7879d682ab |
+50
-42
@@ -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<Value>) -> (Option<String>, 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ExtractedContents>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExtractedContents {
|
||||
/// Parsed from `payload.image` (single image result).
|
||||
pub image: Option<ImageData>,
|
||||
|
||||
/// Parsed from `payload.images` (batch image results).
|
||||
pub images: Option<Vec<ImagesData>>,
|
||||
|
||||
/// Parsed from `payload.video`.
|
||||
pub video: Option<VideoData>,
|
||||
|
||||
/// Parsed from `payload.model_glb`.
|
||||
pub model_glb: Option<ModelGlbData>,
|
||||
|
||||
/// Parsed from `payload.model_mesh`.
|
||||
pub model_mesh: Option<ModelMeshData>,
|
||||
|
||||
/// Parsed from `payload.thumbnail`.
|
||||
pub thumbnail: Option<ThumbnailData>,
|
||||
}
|
||||
|
||||
/// Data under `payload.image`:
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ImageData {
|
||||
pub url: Option<String>,
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<u64>,
|
||||
pub height: Option<u64>,
|
||||
pub width: Option<u64>,
|
||||
}
|
||||
|
||||
/// Data under `payload.images` (a list of these):
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ImagesData {
|
||||
pub url: Option<String>,
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<u64>,
|
||||
pub height: Option<u64>,
|
||||
pub width: Option<u64>,
|
||||
}
|
||||
|
||||
/// Data under `payload.video`:
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VideoData {
|
||||
pub url: Option<String>,
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<u64>,
|
||||
}
|
||||
|
||||
/// Data under `payload.model_glb` (there may be other sibling keys too)
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ModelGlbData {
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
/// Data under `payload.model_mesh` (there may be other sibling keys too)
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ModelMeshData {
|
||||
pub content_type: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorData {
|
||||
/// The first human-readable message from `payload.detail[].msg`, if any.
|
||||
pub message: Option<String>,
|
||||
|
||||
/// The first machine-readable error type from `payload.detail[].type`, if any.
|
||||
pub error_type: Option<WebhookErrorType>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PayloadErrorData {
|
||||
pub payload_error: String,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod hydrated_webhook_contents;
|
||||
pub mod success_case_extractors;
|
||||
+100
@@ -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<String, Value>) -> Option<ImageData> {
|
||||
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<String, serde_json::Value> = 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<String, serde_json::Value> = 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<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
"images": [{"url": "https://example.com/img.png"}]
|
||||
}"#).unwrap();
|
||||
|
||||
assert!(extract_image(&obj).is_none());
|
||||
}
|
||||
}
|
||||
+96
@@ -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<String, Value>) -> Option<Vec<ImagesData>> {
|
||||
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<String, serde_json::Value> = 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<String, serde_json::Value> = 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<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
"video": {"url": "https://example.com/v.mp4"}
|
||||
}"#).unwrap();
|
||||
|
||||
assert!(extract_images(&obj).is_none());
|
||||
}
|
||||
}
|
||||
+91
@@ -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<String, Value>) -> Option<ModelGlbData> {
|
||||
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<String, serde_json::Value> = 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<String, serde_json::Value> = 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<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
"video": {"url": "https://example.com/v.mp4"}
|
||||
}"#).unwrap();
|
||||
|
||||
assert!(extract_model_glb(&obj).is_none());
|
||||
}
|
||||
}
|
||||
+89
@@ -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<String, Value>) -> Option<ModelMeshData> {
|
||||
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<String, serde_json::Value> = 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<String, serde_json::Value> = 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<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
"image": {"url": "https://example.com/img.png"}
|
||||
}"#).unwrap();
|
||||
|
||||
assert!(extract_model_mesh(&obj).is_none());
|
||||
}
|
||||
}
|
||||
+86
@@ -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<String, Value>) -> Option<ThumbnailData> {
|
||||
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<String, serde_json::Value> = 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<String, serde_json::Value> = 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());
|
||||
}
|
||||
}
|
||||
+90
@@ -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<String, Value>) -> Option<VideoData> {
|
||||
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<String, serde_json::Value> = 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<String, serde_json::Value> = 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<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
"images": [{"url": "https://example.com/img.png"}]
|
||||
}"#).unwrap();
|
||||
|
||||
assert!(extract_video(&obj).is_none());
|
||||
}
|
||||
}
|
||||
@@ -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<ExtractedContents> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod parse_webhook_inner_payload;
|
||||
pub mod parse_webhook_payload;
|
||||
pub mod payload;
|
||||
pub mod raw;
|
||||
pub mod hydrated;
|
||||
pub mod hydrate_webhook_contents;
|
||||
pub mod parse_raw_webhook_payload;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
use crate::webhook_api::raw::raw_webhook_payload::RawWebhookPayload;
|
||||
|
||||
pub fn parse_raw_webhook_payload(json: &str) -> Result<RawWebhookPayload, serde_json::Error> {
|
||||
serde_json::from_str(json)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
use crate::webhook_api::payload::webhook_payload::WebhookPayload;
|
||||
|
||||
pub fn parse_webhook_payload(json: &str) -> Result<WebhookPayload, serde_json::Error> {
|
||||
serde_json::from_str(json)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod webhook_inner_payload;
|
||||
pub mod webhook_payload;
|
||||
pub mod webhook_error_type;
|
||||
@@ -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<String>,
|
||||
/// The first machine-readable error type from `payload.detail[].type`, if any.
|
||||
pub error_type: Option<WebhookErrorType>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PayloadErrorData {
|
||||
pub payload_error: String,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod webhook_error_type;
|
||||
pub mod raw_webhook_payload;
|
||||
+3
-3
@@ -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,
|
||||
@@ -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"
|
||||
}
|
||||
+37
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+14
-12
@@ -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,
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+38
-17
@@ -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<Json<SimpleGenericJsonSuccess>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-21
@@ -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<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub height: Option<usize>,
|
||||
pub width: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn process_image_payload(
|
||||
payload: &Map<String, Value>,
|
||||
image_data: &ImageData,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
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());
|
||||
|
||||
+11
-32
@@ -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<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub height: Option<usize>,
|
||||
pub width: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn process_images_payload(
|
||||
payload: &Map<String, Value>,
|
||||
images_data: &[ImagesData],
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<(Option<MediaFileToken>, Option<BatchGenerationToken>)> {
|
||||
|
||||
let images_value = payload.get("images")
|
||||
.ok_or_else(|| anyhow!("no `images` key in payload"))?;
|
||||
|
||||
info!("Fal Images Payload: {:?}", images_value);
|
||||
|
||||
let images: Vec<FalWebhookImage> = 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<MediaFileToken> {
|
||||
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>,
|
||||
|
||||
+15
-37
@@ -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<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn process_model_glb_payload(
|
||||
payload: &Map<String, Value>,
|
||||
model_glb_data: &ModelGlbData,
|
||||
maybe_thumbnail_data: Option<&ThumbnailData>,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
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<String, Value>,
|
||||
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))?;
|
||||
|
||||
+4
-24
@@ -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<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn process_model_mesh_payload(
|
||||
payload: &Map<String, Value>,
|
||||
model_mesh_data: &ModelMeshData,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
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
|
||||
|
||||
+3
-25
@@ -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<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub file_size: Option<usize>,
|
||||
pub height: Option<usize>, // NB: Might not be on videos
|
||||
pub width: Option<usize>, // NB: Might not be on videos
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn process_video_payload(
|
||||
payload: &Map<String, Value>,
|
||||
video_data: &VideoData,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user