use serde_json::{json, Map, Value};
use crate::error::ImError;
pub fn normalize_command_payload(public: &str, payload: &[u8]) -> Result<Vec<u8>, ImError> {
let mut value: Value = serde_json::from_slice(payload)
.map_err(|e| ImError::Parse(format!("{public} payload: {e}")))?;
let obj = value
.as_object_mut()
.ok_or_else(|| ImError::Parse(format!("{public} payload must be object")))?;
normalize_top_level_keys(obj);
normalize_public_semantics(public, obj)?;
serde_json::to_vec(&value).map_err(|e| ImError::Serialize(e.to_string()))
}
fn normalize_top_level_keys(obj: &mut Map<String, Value>) {
let keys: Vec<String> = obj.keys().cloned().collect();
for key in keys {
let normalized = camel_to_snake(&key);
if normalized != key {
move_key(obj, &key, &normalized);
}
}
}
fn normalize_public_semantics(public: &str, obj: &mut Map<String, Value>) -> Result<(), ImError> {
match public {
"im_send_quick_reply" => move_key(obj, "reaction", "emoji"),
"im_read_channel" => {
let channel_id = obj
.get("channel_id")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| ImError::Parse("im_read_channel: 缺 channel_id".to_string()))?;
obj.insert(
"channels".to_string(),
json!([{ "id": channel_id, "isRoot": true }]),
);
}
"im_mark_read" => {
if !obj.contains_key("posts") {
if let Some(post_id) = obj.get("post_id").and_then(Value::as_str) {
obj.insert("posts".to_string(), json!([post_id]));
}
}
}
"im_get_replies" => move_key(obj, "post_id", "reply_id"),
"im_get_reply_branch" => move_key(obj, "post_id", "reply_first_level_id"),
"im_locate_post" => {
if !obj.contains_key("post_ids") {
if let Some(post_id) = obj.get("post_id").and_then(Value::as_str) {
obj.insert("post_ids".to_string(), json!([post_id]));
}
}
obj.insert("locate".to_string(), Value::Bool(true));
}
"im_create_schedule" => {
if !obj.contains_key("schedule_post_at") {
move_key(obj, "send_at", "schedule_post_at");
}
if let Some(post) = obj.get("post").and_then(Value::as_object).cloned() {
if !obj.contains_key("message") {
if let Some(message) = post.get("message").cloned() {
obj.insert("message".to_string(), message);
}
}
if !obj.contains_key("temporary_id") {
if let Some(temporary_id) = post
.get("temporaryId")
.or_else(|| post.get("temporary_id"))
.cloned()
{
obj.insert("temporary_id".to_string(), temporary_id);
}
}
}
}
"im_channel_settings" => {
let settings = obj
.remove("settings")
.and_then(|value| value.as_object().cloned())
.ok_or_else(|| ImError::Parse("im_channel_settings: 缺 settings".to_string()))?;
copy_setting(
&settings,
obj,
&["displayName", "display_name"],
"display_name",
);
copy_setting(&settings, obj, &["description", "purpose"], "purpose");
copy_setting(&settings, obj, &["rules", "header"], "header");
}
_ => {}
}
Ok(())
}
fn copy_setting(
settings: &Map<String, Value>,
target: &mut Map<String, Value>,
keys: &[&str],
target_key: &str,
) {
if let Some(value) = keys.iter().find_map(|key| settings.get(*key)).cloned() {
target.insert(target_key.to_string(), value);
}
}
fn camel_to_snake(value: &str) -> String {
let mut normalized = String::with_capacity(value.len());
for ch in value.chars() {
if ch.is_ascii_uppercase() {
normalized.push('_');
normalized.push(ch.to_ascii_lowercase());
} else {
normalized.push(ch);
}
}
normalized
}
fn move_key(obj: &mut Map<String, Value>, from: &str, to: &str) {
if !obj.contains_key(to) {
if let Some(value) = obj.remove(from) {
obj.insert(to.to_string(), value);
}
} else {
obj.remove(from);
}
}