1use 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#[derive(Clone, Debug)]
75pub enum NoticeEventData {
76 Msg(NoticeMsg),
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81pub enum NoticeEventType {
82 Msg,
83}
84
85pub type NoticeListener = Arc<dyn Fn(NoticeEventData) + Send + Sync + 'static>;
86
87pub type NoticeHandler = ParsedMessageHandler<NoticeEventType, NoticeEventData>;
89
90#[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
108pub 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 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 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 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 pub fn disconnect(&mut self) {
186 self.connection.disconnect();
187 }
188
189 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 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, ¬ice_type))
224 .collect::<Result<Vec<_>, _>>()?;
225 Ok(list)
226 }
227
228 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 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}