Skip to main content

fishpi_sdk/api/
notice.rs

1//! 通知 API 模块
2//!
3//! 这个模块提供了与通知相关的 API 操作,包括连接通知 WebSocket、监听通知事件、获取未读消息数、查询消息列表、标记已读等功能。
4//! 主要结构体是 `Notice`,用于管理通知的 WebSocket 连接和事件监听。
5//! 事件通过 `NoticeEventData` 枚举表示,支持通知消息类型。
6//!
7//! # 主要组件
8//!
9//! - [`Notice`] - 通知客户端结构体,负责连接、监听和管理通知。
10//! - [`NoticeHandler`] - 通知消息处理器,实现 `MessageHandler` trait,处理 WebSocket 消息并发射事件。
11//! - [`NoticeEventData`] - 通知事件数据枚举,包装通知消息。
12//! - [`NoticeListener`] - 通知事件监听器类型别名,定义监听器函数的签名。
13//!
14//! # 方法列表
15//!
16//! - [`Notice::new`] - 创建新的通知客户端实例。
17//! - [`Notice::connect`] - 连接通知 WebSocket。
18//! - [`Notice::reconnect`] - 重连通知 WebSocket。
19//! - [`Notice::on_notice`] - 监听通知消息事件。
20//! - [`Notice::off`] - 移除事件监听器。
21//! - [`Notice::disconnect`] - 断开连接。
22//! - [`Notice::count`] - 获取未读消息数。
23//! - [`Notice::list`] - 获取消息列表。
24//! - [`Notice::make_read`] - 已读指定类型消息。
25//! - [`Notice::read_all`] - 已读所有消息。
26//!
27//! # 示例
28//!
29//! ```rust,no_run
30//! use fishpi_sdk::api::notice::Notice;
31//! use fishpi_sdk::model::notice::NoticeMsg;
32//!
33//! #[tokio::main]
34//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
35//!     let mut notice = Notice::new("your_api_key".to_string());
36//!
37//!     // 监听通知消息(直接传递 NoticeMsg,无需 match)
38//!     notice.on_notice(|msg: NoticeMsg| {
39//!         println!("Received message: {}", msg.content.unwrap_or_default());
40//!     }).await;
41//!
42//!     // 连接通知
43//!     notice.connect(false).await?;
44//!
45//!     // 获取未读消息数
46//!     let count = notice.count().await?;
47//!     println!("Unread count: {:?}", count);
48//!
49//!     Ok(())
50//! }
51//! ```
52//!
53//! # 事件类型
54//!
55//! 通知支持以下事件类型(通过特定 `on_*` 方法监听):
56//!
57//! - `Msg` - 通知消息接收。
58
59use std::sync::Arc;
60
61use serde_json::Value;
62
63use crate::{
64    api::ws::{
65        ParsedMessageHandler, RetryPolicy, WebSocketError, WsConnection, WsLogHook, build_ws_url,
66    },
67    model::notice::{NoticeCount, NoticeItem, NoticeList, NoticeMsg, NoticeMsgType, NoticeType},
68    utils::{build_http_path, error::Error, get},
69};
70
71const DOMAIN: &str = "fishpi.cn";
72
73/// 通知项联合类型
74#[derive(Clone, Debug)]
75pub enum NoticeEventData {
76    Msg(NoticeMsg),
77}
78
79/// 通知事件类型枚举
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81pub enum NoticeEventType {
82    Msg,
83}
84
85pub type NoticeListener = Arc<dyn Fn(NoticeEventData) + Send + Sync + 'static>;
86
87/// 消息处理器
88pub type NoticeHandler = ParsedMessageHandler<NoticeEventType, NoticeEventData>;
89
90/// 解析通知消息,返回(事件类型,事件数据)
91#[allow(non_snake_case)]
92fn parse_notice_message(data: &Value) -> Result<(NoticeEventType, NoticeEventData), Error> {
93    let command = data
94        .get("command")
95        .and_then(|v| v.as_str())
96        .or_else(|| data.get("data").and_then(|v| v.get("command")).and_then(|v| v.as_str()))
97        .ok_or_else(|| Error::Parse("Missing command field".to_string()))?;
98
99    if NoticeMsgType::values().contains(&command) {
100        let msg = NoticeMsg::from_value(data)
101            .or_else(|_| data.get("data").ok_or_else(|| Error::Parse("Missing data field".to_string())).and_then(NoticeMsg::from_value))?;
102        Ok((NoticeEventType::Msg, NoticeEventData::Msg(msg)))
103    } else {
104        Err(Error::Parse(format!("Unsupported command: {}", command)))
105    }
106}
107
108/// 通知客户端
109pub struct Notice {
110    connection: WsConnection,
111    handler: NoticeHandler,
112    api_key: String,
113}
114
115impl Notice {
116    pub fn new(api_key: String) -> Self {
117        Self {
118            connection: WsConnection::new(),
119            handler: NoticeHandler::new(parse_notice_message, None, "notice"),
120            api_key,
121        }
122    }
123
124    fn ws_url(&self) -> Result<String, WebSocketError> {
125        build_ws_url(
126            DOMAIN,
127            "user-channel",
128            &[("apiKey", self.api_key.clone())],
129        )
130    }
131
132    pub async fn connect(&mut self, reload: bool) -> Result<(), WebSocketError> {
133        let url = self.ws_url()?;
134        self.connection
135            .connect(reload, &url, self.handler.clone())
136            .await
137    }
138
139    /// 重连
140    pub async fn reconnect(&mut self) -> Result<(), WebSocketError> {
141        let url = self.ws_url()?;
142        self.connection.reconnect(&url, self.handler.clone()).await
143    }
144
145    pub fn set_reconnect_policy(&mut self, policy: RetryPolicy) {
146        self.connection.set_retry_policy(policy);
147    }
148
149    pub fn on_ws_log<F>(&mut self, hook: F)
150    where
151        F: Fn(&str) + Send + Sync + 'static,
152    {
153        let hook = Arc::new(hook) as WsLogHook;
154        self.connection.set_log_hook_arc(hook.clone());
155        self.handler.set_log_hook_arc(hook);
156    }
157
158    /// 移除监听
159    pub async fn off(&self, event_type: NoticeEventType) {
160        self.handler
161            .get_emitter()
162            .remove_listener(Some(event_type))
163            .await;
164    }
165
166    /// 监听通知消息事件
167    pub async fn on_notice<F>(&self, listener: F)
168    where
169        F: Fn(NoticeMsg) + Send + Sync + 'static,
170    {
171        self.add_listener(NoticeEventType::Msg, move |event: NoticeEventData| {
172            let NoticeEventData::Msg(msg) = event;
173            listener(msg);
174        }).await;
175    }
176
177    async fn add_listener<F>(&self, event: NoticeEventType, listener: F)
178    where
179        F: Fn(NoticeEventData) + Send + Sync + 'static,
180    {
181        self.handler.get_emitter().add_listener(event, listener).await;
182    }
183
184    /// 断开连接
185    pub fn disconnect(&mut self) {
186        self.connection.disconnect();
187    }
188
189    /// 获取未读消息数
190    ///
191    /// 返回 [NoticeCount]
192    pub async fn count(&self) -> Result<NoticeCount, Error> {
193        let url = build_http_path(
194            "notifications/unread/count",
195            &[("apiKey", self.api_key.clone())],
196        );
197        let resp = get(&url).await?;
198        let count = NoticeCount::from_value(&resp)?;
199
200        Ok(count)
201    }
202
203    /// 获取消息列表
204    ///
205    /// * `type` 消息类型
206    ///
207    /// 返回消息列表
208    pub async fn list(&self, notice_type: NoticeType) -> Result<NoticeList, Error> {
209        let url = build_http_path(
210            "api/getNotifications",
211            &[
212                ("apiKey", self.api_key.clone()),
213                ("type", notice_type.as_str().to_string()),
214            ],
215        );
216        let resp = get(&url).await?;
217
218        let data_array = resp["data"]
219            .as_array()
220            .ok_or_else(|| Error::Api("Data is not an array".to_string()))?;
221        let list: Vec<NoticeItem> = data_array
222            .iter()
223            .map(|item| NoticeItem::from_value(item, &notice_type))
224            .collect::<Result<Vec<_>, _>>()?;
225        Ok(list)
226    }
227
228    /// 已读指定类型消息
229    ///
230    /// - `type` 消息类型
231    ///
232    /// 返回执行结果
233    pub async fn make_read(&self, notice_type: NoticeType) -> Result<bool, Error> {
234        let url = build_http_path(
235            &format!("notifications/make-read/{}", notice_type.as_str()),
236            &[("apiKey", self.api_key.clone())],
237        );
238        let resp = get(&url).await?;
239
240        if let Some(code) = resp["code"].as_i64()
241            && code != 0
242        {
243            return Err(Error::Api(
244                resp["msg"].as_str().unwrap_or("Api error").to_string(),
245            ));
246        }
247
248        Ok(true)
249    }
250
251    /// 已读所有消息
252    pub async fn read_all(&self) -> Result<bool, Error> {
253        let url = build_http_path("notifications/all-read", &[("apiKey", self.api_key.clone())]);
254        let resp = get(&url).await?;
255        if let Some(code) = resp["code"].as_i64()
256            && code != 0
257        {
258            return Err(Error::Api(
259                resp["msg"].as_str().unwrap_or("Api error").to_string(),
260            ));
261        }
262        Ok(true)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::{NoticeEventData, NoticeEventType, parse_notice_message};
269    use serde_json::json;
270
271    #[test]
272    fn parse_notice_warn_broadcast() {
273        let payload = json!({
274            "command": "warnBroadcast",
275            "userId": "u1",
276            "warnBroadcastText": "hello",
277            "who": "system"
278        });
279
280        let (event_type, event) = parse_notice_message(&payload).expect("should parse");
281        assert!(matches!(event_type, NoticeEventType::Msg));
282        match event {
283            NoticeEventData::Msg(msg) => assert_eq!(msg.content.as_deref(), Some("hello")),
284        }
285    }
286
287    #[test]
288    fn parse_notice_unsupported_command_fails() {
289        let payload = json!({
290            "command": "unknown",
291            "userId": "u1"
292        });
293
294        assert!(parse_notice_message(&payload).is_err());
295    }
296}