1use std::{pin::Pin, sync::Arc};
71
72use serde_json::{Value, json};
73
74use crate::{
75 api::ws::{MessageHandler, WebSocketClient, build_ws_url},
76 model::article::{
77 ArticleDetail, ArticleList, ArticleListType, ArticlePost, ArticleType, Pagination,
78 },
79 utils::{ResponseResult, build_http_path, error::Error, get, post},
80};
81
82pub type ArticleListener = Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync + 'static>;
84
85pub struct ArticleMessageHandler {
87 callback: ArticleListener,
88}
89
90impl ArticleMessageHandler {
91 pub fn new(callback: ArticleListener) -> Self {
92 Self { callback }
93 }
94}
95
96impl MessageHandler for ArticleMessageHandler {
97 fn handle_message(&self, msg: String) {
98 let callback = Arc::clone(&self.callback);
99 let msg = msg.clone();
100 tokio::spawn(async move {
101 if let Ok(json) = serde_json::from_str::<Value>(&msg) {
102 callback(json).await;
103 } else {
104 callback(Value::String(msg)).await;
105 }
106 });
107 }
108}
109
110pub struct Article {
111 api_key: String,
112}
113
114impl Article {
115 pub fn new(api_key: String) -> Self {
116 Self { api_key }
117 }
118
119 pub async fn post_article(&self, data: &ArticlePost) -> Result<String, Error> {
125 let url = "article".to_string();
126
127 let mut data_json = data.to_json()?;
128 data_json["apiKey"] = Value::String(self.api_key.clone());
129
130 let resp = post(&url, Some(data_json)).await?;
131
132 if resp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
133 return Err(Error::Api(
134 resp["msg"].as_str().unwrap_or("API error").to_string(),
135 ));
136 }
137
138 let article_id = resp["articleId"]
139 .as_str()
140 .ok_or_else(|| Error::Api("Missing articleId in response".to_string()))?
141 .to_string();
142
143 Ok(article_id)
144 }
145
146 pub async fn update_article(&self, id: &str, data: &ArticlePost) -> Result<String, Error> {
153 let url = format!("article/{}", id);
154
155 let mut data_json = data.to_json()?;
156 data_json["apiKey"] = Value::String(self.api_key.clone());
157
158 let resp = post(&url, Some(data_json)).await?;
159
160 if resp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
161 return Err(Error::Api(
162 resp["msg"].as_str().unwrap_or("API error").to_string(),
163 ));
164 }
165
166 let article_id = resp["articleId"]
167 .as_str()
168 .ok_or_else(|| Error::Api("Missing articleId in response".to_string()))?
169 .to_string();
170
171 Ok(article_id)
172 }
173
174 pub async fn list(
183 &self,
184 type_: ArticleListType,
185 page: u32,
186 size: u32,
187 tag: Option<&str>,
188 ) -> Result<ArticleList, Error> {
189 let base = if let Some(tag) = tag {
190 format!("tag/{}", tag)
191 } else {
192 "recent".to_string()
193 };
194
195 let url = build_http_path(
196 &format!("api/articles/{}{}", base, type_.to_code()),
197 &[
198 ("p", page.to_string()),
199 ("size", size.to_string()),
200 ("apiKey", self.api_key.clone()),
201 ],
202 );
203
204 let rsp = get(&url).await?;
205
206 if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
207 return Err(Error::Api(
208 rsp["msg"].as_str().unwrap_or("API error").to_string(),
209 ));
210 }
211
212 ArticleList::from_value(&rsp["data"])
213 }
214
215 pub async fn list_by_user(
223 &self,
224 user: &str,
225 page: u32,
226 size: u32,
227 ) -> Result<ArticleList, Error> {
228 let url = build_http_path(
229 &format!("api/articles/user/{}", user),
230 &[
231 ("p", page.to_string()),
232 ("size", size.to_string()),
233 ("apiKey", self.api_key.clone()),
234 ],
235 );
236
237 let rsp = get(&url).await?;
238
239 if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
240 return Err(Error::Api(
241 rsp["msg"].as_str().unwrap_or("API error").to_string(),
242 ));
243 }
244
245 ArticleList::from_value(&rsp["data"])
246 }
247
248 pub async fn detail(&self, id: &str, p: u32) -> Result<ArticleDetail, Error> {
255 let url = build_http_path(
256 &format!("api/article/{}", id),
257 &[("p", p.to_string()), ("apiKey", self.api_key.clone())],
258 );
259
260 let rsp = get(&url).await?;
261
262 if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
263 return Err(Error::Api(
264 rsp["msg"].as_str().unwrap_or("API error").to_string(),
265 ));
266 }
267
268 let data = &rsp["data"];
269 let article_node = &data["article"];
270 let mut article_detail = ArticleDetail::from_value(article_node)?;
271 article_detail.pagination = Some(Pagination::from_value(&data["pagination"])?);
272
273 Ok(article_detail)
274 }
275
276 pub async fn vote(&self, id: &str, like: bool) -> Result<bool, Error> {
283 let url = format!("vote/{}/article", if like { "up" } else { "down" });
284
285 let data = json!({
286 "dataId": id,
287 "apiKey": self.api_key,
288 });
289
290 let rsp = post(&url, Some(data)).await?;
291
292 if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
293 return Err(Error::Api(
294 rsp["msg"].as_str().unwrap_or("API error").to_string(),
295 ));
296 }
297
298 Ok(rsp.get("type").and_then(|v| v.as_i64()) == Some(-1))
299 }
300
301 pub async fn thank(&self, id: &str) -> Result<ResponseResult, Error> {
307 let url = build_http_path(
308 "article/thank",
309 &[
310 ("articleId", id.to_string()),
311 ("apiKey", self.api_key.clone()),
312 ],
313 );
314
315 let rsp = post(&url, None).await?;
316
317 ResponseResult::from_value(&rsp)
318 }
319
320 pub async fn follow(&self, id: &str) -> Result<ResponseResult, Error> {
326 let url = "follow/article".to_string();
327
328 let data = json!({
329 "apiKey": self.api_key,
330 "followingId": id,
331 });
332
333 let rsp = post(&url, Some(data)).await?;
334
335 ResponseResult::from_value(&rsp)
336 }
337
338 pub async fn watch(&self, following_id: &str) -> Result<ResponseResult, Error> {
344 let url = "follow/article-watch".to_string();
345
346 let data = json!({
347 "apiKey": self.api_key,
348 "followingId": following_id,
349 });
350
351 let rsp = post(&url, Some(data)).await?;
352
353 ResponseResult::from_value(&rsp)
354 }
355
356 pub async fn reward(&self, id: &str) -> Result<ResponseResult, Error> {
362 let url = build_http_path("article/reward", &[("articleId", id.to_string())]);
363
364 let data = json!({
365 "apiKey": self.api_key,
366 });
367
368 let rsp = post(&url, Some(data)).await?;
369
370 ResponseResult::from_value(&rsp)
371 }
372
373 pub async fn heat(&self, id: &str) -> Result<u32, Error> {
379 let url = build_http_path(
380 &format!("api/article/heat/{}", id),
381 &[("apiKey", self.api_key.clone())],
382 );
383
384 let rsp = get(&url).await?;
385
386 if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
387 return Err(Error::Api(
388 rsp["msg"].as_str().unwrap_or("API error").to_string(),
389 ));
390 }
391
392 let heat = rsp["articleHeat"]
393 .as_u64()
394 .ok_or_else(|| Error::Api("Missing heat data in response".to_string()))?
395 as u32;
396
397 Ok(heat)
398 }
399
400 pub async fn add_listener(
408 &self,
409 id: &str,
410 type_: ArticleType,
411 callback: ArticleListener,
412 ) -> Result<WebSocketClient, Error> {
413 let url = build_ws_url(
414 "fishpi.cn",
415 "article-channel",
416 &[
417 ("apiKey", self.api_key.clone()),
418 ("articleId", id.to_string()),
419 ("articleType", (type_ as u8).to_string()),
420 ],
421 )
422 .map_err(|e| Error::Api(format!("WebSocket URL build failed: {}", e)))?;
423
424 let handler = ArticleMessageHandler::new(callback);
425 let ws = WebSocketClient::connect(&url, handler)
426 .await
427 .map_err(|e| Error::Api(format!("WebSocket connection failed: {}", e)))?;
428
429 Ok(ws)
430 }
431}