use std::{pin::Pin, sync::Arc};
use serde_json::{Value, json};
use crate::{
api::ws::{MessageHandler, WebSocketClient, build_ws_url},
model::article::{
ArticleDetail, ArticleList, ArticleListType, ArticlePost, ArticleType, Pagination,
},
utils::{ResponseResult, build_http_path, error::Error, get, post},
};
pub type ArticleListener = Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync + 'static>;
pub struct ArticleMessageHandler {
callback: ArticleListener,
}
impl ArticleMessageHandler {
pub fn new(callback: ArticleListener) -> Self {
Self { callback }
}
}
impl MessageHandler for ArticleMessageHandler {
fn handle_message(&self, msg: String) {
let callback = Arc::clone(&self.callback);
let msg = msg.clone();
tokio::spawn(async move {
if let Ok(json) = serde_json::from_str::<Value>(&msg) {
callback(json).await;
} else {
callback(Value::String(msg)).await;
}
});
}
}
pub struct Article {
api_key: String,
}
impl Article {
pub fn new(api_key: String) -> Self {
Self { api_key }
}
pub async fn post_article(&self, data: &ArticlePost) -> Result<String, Error> {
let url = "article".to_string();
let mut data_json = data.to_json()?;
data_json["apiKey"] = Value::String(self.api_key.clone());
let resp = post(&url, Some(data_json)).await?;
if resp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
return Err(Error::Api(
resp["msg"].as_str().unwrap_or("API error").to_string(),
));
}
let article_id = resp["articleId"]
.as_str()
.ok_or_else(|| Error::Api("Missing articleId in response".to_string()))?
.to_string();
Ok(article_id)
}
pub async fn update_article(&self, id: &str, data: &ArticlePost) -> Result<String, Error> {
let url = format!("article/{}", id);
let mut data_json = data.to_json()?;
data_json["apiKey"] = Value::String(self.api_key.clone());
let resp = post(&url, Some(data_json)).await?;
if resp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
return Err(Error::Api(
resp["msg"].as_str().unwrap_or("API error").to_string(),
));
}
let article_id = resp["articleId"]
.as_str()
.ok_or_else(|| Error::Api("Missing articleId in response".to_string()))?
.to_string();
Ok(article_id)
}
pub async fn list(
&self,
type_: ArticleListType,
page: u32,
size: u32,
tag: Option<&str>,
) -> Result<ArticleList, Error> {
let base = if let Some(tag) = tag {
format!("tag/{}", tag)
} else {
"recent".to_string()
};
let url = build_http_path(
&format!("api/articles/{}{}", base, type_.to_code()),
&[
("p", page.to_string()),
("size", size.to_string()),
("apiKey", self.api_key.clone()),
],
);
let rsp = get(&url).await?;
if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
return Err(Error::Api(
rsp["msg"].as_str().unwrap_or("API error").to_string(),
));
}
ArticleList::from_value(&rsp["data"])
}
pub async fn list_by_user(
&self,
user: &str,
page: u32,
size: u32,
) -> Result<ArticleList, Error> {
let url = build_http_path(
&format!("api/articles/user/{}", user),
&[
("p", page.to_string()),
("size", size.to_string()),
("apiKey", self.api_key.clone()),
],
);
let rsp = get(&url).await?;
if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
return Err(Error::Api(
rsp["msg"].as_str().unwrap_or("API error").to_string(),
));
}
ArticleList::from_value(&rsp["data"])
}
pub async fn detail(&self, id: &str, p: u32) -> Result<ArticleDetail, Error> {
let url = build_http_path(
&format!("api/article/{}", id),
&[("p", p.to_string()), ("apiKey", self.api_key.clone())],
);
let rsp = get(&url).await?;
if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
return Err(Error::Api(
rsp["msg"].as_str().unwrap_or("API error").to_string(),
));
}
let data = &rsp["data"];
let article_node = &data["article"];
let mut article_detail = ArticleDetail::from_value(article_node)?;
article_detail.pagination = Some(Pagination::from_value(&data["pagination"])?);
Ok(article_detail)
}
pub async fn vote(&self, id: &str, like: bool) -> Result<bool, Error> {
let url = format!("vote/{}/article", if like { "up" } else { "down" });
let data = json!({
"dataId": id,
"apiKey": self.api_key,
});
let rsp = post(&url, Some(data)).await?;
if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
return Err(Error::Api(
rsp["msg"].as_str().unwrap_or("API error").to_string(),
));
}
Ok(rsp.get("type").and_then(|v| v.as_i64()) == Some(-1))
}
pub async fn thank(&self, id: &str) -> Result<ResponseResult, Error> {
let url = build_http_path(
"article/thank",
&[
("articleId", id.to_string()),
("apiKey", self.api_key.clone()),
],
);
let rsp = post(&url, None).await?;
ResponseResult::from_value(&rsp)
}
pub async fn follow(&self, id: &str) -> Result<ResponseResult, Error> {
let url = "follow/article".to_string();
let data = json!({
"apiKey": self.api_key,
"followingId": id,
});
let rsp = post(&url, Some(data)).await?;
ResponseResult::from_value(&rsp)
}
pub async fn watch(&self, following_id: &str) -> Result<ResponseResult, Error> {
let url = "follow/article-watch".to_string();
let data = json!({
"apiKey": self.api_key,
"followingId": following_id,
});
let rsp = post(&url, Some(data)).await?;
ResponseResult::from_value(&rsp)
}
pub async fn reward(&self, id: &str) -> Result<ResponseResult, Error> {
let url = build_http_path("article/reward", &[("articleId", id.to_string())]);
let data = json!({
"apiKey": self.api_key,
});
let rsp = post(&url, Some(data)).await?;
ResponseResult::from_value(&rsp)
}
pub async fn heat(&self, id: &str) -> Result<u32, Error> {
let url = build_http_path(
&format!("api/article/heat/{}", id),
&[("apiKey", self.api_key.clone())],
);
let rsp = get(&url).await?;
if rsp.get("code").and_then(|c| c.as_i64()).unwrap_or(-1) != 0 {
return Err(Error::Api(
rsp["msg"].as_str().unwrap_or("API error").to_string(),
));
}
let heat = rsp["articleHeat"]
.as_u64()
.ok_or_else(|| Error::Api("Missing heat data in response".to_string()))?
as u32;
Ok(heat)
}
pub async fn add_listener(
&self,
id: &str,
type_: ArticleType,
callback: ArticleListener,
) -> Result<WebSocketClient, Error> {
let url = build_ws_url(
"fishpi.cn",
"article-channel",
&[
("apiKey", self.api_key.clone()),
("articleId", id.to_string()),
("articleType", (type_ as u8).to_string()),
],
)
.map_err(|e| Error::Api(format!("WebSocket URL build failed: {}", e)))?;
let handler = ArticleMessageHandler::new(callback);
let ws = WebSocketClient::connect(&url, handler)
.await
.map_err(|e| Error::Api(format!("WebSocket connection failed: {}", e)))?;
Ok(ws)
}
}