use std::{pin::Pin, sync::Arc};
use serde_json::{Value, json};
use crate::{
api::ws::{MessageHandler, WebSocketClient},
model::article::{ArticleDetail, ArticleList, ArticleListType, ArticlePost, ArticleType, Pagination},
utils::{ResponseResult, 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 = format!(
"api/articles/{}{}?p={}&size={}&apiKey={}",
base,
type_.to_code(),
page,
size,
self.api_key
);
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 = format!(
"api/articles/user/{}?p={}&size={}&apiKey={}",
user, page, size, self.api_key
);
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 = format!("api/article/{}?p={}&apiKey={}", id, p, self.api_key);
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 mut article_detail = ArticleDetail::from_value(&data["article"])?;
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["type"].as_i64().unwrap_or(0) == 0)
}
pub async fn thank(&self, id: &str) -> Result<ResponseResult, Error> {
let url = "article/thank".to_string();
let data = json!({
"apiKey": self.api_key,
"articleId": id,
});
let rsp = post(&url, Some(data)).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 = format!("article/reward?articleId={}", id);
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 = format!("api/article/heat/{}?apiKey={}", id, self.api_key);
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 = format!(
"wss://fishpi.cn/article-channel?apiKey={}&articleId={}&articleType={}",
self.api_key, id, type_ as u8
);
let handler = ArticleMessageHandler::new(callback);
let ws = WebSocketClient::connect(&url, handler)
.await
.map_err(|e| Error::Api(format!("WebSocket connection failed: {}", e)))?;
Ok(ws)
}
}