use serde_json::{json, Value};
use crate::error::ImError;
use crate::outbound::registry::{require_str, OutboundCommand, OutboundRegistration};
struct PostReadCommand;
impl OutboundCommand for PostReadCommand {
fn name(&self) -> &'static str {
"im_post_read"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(args, &["channel_id", "posts"], self.name())?;
let channel_id = require_str(args, "channel_id", self.name())?;
if crate::state::ChannelId::from_str(channel_id).is_none() {
return Err(ImError::Parse(format!(
"{}: 非 canonical channel_id",
self.name()
)));
}
let posts = args
.get("posts")
.and_then(Value::as_array)
.filter(|items| !items.is_empty())
.ok_or_else(|| {
ImError::Parse(format!(
"{}: posts 必须是非空 canonical 字符串数组",
self.name()
))
})?;
let posts: Vec<&str> = posts
.iter()
.map(|post| {
let post = post.as_str().ok_or_else(|| {
ImError::Parse(format!("{}: posts 只能包含字符串", self.name()))
})?;
if !crate::state::is_canonical_post_id(post) {
return Err(ImError::Parse(format!(
"{}: posts 只能包含 canonical post id",
self.name()
)));
}
Ok(post)
})
.collect::<Result<_, ImError>>()?;
Ok((
"post/read",
json!({ "channelId": channel_id, "posts": posts }),
))
}
}
static POST_READ: PostReadCommand = PostReadCommand;
inventory::submit! {
OutboundRegistration {
name: "im_post_read",
command: &POST_READ,
}
}
struct RevokeCommand;
impl OutboundCommand for RevokeCommand {
fn name(&self) -> &'static str {
"im_revoke"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let post_id = require_str(args, "post_id", self.name())?;
Ok(("posts/revoke", json!({ "postId": post_id })))
}
}
static REVOKE: RevokeCommand = RevokeCommand;
inventory::submit! {
OutboundRegistration {
name: "im_revoke",
command: &REVOKE,
}
}
struct CreateScheduleCommand;
impl OutboundCommand for CreateScheduleCommand {
fn name(&self) -> &'static str {
"im_create_schedule"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(
args,
&[
"channel_id",
"message",
"schedule_post_at",
"type",
"props",
"viewers",
"mentions",
"req_id",
],
self.name(),
)?;
let channel_id = require_str(args, "channel_id", self.name())?;
let message = args
.get("message")
.and_then(Value::as_str)
.ok_or_else(|| ImError::Parse("im_create_schedule: message must be string".into()))?;
if message.trim().is_empty()
&& !args
.get("props")
.and_then(Value::as_object)
.is_some_and(|props| !props.is_empty())
{
return Err(ImError::Parse("im_create_schedule: empty content".into()));
}
let schedule_post_at = args
.get("schedule_post_at")
.and_then(Value::as_i64)
.filter(|value| *value > 0)
.ok_or_else(|| {
ImError::Parse(format!(
"{}: 缺/坏 schedule_post_at(正 int64 毫秒)",
self.name()
))
})?;
let mut post = json!({ "channelId": channel_id, "message": message });
if let Some(post_type) = args.get("type") {
let post_type = post_type
.as_str()
.filter(|value| !value.is_empty())
.ok_or_else(|| ImError::Parse(format!("{}: type 必须为非空字符串", self.name())))?;
post["type"] = json!(post_type);
}
if let Some(props) = args.get("props") {
if !props.is_object() {
return Err(ImError::Parse(format!(
"{}: props 必须为 object",
self.name()
)));
}
post["props"] = props.clone();
}
for field in ["viewers", "mentions"] {
if let Some(values) = args.get(field) {
if !values
.as_array()
.is_some_and(|values| values.iter().all(Value::is_string))
{
return Err(ImError::Parse(format!(
"im_create_schedule: {field} must be string array"
)));
}
post[field] = values.clone();
}
}
Ok((
"posts/createSchedule",
json!({ "post": post, "schedulePostAt": schedule_post_at }),
))
}
}
static CREATE_SCHEDULE: CreateScheduleCommand = CreateScheduleCommand;
inventory::submit! {
OutboundRegistration {
name: "im_create_schedule",
command: &CREATE_SCHEDULE,
}
}
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}: 未知字段 {unknown}")));
}
Ok(())
}
struct CancelScheduleCommand;
impl OutboundCommand for CancelScheduleCommand {
fn name(&self) -> &'static str {
"im_cancel_schedule"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(args, &["channel_id", "req_id"], self.name())?;
let channel_id = require_str(args, "channel_id", self.name())?;
Ok(("posts/cancelSchedule", json!({ "channelId": channel_id })))
}
}
static CANCEL_SCHEDULE: CancelScheduleCommand = CancelScheduleCommand;
inventory::submit! {
OutboundRegistration {
name: "im_cancel_schedule",
command: &CANCEL_SCHEDULE,
}
}