use serde_json::{json, Value};
use crate::error::ImError;
use crate::outbound::registry::{require_str, OutboundCommand, OutboundRegistration};
fn put_opt_str(body: &mut Value, args: &Value, src_key: &str, wire_key: &str) {
if let Some(v) = args.get(src_key).and_then(Value::as_str) {
body[wire_key] = json!(v);
}
}
fn put_opt_val(body: &mut Value, args: &Value, src_key: &str, wire_key: &str) {
if let Some(v) = args.get(src_key) {
if !v.is_null() {
body[wire_key] = v.clone();
}
}
}
struct ChangeInfoCommand;
impl OutboundCommand for ChangeInfoCommand {
fn name(&self) -> &'static str {
"im_channel_change_info"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let channel_id = require_str(args, "channel_id", self.name())?;
let mut b = json!({ "channelId": channel_id });
put_opt_str(&mut b, args, "name", "name");
put_opt_str(&mut b, args, "display_name", "displayName");
put_opt_str(&mut b, args, "header", "header");
put_opt_str(&mut b, args, "purpose", "purpose");
put_opt_str(&mut b, args, "orient", "orient");
put_opt_str(&mut b, args, "module", "module");
put_opt_str(&mut b, args, "picture_type", "pictureType");
put_opt_val(&mut b, args, "picture", "picture");
put_opt_val(&mut b, args, "source", "source");
if let Some(purpose) = args.get("purpose") {
if !purpose.is_null() && !purpose.is_string() {
return Err(ImError::Parse(format!(
"{}: purpose 必须为字符串",
self.name()
)));
}
}
Ok(("channel/change/info", b))
}
}
static CHANGE_INFO: ChangeInfoCommand = ChangeInfoCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_change_info",
command: &CHANGE_INFO,
}
}
struct ChangeSourceCommand;
impl OutboundCommand for ChangeSourceCommand {
fn name(&self) -> &'static str {
"im_channel_change_source"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let channel_id = require_str(args, "channel_id", self.name())?;
let title = args.get("title").and_then(Value::as_str).unwrap_or("");
Ok((
"channel/change/source",
json!({ "id": channel_id, "title": title }),
))
}
}
static CHANGE_SOURCE: ChangeSourceCommand = ChangeSourceCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_change_source",
command: &CHANGE_SOURCE,
}
}
struct ChangePictureCommand;
impl OutboundCommand for ChangePictureCommand {
fn name(&self) -> &'static str {
"im_channel_change_picture"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let channel_id = require_str(args, "channel_id", self.name())?;
let picture_type = require_str(args, "picture_type", self.name())?;
let picture = args
.get("picture")
.filter(|v| !v.is_null())
.ok_or_else(|| ImError::Parse(format!("{}: 缺 picture(map 非 null)", self.name())))?
.clone();
Ok((
"channel/change/picture",
json!({ "id": channel_id, "pictureType": picture_type, "picture": picture }),
))
}
}
static CHANGE_PICTURE: ChangePictureCommand = ChangePictureCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_change_picture",
command: &CHANGE_PICTURE,
}
}
struct ChangeNoticeCommand;
impl OutboundCommand for ChangeNoticeCommand {
fn name(&self) -> &'static str {
"im_channel_change_notice"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let channel_id = require_str(args, "channel_id", self.name())?;
let notice = args
.get("notice")
.filter(|value| value.is_object())
.ok_or_else(|| ImError::Parse(format!("{}: 缺 notice 对象", self.name())))?;
let text = notice
.get("text")
.and_then(Value::as_str)
.ok_or_else(|| ImError::Parse(format!("{}: notice.text 必须为字符串", self.name())))?;
Ok((
"channel/change/notice",
json!({
"id": channel_id,
"notice": {"text": text},
}),
))
}
}
static CHANGE_NOTICE: ChangeNoticeCommand = ChangeNoticeCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_change_notice",
command: &CHANGE_NOTICE,
}
}
struct ChangeTopCommand;
impl OutboundCommand for ChangeTopCommand {
fn name(&self) -> &'static str {
"im_channel_change_top"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let channel_id = require_str(args, "channel_id", self.name())?;
let top = args
.get("top")
.and_then(Value::as_bool)
.ok_or_else(|| ImError::Parse(format!("{}: 缺 top(bool)", self.name())))?;
Ok((
"channel/change/top",
json!({ "channelId": channel_id, "top": top }),
))
}
}
static CHANGE_TOP: ChangeTopCommand = ChangeTopCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_change_top",
command: &CHANGE_TOP,
}
}
struct ChangePermissionCommand;
impl OutboundCommand for ChangePermissionCommand {
fn name(&self) -> &'static str {
"im_channel_change_permission"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let channel_id = require_str(args, "channel_id", self.name())?;
let mut body = json!({ "channelId": channel_id });
let mut patch_count = 0;
for (arg_key, wire_key) in [
("mention_permission", "mentionPermission"),
("notice_permission", "noticePermission"),
("top_permission", "topPermission"),
] {
if let Some(value) = args.get(arg_key) {
let permission = value
.as_str()
.filter(|value| matches!(*value, "BOSS" | "CREATOR" | "MANAGER" | "MEMBER"))
.ok_or_else(|| {
ImError::Parse(format!(
"{}: {arg_key} 必须是 BOSS/CREATOR/MANAGER/MEMBER",
self.name()
))
})?;
body[wire_key] = json!(permission);
patch_count += 1;
}
}
if patch_count == 0 {
return Err(ImError::Parse(format!(
"{}: 至少提供一个权限字段",
self.name()
)));
}
Ok(("channel/change/permission", body))
}
}
static CHANGE_PERMISSION: ChangePermissionCommand = ChangePermissionCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_change_permission",
command: &CHANGE_PERMISSION,
}
}
#[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 change_permission_dispatch_and_body() {
assert!(is_outbound("im_channel_change_permission"));
let corr = Correlation::from_raw(1);
let payload = serde_json::to_vec(&json!({
"channel_id": "c1",
"mention_permission": "CREATOR",
"top_permission": "MEMBER",
}))
.unwrap();
let effects = handle_outbound(
"im_channel_change_permission",
&payload,
"http://h/api",
"http://h",
Some("conn1"),
corr,
)
.expect("change_permission should dispatch");
match &effects[0] {
Effect::Http { req, .. } => {
assert!(
req.url.ends_with("/channel/change/permission"),
"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["mentionPermission"], "CREATOR");
assert!(body.get("noticePermission").is_none());
assert_eq!(body["topPermission"], "MEMBER");
let legacy_version_key = format!("{}{}", "expected", "Version");
assert!(body.get(&legacy_version_key).is_none());
}
other => panic!("expected Http, got {other:?}"),
}
}
#[test]
fn change_permission_missing_field_errors() {
let corr = Correlation::from_raw(2);
let payload = serde_json::to_vec(&json!({
"channel_id": "c1",
}))
.unwrap();
assert!(handle_outbound(
"im_channel_change_permission",
&payload,
"http://h/api",
"http://h",
None,
corr,
)
.is_err());
for invalid in [json!({"channel_id": "c1", "mention_permission": "OWNER"})] {
assert!(handle_outbound(
"im_channel_change_permission",
&serde_json::to_vec(&invalid).unwrap(),
"http://h/api",
"http://h",
None,
corr,
)
.is_err());
}
}
#[test]
fn change_notice_requires_text_without_version_condition() {
let corr = Correlation::from_raw(3);
let payload = serde_json::to_vec(&json!({
"channel_id": "c1",
"notice": {"text": "公告"},
}))
.unwrap();
let effects = handle_outbound(
"im_channel_change_notice",
&payload,
"http://h/api",
"http://h",
None,
corr,
)
.expect("notice update should dispatch");
let Effect::Http { req, .. } = &effects[0] else {
panic!("expected HTTP effect")
};
let body: serde_json::Value = serde_json::from_slice(req.body.as_ref().unwrap()).unwrap();
assert_eq!(
body,
json!({
"id": "c1",
"notice": {"text": "公告"},
})
);
for invalid in [
json!({"channel_id": "c1", "notice": {"message": "公告"}}),
json!({"channel_id": "c1"}),
] {
assert!(
handle_outbound(
"im_channel_change_notice",
serde_json::to_vec(&invalid).unwrap().as_slice(),
"http://h/api",
"http://h",
None,
corr,
)
.is_err(),
"invalid notice intent unexpectedly dispatched: {invalid}"
);
}
}
}