use serde_json::{json, Map, Value};
use crate::error::ImError;
use crate::outbound::registry::{require_str, OutboundCommand, OutboundRegistration};
struct UpdatePostPropsCommand;
impl OutboundCommand for UpdatePostPropsCommand {
fn name(&self) -> &'static str {
"im_update_post_props"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let post_id = require_str(args, "post_id", self.name())?;
let props = args
.get("props")
.filter(|v| v.is_object())
.ok_or_else(|| ImError::Parse(format!("{}: 缺/坏 props(须为对象)", self.name())))?;
Ok((
"posts/updatePostProps",
json!({ "postId": post_id, "props": props }),
))
}
}
static UPDATE_POST_PROPS: UpdatePostPropsCommand = UpdatePostPropsCommand;
inventory::submit! {
OutboundRegistration {
name: "im_update_post_props",
command: &UPDATE_POST_PROPS,
}
}
pub const HOST_PRIVATE_MEDIA_FIELDS: [&str; 15] = [
"mediaInput",
"localPath",
"local_path",
"localUrl",
"local_url",
"sha256",
"headers",
"url",
"publicUrl",
"uploadId",
"uploadToken",
"objectKey",
"etag",
"method",
"state",
];
const AUDIO_FILE_KEYS: [&str; 7] = [
"bucket",
"id",
"uri",
"name",
"contentType",
"size",
"duration",
];
const VIDEO_FILE_KEYS: [&str; 9] = [
"bucket",
"id",
"uri",
"name",
"contentType",
"size",
"duration",
"width",
"height",
];
const TEMPLATE_FILE_KEYS: [&str; 5] = ["bucket", "id", "uri", "width", "height"];
const TEMPLATE_INNER_IMAGE: &str = "IMAGE";
pub fn build_media_message_props(msg_type: &str, props: &Value) -> Result<Value, ImError> {
let object = props
.as_object()
.ok_or_else(|| ImError::Parse(format!("{msg_type} props 必须是对象")))?;
match msg_type {
"AUDIO" | "VIDEO" => build_single_file_props(msg_type, object),
"TEMPLATE" => build_template_props(object),
other => Err(ImError::Parse(format!(
"{other} 不是 MessageV3 媒体消息类型(仅 AUDIO/VIDEO/TEMPLATE)"
))),
}
}
fn build_single_file_props(msg_type: &str, props: &Map<String, Value>) -> Result<Value, ImError> {
if props.contains_key("files") {
return Err(ImError::Parse(format!(
"{msg_type} 必须使用单 props.file,不得携带 props.files"
)));
}
let file = props
.get("file")
.ok_or_else(|| ImError::Parse(format!("{msg_type} 缺少 props.file")))?;
let is_video = msg_type == "VIDEO";
let mut node = stable_object_reference(file, msg_type)?;
node.insert(
"name".to_string(),
json!(non_empty_str(file, "name", msg_type)?),
);
node.insert(
"contentType".to_string(),
json!(non_empty_str(file, "contentType", msg_type)?),
);
node.insert("size".to_string(), json!(positive_size(file, msg_type)?));
node.insert(
"duration".to_string(),
json!(host_duration_seconds(file, msg_type)?),
);
if is_video {
node.insert(
"width".to_string(),
json!(host_dimension(file, "width", msg_type)?),
);
node.insert(
"height".to_string(),
json!(host_dimension(file, "height", msg_type)?),
);
} else if file.get("width").is_some() || file.get("height").is_some() {
return Err(ImError::Parse(
"AUDIO 不得携带 width/height(Host 不为音频提取尺寸)".to_string(),
));
}
let expected: &[&str] = if is_video {
&VIDEO_FILE_KEYS
} else {
&AUDIO_FILE_KEYS
};
debug_assert_eq!(node.len(), expected.len());
let mut out = props.clone();
out.insert("file".to_string(), Value::Object(node));
Ok(Value::Object(out))
}
fn build_template_props(props: &Map<String, Value>) -> Result<Value, ImError> {
if props.contains_key("file") || props.contains_key("files") {
return Err(ImError::Parse(
"TEMPLATE 壳不得回落为顶层 props.file / props.files".to_string(),
));
}
let template = props
.get("template")
.and_then(Value::as_object)
.ok_or_else(|| ImError::Parse("TEMPLATE 缺少 props.template 壳".to_string()))?;
let inner_type = template
.get("type")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| ImError::Parse("TEMPLATE 缺少 props.template.type".to_string()))?;
let file = template
.get("file")
.ok_or_else(|| ImError::Parse("TEMPLATE/IMAGE 缺少 props.template.file".to_string()))?;
if inner_type != TEMPLATE_INNER_IMAGE {
return Err(ImError::Parse(format!(
"TEMPLATE 挂载图片时 props.template.type 必须是 IMAGE,不得回落为 {inner_type}"
)));
}
let mut node = stable_object_reference(file, "TEMPLATE/IMAGE")?;
node.insert(
"width".to_string(),
json!(host_dimension(file, "width", "TEMPLATE/IMAGE")?),
);
node.insert(
"height".to_string(),
json!(host_dimension(file, "height", "TEMPLATE/IMAGE")?),
);
debug_assert_eq!(node.len(), TEMPLATE_FILE_KEYS.len());
let mut inner = template.clone();
inner.insert("type".to_string(), json!(TEMPLATE_INNER_IMAGE));
inner.insert("file".to_string(), Value::Object(node));
let mut out = props.clone();
out.insert("template".to_string(), Value::Object(inner));
Ok(Value::Object(out))
}
fn stable_object_reference(file: &Value, label: &str) -> Result<Map<String, Value>, ImError> {
let object = file
.as_object()
.ok_or_else(|| ImError::Parse(format!("{label} 媒体节点必须是对象")))?;
if let Some(field) = HOST_PRIVATE_MEDIA_FIELDS
.iter()
.find(|field| object.contains_key(**field))
{
return Err(ImError::Parse(format!(
"{label} 最终 props 不得携带 Host 私有字段 {field}"
)));
}
let bucket = non_empty_str(file, "bucket", label)?;
let id = non_empty_str(file, "id", label)?;
let uri = non_empty_str(file, "uri", label)?;
if bucket.len() > 64
|| id.len() > 255
|| !uri.starts_with('/')
|| uri.starts_with("//")
|| uri.contains("..")
|| uri.contains(['?', '#', '\\'])
{
return Err(ImError::Parse(format!("{label} 稳定对象引用非法")));
}
let mut node = Map::new();
node.insert("bucket".to_string(), json!(bucket));
node.insert("id".to_string(), json!(id));
node.insert("uri".to_string(), json!(uri));
Ok(node)
}
fn non_empty_str(node: &Value, key: &str, label: &str) -> Result<String, ImError> {
node.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| ImError::Parse(format!("{label} 缺少非空 {key}")))
}
fn positive_size(node: &Value, label: &str) -> Result<u64, ImError> {
node.get("size")
.and_then(Value::as_u64)
.filter(|value| (1..=(64 << 20)).contains(value))
.ok_or_else(|| ImError::Parse(format!("{label} size 必须是 1..=67108864 的整数")))
}
fn host_duration_seconds(node: &Value, label: &str) -> Result<f64, ImError> {
node.get("duration")
.and_then(Value::as_f64)
.filter(|value| value.is_finite() && *value > 0.0)
.ok_or_else(|| ImError::Parse(format!("{label} duration 必须是 Host 提取的正数秒值")))
}
fn host_dimension(node: &Value, key: &str, label: &str) -> Result<u64, ImError> {
node.get(key)
.and_then(Value::as_u64)
.filter(|value| (1..=65_535).contains(value))
.ok_or_else(|| ImError::Parse(format!("{label} {key} 必须是 Host 提取的正整数像素值")))
}
pub fn strip_host_private_media_fields(props: &Value) -> Value {
let mut sanitized = props.clone();
if let Some(object) = sanitized.as_object_mut() {
strip_media_container(object);
if let Some(template) = object.get_mut("template").and_then(Value::as_object_mut) {
strip_media_container(template);
}
}
sanitized
}
fn strip_media_container(container: &mut Map<String, Value>) {
if let Some(file) = container.get_mut("file") {
strip_media_node(file);
}
if let Some(files) = container.get_mut("files").and_then(Value::as_array_mut) {
for file in files {
strip_media_node(file);
}
}
}
fn strip_media_node(node: &mut Value) {
let Some(object) = node.as_object_mut() else {
return;
};
for field in HOST_PRIVATE_MEDIA_FIELDS {
object.remove(field);
}
if let Some(upload) = object.get_mut("upload").and_then(Value::as_object_mut) {
for field in HOST_PRIVATE_MEDIA_FIELDS {
upload.remove(field);
}
}
}