Skip to main content

fishpi_sdk/api/
article.rs

1//! 文章 API 模块
2//!
3//! 这个模块提供了与文章相关的 API 操作,包括发布、更新、查询、点赞、感谢、收藏、关注、打赏、获取在线人数和 WebSocket 监听等功能。
4//! 主要结构体是 `Article`,用于管理文章相关的 HTTP 请求和 WebSocket 连接。
5//! 事件通过 `ArticleListener` 回调处理,支持实时消息监听。
6//!
7//! # 主要组件
8//!
9//! - [`Article`] - 文章客户端结构体,负责所有文章相关的 API 调用和 WebSocket 连接。
10//! - [`ArticleMessageHandler`] - 文章消息处理器,实现 `MessageHandler` trait,处理 WebSocket 消息并异步调用回调。
11//! - [`ArticleListener`] - 文章监听器类型别名,定义异步监听器函数的签名,用于处理接收到的消息,支持多线程共享。
12//!
13//! # 方法列表
14//!
15//! - [`Article::new`] - 创建新的文章客户端实例。
16//! - [`Article::post_article`] - 发布新文章。
17//! - [`Article::update_article`] - 更新现有文章。
18//! - [`Article::list`] - 查询文章列表(支持类型、标签、分页)。
19//! - [`Article::list_by_user`] - 查询指定用户的文章列表。
20//! - [`Article::detail`] - 获取文章详情(包括评论分页)。
21//! - [`Article::vote`] - 点赞或点踩文章。
22//! - [`Article::thank`] - 感谢文章。
23//! - [`Article::follow`] - 收藏或取消收藏文章。
24//! - [`Article::watch`] - 关注或取消关注文章。
25//! - [`Article::reward`] - 打赏文章。
26//! - [`Article::heat`] - 获取文章在线人数。
27//! - [`Article::add_listener`] - 添加文章 WebSocket 监听器。
28//!
29//! # 示例
30//!
31//! ```rust,no_run
32//! use fishpi_sdk::api::article::{Article, ArticleListener};
33//! use fishpi_sdk::model::article::{ArticlePost, ArticleType};
34//! use serde_json::Value;
35//! use std::sync::Arc;
36//!
37//! #[tokio::main]
38//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
39//!     let article = Article::new("your_api_key".to_string());
40//!
41//!     let data = ArticlePost {
42//!         title: "Test Title".to_string(),
43//!         content: "Test Content".to_string(),
44//!         tags: "test".to_string(),
45//!         commentable: true,
46//!         notifyFollowers: false,
47//!         type_: ArticleType::Normal,
48//!         showInList: 1,
49//!         rewardContent: None,
50//!         rewardPoint: None,
51//!         anonymous: None,
52//!         offerPoint: None,
53//!     };
54//!     let article_id = article.post_article(&data).await?;
55//!     let detail = article.detail(&article_id, 1).await?;
56//!     println!("Article title: {}", detail.title);
57//!
58//!     let callback: ArticleListener = Arc::new(|msg: Value| {
59//!         Box::pin(async move {
60//!             println!("Received message: {:?}", msg);
61//!         })
62//!     });
63//!     let _ws_client = article
64//!         .add_listener(&article_id, ArticleType::Normal, Arc::clone(&callback))
65//!         .await?;
66//!
67//!     Ok(())
68//! }
69//! ```
70use 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
82/// 文章监听器类型
83pub type ArticleListener = Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync + 'static>;
84
85/// 文章消息处理器
86pub 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    /// 发布文章
120    ///
121    /// * `data` 文章信息 [ArticlePost]
122    ///
123    /// 返回文章 Id
124    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    /// 更新文章
147    ///
148    /// * `id` 文章 Id
149    /// * `data` 文章信息 [ArticlePost]
150    ///
151    /// 返回文章 Id
152    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    /// 查询文章列表
175    ///
176    /// * `type` 查询类型,来自 [ArticleListType]
177    /// * `tag` 指定查询标签,可选
178    /// * `page` 页码
179    /// * `size` 每页数量
180    ///
181    /// 返回文章列表
182    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    /// 查询文章列表
216    ///
217    /// - `user` 指定用户
218    /// - `page` 页码
219    /// - `size` 每页数量
220    ///
221    /// 返回文章列表
222    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    /// 获取文章详情
249    ///
250    /// - `id` 文章id
251    /// - `p` 评论页码
252    ///
253    /// 返回文章详情 [ArticleDetail]
254    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    /// 点赞/取消点赞文章
277    ///
278    /// - `id` 文章id
279    /// - `like` 点赞类型,true 为点赞,false 为点踩
280    ///
281    /// 返回文章点赞状态,true 为点赞,false 为点踩
282    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    /// 感谢文章
302    ///
303    /// - `id` 文章id
304    ///
305    /// 返回执行结果
306    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    /// 收藏/取消收藏文章
321    ///
322    /// - `id` 文章id
323    ///
324    /// 返回执行结果
325    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    /// 关注/取消关注文章
339    ///
340    /// - `followingId` 文章id
341    ///
342    /// 返回执行结果
343    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    /// 打赏文章
357    ///
358    /// - `id` 文章id
359    ///
360    /// 返回执行结果
361    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    /// 获取文章在线人数
374    ///
375    /// - `id` 文章id
376    ///
377    /// 返回在线人数
378    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    /// 添加文章监听器
401    ///
402    /// - `id` 文章id
403    /// - `type_` 文章类型
404    /// - `callback` 监听回调
405    ///
406    /// 返回 WebSocketClient
407    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}