use crate::api::BotApi;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Debug, Clone, Serialize)]
pub struct GroupManageEvent {
#[serde(skip)]
api: BotApi,
pub event_id: Option<String>,
pub timestamp: Option<u64>,
pub group_openid: Option<String>,
pub op_member_openid: Option<String>,
}
impl GroupManageEvent {
pub fn new(
api: BotApi,
event_id: Option<String>,
data: &HashMap<String, serde_json::Value>,
) -> Self {
Self {
api,
event_id,
timestamp: data.get("timestamp").and_then(|v| v.as_u64()),
group_openid: data
.get("group_openid")
.and_then(|v| v.as_str())
.map(String::from),
op_member_openid: data
.get("op_member_openid")
.and_then(|v| v.as_str())
.map(String::from),
}
}
pub fn api(&self) -> &BotApi {
&self.api
}
pub fn formatted_timestamp(&self) -> Option<String> {
self.timestamp.map(|ts| {
let datetime = chrono::DateTime::from_timestamp(ts as i64, 0).unwrap_or_default();
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
})
}
}
impl std::fmt::Display for GroupManageEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"GroupManageEvent {{ event_id: {:?}, timestamp: {:?}, group_openid: {:?}, op_member_openid: {:?} }}",
self.event_id, self.timestamp, self.group_openid, self.op_member_openid
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct C2CFriendData {
pub openid: String,
#[serde(default)]
pub timestamp: u64,
#[serde(default)]
pub nick: String,
#[serde(default)]
pub avatar: String,
}
impl C2CFriendData {
pub fn new(data: &serde_json::Value) -> Self {
serde_json::from_value(data.clone()).unwrap_or_default()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct C2CManageEvent {
#[serde(skip)]
api: BotApi,
pub event_id: Option<String>,
pub timestamp: Option<u64>,
pub openid: Option<String>,
pub nick: Option<String>,
pub avatar: Option<String>,
}
impl C2CManageEvent {
pub fn new(
api: BotApi,
event_id: Option<String>,
data: &HashMap<String, serde_json::Value>,
) -> Self {
Self {
api,
event_id,
timestamp: data.get("timestamp").and_then(|v| v.as_u64()),
openid: data
.get("openid")
.and_then(|v| v.as_str())
.map(String::from),
nick: data.get("nick").and_then(|v| v.as_str()).map(String::from),
avatar: data
.get("avatar")
.and_then(|v| v.as_str())
.map(String::from),
}
}
pub fn api(&self) -> &BotApi {
&self.api
}
pub fn formatted_timestamp(&self) -> Option<String> {
self.timestamp.map(|ts| {
let datetime = chrono::DateTime::from_timestamp(ts as i64, 0).unwrap_or_default();
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
})
}
}
impl std::fmt::Display for C2CManageEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"C2CManageEvent {{ event_id: {:?}, timestamp: {:?}, openid: {:?}, nick: {:?}, avatar: {:?} }}",
self.event_id, self.timestamp, self.openid, self.nick, self.avatar
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct EnterAioEvent {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub user_openid: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub from_source: String,
#[serde(skip)]
pub event_id: Option<String>,
}
pub type EnterAIO = EnterAioEvent;
impl EnterAioEvent {
pub fn new(event_id: Option<String>, data: &serde_json::Value) -> Self {
let mut event = serde_json::from_value::<Self>(data.clone()).unwrap_or_default();
event.event_id = event_id;
event
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SubscribeMessageStatusData {
#[serde(default)]
pub group_openid: String,
#[serde(default)]
pub openid: String,
#[serde(default)]
pub result: Vec<SubscribeMsgTemplateResult>,
#[serde(skip)]
pub event_id: Option<String>,
}
impl SubscribeMessageStatusData {
pub fn new(event_id: Option<String>, data: &serde_json::Value) -> Self {
let mut event = serde_json::from_value::<Self>(data.clone()).unwrap_or_default();
event.event_id = event_id;
event
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SubscribeMsgTemplateResult {
#[serde(default)]
pub template_id: i32,
#[serde(default)]
pub custom_template_id: String,
#[serde(default)]
pub op: u32,
#[serde(default)]
pub subscribe_id: String,
#[serde(default)]
pub update_ts: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ManageEventType {
GroupAddRobot,
GroupDelRobot,
GroupMsgReject,
GroupMsgReceive,
FriendAdd,
FriendDel,
C2CMsgReject,
C2CMsgReceive,
}
impl ManageEventType {
pub fn as_str(&self) -> &'static str {
match self {
Self::GroupAddRobot => "group_add_robot",
Self::GroupDelRobot => "group_del_robot",
Self::GroupMsgReject => "group_msg_reject",
Self::GroupMsgReceive => "group_msg_receive",
Self::FriendAdd => "friend_add",
Self::FriendDel => "friend_del",
Self::C2CMsgReject => "c2c_msg_reject",
Self::C2CMsgReceive => "c2c_msg_receive",
}
}
pub fn is_group_event(&self) -> bool {
matches!(
self,
Self::GroupAddRobot
| Self::GroupDelRobot
| Self::GroupMsgReject
| Self::GroupMsgReceive
)
}
pub fn is_c2c_event(&self) -> bool {
matches!(
self,
Self::FriendAdd | Self::FriendDel | Self::C2CMsgReject | Self::C2CMsgReceive
)
}
}
impl FromStr for ManageEventType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"group_add_robot" => Ok(Self::GroupAddRobot),
"group_del_robot" => Ok(Self::GroupDelRobot),
"group_msg_reject" => Ok(Self::GroupMsgReject),
"group_msg_receive" => Ok(Self::GroupMsgReceive),
"friend_add" => Ok(Self::FriendAdd),
"friend_del" => Ok(Self::FriendDel),
"c2c_msg_reject" => Ok(Self::C2CMsgReject),
"c2c_msg_receive" => Ok(Self::C2CMsgReceive),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manage_event_type_from_str() {
assert_eq!(
"group_add_robot".parse::<ManageEventType>(),
Ok(ManageEventType::GroupAddRobot)
);
assert_eq!(
"friend_add".parse::<ManageEventType>(),
Ok(ManageEventType::FriendAdd)
);
assert_eq!("invalid".parse::<ManageEventType>(), Err(()));
}
#[test]
fn test_manage_event_type_as_str() {
assert_eq!(ManageEventType::GroupAddRobot.as_str(), "group_add_robot");
assert_eq!(ManageEventType::FriendAdd.as_str(), "friend_add");
}
#[test]
fn test_is_group_event() {
assert!(ManageEventType::GroupAddRobot.is_group_event());
assert!(!ManageEventType::FriendAdd.is_group_event());
}
#[test]
fn test_is_c2c_event() {
assert!(ManageEventType::FriendAdd.is_c2c_event());
assert!(!ManageEventType::GroupAddRobot.is_c2c_event());
}
#[test]
fn botgo_enter_aio_uses_zero_value_omitempty_shape() {
let event = EnterAioEvent::new(
Some("event-1".to_string()),
&serde_json::json!({
"user_openid": "user-1",
"from_source": "profile"
}),
);
assert_eq!(event.user_openid, "user-1");
assert_eq!(event.from_source, "profile");
assert_eq!(event.event_id.as_deref(), Some("event-1"));
let value = serde_json::to_value(&event).unwrap();
assert_eq!(
value,
serde_json::json!({
"user_openid": "user-1",
"from_source": "profile"
})
);
let empty = serde_json::to_value(EnterAioEvent::default()).unwrap();
assert_eq!(empty, serde_json::json!({}));
}
#[test]
fn botgo_subscribe_message_status_uses_required_zero_value_fields() {
let event = SubscribeMessageStatusData::new(
Some("event-1".to_string()),
&serde_json::json!({
"group_openid": "group-1",
"openid": "user-1",
"result": [{
"template_id": 1,
"custom_template_id": "custom-1",
"op": 2,
"subscribe_id": "sub-1",
"update_ts": 1710000000
}]
}),
);
assert_eq!(event.group_openid, "group-1");
assert_eq!(event.openid, "user-1");
assert_eq!(event.event_id.as_deref(), Some("event-1"));
assert_eq!(event.result[0].template_id, 1);
assert_eq!(event.result[0].custom_template_id, "custom-1");
assert_eq!(event.result[0].op, 2);
assert_eq!(event.result[0].subscribe_id, "sub-1");
assert_eq!(event.result[0].update_ts, 1_710_000_000);
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["group_openid"], "group-1");
assert_eq!(value["openid"], "user-1");
assert_eq!(value["result"][0]["template_id"], 1);
assert_eq!(value["result"][0]["custom_template_id"], "custom-1");
assert_eq!(value["result"][0]["op"], 2);
assert_eq!(value["result"][0]["subscribe_id"], "sub-1");
assert_eq!(value["result"][0]["update_ts"], 1_710_000_000_u64);
assert!(value.get("event_id").is_none());
let empty = serde_json::to_value(SubscribeMessageStatusData::default()).unwrap();
assert_eq!(empty["group_openid"], "");
assert_eq!(empty["openid"], "");
assert_eq!(empty["result"], serde_json::json!([]));
}
}