use serde_json::{json, Value};
use crate::error::ImError;
use crate::outbound::registry::{require_str, OutboundCommand, OutboundRegistration};
fn require_pin_keys(args: &Value, 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| !matches!(key.as_str(), "channel_id" | "post_id"))
{
return Err(ImError::Parse(format!(
"{cmd}: 未知或非 canonical 字段 '{unknown}'"
)));
}
Ok(())
}
struct SetMessageTopCommand;
impl OutboundCommand for SetMessageTopCommand {
fn name(&self) -> &'static str {
"im_set_message_top"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_pin_keys(args, self.name())?;
let channel_id = require_str(args, "channel_id", self.name())?;
let post_id = require_str(args, "post_id", self.name())?;
Ok((
"channel/add/postPinned",
json!({ "channelId": channel_id, "postId": post_id }),
))
}
}
static SET_MESSAGE_TOP: SetMessageTopCommand = SetMessageTopCommand;
inventory::submit! {
OutboundRegistration {
name: "im_set_message_top",
command: &SET_MESSAGE_TOP,
}
}
struct SetMessageUntopCommand;
impl OutboundCommand for SetMessageUntopCommand {
fn name(&self) -> &'static str {
"im_set_message_untop"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_pin_keys(args, self.name())?;
let channel_id = require_str(args, "channel_id", self.name())?;
let post_id = require_str(args, "post_id", self.name())?;
Ok((
"channel/remove/postPinned",
json!({ "channelId": channel_id, "postId": post_id }),
))
}
}
static SET_MESSAGE_UNTOP: SetMessageUntopCommand = SetMessageUntopCommand;
inventory::submit! {
OutboundRegistration {
name: "im_set_message_untop",
command: &SET_MESSAGE_UNTOP,
}
}
#[cfg(test)]
mod tests {
use helix_core::effect::Effect;
use helix_core::Correlation;
use serde_json::json;
use crate::outbound::registry::{handle_outbound, is_outbound};
#[test]
fn set_message_top_dispatch_and_body() {
assert!(is_outbound("im_set_message_top"));
let corr = Correlation::from_raw(1);
let payload = serde_json::to_vec(&json!({ "channel_id": "c1", "post_id": "p1" })).unwrap();
let effects = handle_outbound(
"im_set_message_top",
&payload,
"http://h/api",
"http://h",
Some("conn1"),
corr,
)
.expect("set_message_top should dispatch");
match &effects[0] {
Effect::Http { req, .. } => {
assert!(
req.url.ends_with("/channel/add/postPinned"),
"url={}",
req.url
);
let body: serde_json::Value =
serde_json::from_slice(req.body.as_ref().expect("body")).unwrap();
assert_eq!(body["channelId"], "c1");
assert_eq!(body["postId"], "p1");
}
other => panic!("expected Http, got {other:?}"),
}
}
#[test]
fn set_message_untop_dispatch_and_body() {
assert!(is_outbound("im_set_message_untop"));
let corr = Correlation::from_raw(2);
let payload = serde_json::to_vec(&json!({ "channel_id": "c2", "post_id": "p2" })).unwrap();
let effects = handle_outbound(
"im_set_message_untop",
&payload,
"http://h/api",
"http://h",
None,
corr,
)
.expect("set_message_untop should dispatch");
match &effects[0] {
Effect::Http { req, .. } => {
assert!(
req.url.ends_with("/channel/remove/postPinned"),
"url={}",
req.url
);
let body: serde_json::Value =
serde_json::from_slice(req.body.as_ref().expect("body")).unwrap();
assert_eq!(body["channelId"], "c2");
assert_eq!(body["postId"], "p2");
}
other => panic!("expected Http, got {other:?}"),
}
}
#[test]
fn set_message_top_missing_post_id_errors() {
let corr = Correlation::from_raw(3);
let payload = serde_json::to_vec(&json!({ "channel_id": "c1" })).unwrap();
assert!(handle_outbound(
"im_set_message_top",
&payload,
"http://h/api",
"http://h",
None,
corr
)
.is_err());
}
#[test]
fn set_message_top_rejects_unknown_fields() {
let corr = Correlation::from_raw(4);
let payload = serde_json::to_vec(&json!({
"channel_id": "c1",
"post_id": "p1",
"user_id": "u1"
}))
.unwrap();
assert!(handle_outbound(
"im_set_message_top",
&payload,
"http://h/api",
"http://h",
None,
corr
)
.is_err());
}
}