use serde_json::{json, Value};
use crate::error::ImError;
use crate::outbound::registry::{require_str, OutboundCommand, OutboundRegistration};
struct ChannelLeaveCommand;
impl OutboundCommand for ChannelLeaveCommand {
fn name(&self) -> &'static str {
"im_channel_leave"
}
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 });
if let Some(uid) = args.get("user_id").and_then(Value::as_str) {
b["userId"] = json!(uid);
}
if let Some(nc) = args.get("new_creator") {
if !nc.is_null() {
b["newCreator"] = nc.clone();
}
}
Ok(("channel/member/leave", b))
}
}
static CHANNEL_LEAVE: ChannelLeaveCommand = ChannelLeaveCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_leave",
command: &CHANNEL_LEAVE,
}
}
struct CreateChannelCommand;
impl OutboundCommand for CreateChannelCommand {
fn name(&self) -> &'static str {
"im_create_channel"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(
args,
&[
"type",
"user_ids",
"display_name",
"force_create",
"orient",
"source",
"req_id",
],
self.name(),
)?;
let channel_type = require_str(args, "type", self.name())?;
let user_ids = require_non_empty_string_array(args, "user_ids", self.name())?;
let display_name = require_str(args, "display_name", self.name())?;
let force_create = require_bool(args, "force_create", self.name())?;
if args.get("req_id").is_some() {
require_str(args, "req_id", self.name())?;
}
let mut body = json!({
"type": channel_type,
"userIds": user_ids,
"displayName": display_name,
"forceCreate": force_create,
});
if let Some(orient) = args.get("orient") {
let orient = orient
.as_str()
.ok_or_else(|| ImError::Parse(format!("{}: orient 必须为字符串", self.name())))?;
if orient.chars().count() > 30 {
return Err(ImError::Parse(format!(
"{}: orient 最多 30 个字符",
self.name()
)));
}
body["orient"] = json!(orient);
}
if let Some(source) = args.get("source") {
body["source"] = require_channel_source(source, self.name())?;
}
Ok(("channel/create", body))
}
}
fn require_channel_source(source: &Value, cmd: &str) -> Result<Value, ImError> {
let object = source
.as_object()
.ok_or_else(|| ImError::Parse(format!("{cmd}: source 必须为 object")))?;
if object.len() != 3
|| object
.keys()
.any(|key| !["type", "id", "title"].contains(&key.as_str()))
{
return Err(ImError::Parse(format!(
"{cmd}: source 只允许 type/id/title"
)));
}
for key in ["type", "id", "title"] {
require_str(source, key, cmd)?;
}
Ok(source.clone())
}
static CREATE_CHANNEL: CreateChannelCommand = CreateChannelCommand;
inventory::submit! {
OutboundRegistration {
name: "im_create_channel",
command: &CREATE_CHANNEL,
}
}
struct MakeTopicCommand;
impl OutboundCommand for MakeTopicCommand {
fn name(&self) -> &'static str {
"im_make_topic"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(
args,
&["root_id", "user_ids", "display_name", "req_id"],
self.name(),
)?;
let root_id = require_str(args, "root_id", self.name())?;
let user_ids = require_non_empty_string_array(args, "user_ids", self.name())?;
require_str(args, "req_id", self.name())?;
let mut body = json!({
"rootId": root_id,
"userIds": user_ids,
});
if args.get("display_name").is_some() {
let display_name = require_str(args, "display_name", self.name())?;
body["displayName"] = serde_json::Value::String(display_name.to_string());
}
Ok(("posts/makeTopic", body))
}
}
static MAKE_TOPIC: MakeTopicCommand = MakeTopicCommand;
inventory::submit! {
OutboundRegistration {
name: "im_make_topic",
command: &MAKE_TOPIC,
}
}
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_string_fields(args: &Value, fields: &[&str], cmd: &str) -> Result<(), ImError> {
for field in fields {
if args.get(*field).and_then(Value::as_str).is_none() {
return Err(ImError::Parse(format!("{cmd}: 缺/类型错误字段 '{field}'")));
}
}
Ok(())
}
fn require_string_array(args: &Value, key: &str, cmd: &str) -> Result<Vec<String>, ImError> {
let values = args
.get(key)
.and_then(Value::as_array)
.ok_or_else(|| ImError::Parse(format!("{cmd}: 缺/类型错误数组字段 '{key}'")))?;
values
.iter()
.map(|value| {
value
.as_str()
.filter(|value| !value.trim().is_empty())
.map(str::to_string)
.ok_or_else(|| ImError::Parse(format!("{cmd}: 字段 '{key}' 必须是字符串数组")))
})
.collect()
}
fn require_non_empty_string_array(args: &Value, key: &str, cmd: &str) -> Result<Value, ImError> {
let values = args
.get(key)
.and_then(Value::as_array)
.filter(|values| !values.is_empty())
.ok_or_else(|| ImError::Parse(format!("{cmd}: 缺/空必填数组字段 '{key}'")))?;
if values
.iter()
.any(|value| value.as_str().is_none_or(|value| value.trim().is_empty()))
{
return Err(ImError::Parse(format!(
"{cmd}: 字段 '{key}' 必须是非空字符串数组"
)));
}
Ok(Value::Array(values.clone()))
}
fn require_bool(args: &Value, key: &str, cmd: &str) -> Result<bool, ImError> {
args.get(key)
.and_then(Value::as_bool)
.ok_or_else(|| ImError::Parse(format!("{cmd}: 缺/类型错误的必填布尔字段 '{key}'")))
}
struct MemberChangeCommand;
impl OutboundCommand for MemberChangeCommand {
fn name(&self) -> &'static str {
"im_channel_member_change"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(
args,
&[
"channel_id",
"team_id",
"self_id",
"join_user_ids",
"leave_user_ids",
"req_id",
],
self.name(),
)?;
require_string_fields(args, &["channel_id", "team_id", "self_id"], self.name())?;
if args.get("req_id").is_some() {
require_str(args, "req_id", self.name())?;
}
let channel_id = require_str(args, "channel_id", self.name())?;
let team_id = args.get("team_id").and_then(Value::as_str).unwrap_or("");
let self_id = args.get("self_id").and_then(Value::as_str).unwrap_or("");
let join_user_ids = require_string_array(args, "join_user_ids", self.name())?;
let leave_user_ids = require_string_array(args, "leave_user_ids", self.name())?;
let build_users = |ids: &[String], exclude_self: bool| -> Vec<Value> {
ids.iter()
.filter(|uid| !(exclude_self && uid.as_str() == self_id))
.map(|uid| json!({ "id": uid, "teamId": team_id, "role": "MEMBER" }))
.collect()
};
let mut body = serde_json::Map::new();
body.insert("channelId".into(), Value::String(channel_id.to_string()));
let joins = build_users(&join_user_ids, true);
if !joins.is_empty() {
body.insert("joinUsers".into(), Value::Array(joins));
}
let leaves = build_users(&leave_user_ids, false);
if !leaves.is_empty() {
body.insert("leaveUsers".into(), Value::Array(leaves));
}
Ok(("channel/member/change", Value::Object(body)))
}
}
static MEMBER_CHANGE: MemberChangeCommand = MemberChangeCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_member_change",
command: &MEMBER_CHANGE,
}
}
struct ChannelCloseCommand;
impl OutboundCommand for ChannelCloseCommand {
fn name(&self) -> &'static str {
"im_channel_close"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
let channel_id = require_str(args, "channel_id", self.name())?;
Ok(("channel/close", json!({ "channelId": channel_id })))
}
}
static CHANNEL_CLOSE: ChannelCloseCommand = ChannelCloseCommand;
inventory::submit! {
OutboundRegistration {
name: "im_channel_close",
command: &CHANNEL_CLOSE,
}
}
struct UpdateNicknameCommand;
impl OutboundCommand for UpdateNicknameCommand {
fn name(&self) -> &'static str {
"im_update_member_nickname"
}
fn build(&self, args: &Value) -> Result<(&'static str, Value), ImError> {
require_exact_keys(
args,
&["channel_id", "user_id", "nickname", "req_id"],
self.name(),
)?;
require_string_fields(args, &["channel_id", "user_id", "nickname"], self.name())?;
if args.get("req_id").is_some() {
require_str(args, "req_id", self.name())?;
}
let channel_id = require_str(args, "channel_id", self.name())?;
let nickname = args.get("nickname").and_then(Value::as_str).unwrap_or("");
let user_id = args.get("user_id").and_then(Value::as_str).unwrap_or("");
let b = json!({ "channelId": channel_id, "userId": user_id, "nickname": nickname });
Ok(("channel/member/change/nickname", b))
}
}
static UPDATE_NICKNAME: UpdateNicknameCommand = UpdateNicknameCommand;
inventory::submit! {
OutboundRegistration {
name: "im_update_member_nickname",
command: &UPDATE_NICKNAME,
}
}