use serde_json::{json, Value};
use std::collections::BTreeSet;
use crate::error::ImError;
use crate::outbound::registry::{require_str, OutboundCommand, OutboundRegistration};
const MAX_URGENT_TARGETS: usize = 100;
struct UrgentPostCommand;
impl OutboundCommand for UrgentPostCommand {
fn name(&self) -> &'static str {
"im_urgent_post"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(
args,
&["post_id", "target_ids", "message", "req_id", "channel_id"],
self.name(),
)?;
let post_id = require_str(args, "post_id", self.name())?;
let target_values = args
.get("target_ids")
.and_then(Value::as_array)
.filter(|items| !items.is_empty() && items.len() <= MAX_URGENT_TARGETS)
.ok_or_else(|| {
ImError::Parse(format!(
"{}: target_ids 必须为 1..={MAX_URGENT_TARGETS} 个非空字符串",
self.name()
))
})?;
let mut unique = BTreeSet::new();
let mut target_ids = Vec::with_capacity(target_values.len());
for target in target_values {
let target = target
.as_str()
.filter(|target| !target.is_empty())
.ok_or_else(|| ImError::Parse(format!("{}: target_ids 含空值", self.name())))?;
if !unique.insert(target) {
return Err(ImError::Parse(format!(
"{}: target_ids 不允许重复值",
self.name()
)));
}
target_ids.push(target);
}
require_optional_request_id(args, self.name())?;
let mut b = json!({
"postId": post_id,
"targetIds": target_ids,
});
if let Some(msg) = args.get("message").and_then(Value::as_str) {
b["message"] = json!(msg);
}
Ok(("posts/urgentPost", b))
}
}
static URGENT_POST: UrgentPostCommand = UrgentPostCommand;
inventory::submit! {
OutboundRegistration {
name: "im_urgent_post",
command: &URGENT_POST,
}
}
struct UrgentConfirmCommand;
impl OutboundCommand for UrgentConfirmCommand {
fn name(&self) -> &'static str {
"im_urgent_confirm"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(args, &["post_id", "req_id", "channel_id"], self.name())?;
let post_id = require_str(args, "post_id", self.name())?;
require_optional_request_id(args, self.name())?;
Ok(("posts/urgentConfirm", json!({ "postId": post_id })))
}
}
static URGENT_CONFIRM: UrgentConfirmCommand = UrgentConfirmCommand;
inventory::submit! {
OutboundRegistration {
name: "im_urgent_confirm",
command: &URGENT_CONFIRM,
}
}
struct UrgentCancelCommand;
impl OutboundCommand for UrgentCancelCommand {
fn name(&self) -> &'static str {
"im_urgent_cancel"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(args, &["post_id", "req_id", "channel_id"], self.name())?;
let post_id = require_str(args, "post_id", self.name())?;
require_optional_request_id(args, self.name())?;
Ok(("posts/urgentCancel", json!({ "postId": post_id })))
}
}
static URGENT_CANCEL: UrgentCancelCommand = UrgentCancelCommand;
inventory::submit! {
OutboundRegistration {
name: "im_urgent_cancel",
command: &URGENT_CANCEL,
}
}
fn require_exact_keys(args: &Value, allowed: &[&str], cmd: &str) -> Result<(), ImError> {
let object = args
.as_object()
.ok_or_else(|| ImError::Parse(format!("{cmd}: payload 必须是 object")))?;
if let Some(unknown) = object.keys().find(|key| !allowed.contains(&key.as_str())) {
return Err(ImError::Parse(format!(
"{cmd}: 未知或非 canonical 字段 '{unknown}'"
)));
}
Ok(())
}
fn require_optional_request_id(args: &Value, cmd: &str) -> Result<(), ImError> {
if let Some(value) = args.get("req_id") {
if value.as_str().filter(|value| !value.is_empty()).is_none() {
return Err(ImError::Parse(format!("{cmd}: req_id 必须是非空字符串")));
}
}
Ok(())
}