pickcat_api_collection/lib.rs
1//! # pickcat-api-collection
2//!
3//! Pickcat 社区 API(`https://cdsq.dao3.fun/api/v1`)的异步 Rust 客户端,基于
4//! [`reqwest`]。接口文档见 <https://pickcat-docs.xiaole6324.fun>。
5//!
6//! 所有能力都挂在 [`PickcatAccound`] 上,按章节拆成 trait:
7//!
8//! | 模块 | trait | 内容 |
9//! | --- | --- | --- |
10//! | [`auth`] | [`auth::UserBehavior`] | 会话、注册、邮箱验证、入站考试 |
11//! | [`user`] | [`user::UserProfileBehavior`] | 用户资料、邮箱、关注/粉丝、主题/回帖、徽章、等级、配额等 |
12//! | [`topic`] | [`topic::TopicBehavior`] | 主题列表/推荐/详情、楼层、发布、投稿审核 |
13//! | [`tag`] | [`tag::TagBehavior`] | 分区标签、侧边子标签 |
14//! | [`notification`] | [`notification::NotificationBehavior`] | 通知未读汇总 |
15//! | [`reading_session`] | [`reading_session::ReadingSessionBehavior`] | 阅读埋点上报 |
16//! | [`media`] | [`media::MediaBehavior`] | 文件上传/读取、头像、表情 |
17//!
18//! ## 快速开始
19//!
20//! ```no_run
21//! use pickcat_api_collection::auth::UserBehavior;
22//! use pickcat_api_collection::PickcatAccound;
23//!
24//! #[tokio::main]
25//! async fn main() -> Result<(), pickcat_api_collection::Error> {
26//! // 登录;后续请求自动携带会话 Cookie
27//! let account = PickcatAccound::new("you@example.com", "password").await?;
28//!
29//! let session = account.get_current_user_session().await?;
30//! println!(
31//! "{} Lv.{}",
32//! session.user.username, session.user.level.current
33//! );
34//! Ok(())
35//! }
36//! ```
37//!
38//! ## 鉴权
39//!
40//! Pickcat 使用 Session(Cookie)而不是 JWT:登录成功后凭据由内部带 cookie
41//! store 的 [`reqwest::Client`] 保存,调用方无需手动传 token。
42//!
43//! ## 错误
44//!
45//! 所有方法返回 [`Result`],错误类型为 [`Error`](请求失败 [`Error::Reqwest`]
46//! 或响应反序列化失败 [`Error::Serde`])。
47//!
48//! ## 写接口
49//!
50//! `POST /posts`、`POST /files` 等写接口需要 `Idempotency-Key`
51//! 请求头,可用 [`topic::generate_idempotency_key`] 生成。
52
53use std::time::Duration;
54
55use anyhow::Result;
56use reqwest::Client;
57
58use crate::dto::LoginDTO;
59
60/// 调用 API 时可能出现的错误。
61#[derive(thiserror::Error, Debug)]
62pub enum Error {
63 /// 网络请求或响应读取失败。
64 #[error("RequestError!")]
65 Reqwest(#[from] reqwest::Error),
66 /// 响应体无法按预期 DTO 反序列化。
67 #[error("ParseError")]
68 Serde(#[from] serde_json::Error),
69}
70
71pub mod auth;
72pub mod dto;
73pub mod media;
74pub mod notification;
75pub mod reading_session;
76pub mod tag;
77pub mod topic;
78pub mod user;
79const BASE_URL: &str = "https://cdsq.dao3.fun";
80
81/// 已登录的 Pickcat 账号,是访问所有接口的入口。
82///
83/// 构造时即完成登录并保存会话 Cookie;各章节的方法由对应 trait 提供
84/// (见 crate 文档的表格)。
85#[derive(Debug)]
86pub struct PickcatAccound {
87 pub username: String,
88 pub password: String,
89 pub client: Client,
90 /// API 根地址;默认 `https://cdsq.dao3.fun`,测试时可注入 mock server。
91 pub base_url: String,
92}
93
94impl PickcatAccound {
95 /// 使用默认 API 根地址登录。
96 pub async fn new(username: &str, password: &str) -> Result<Self, Error> {
97 Self::with_base_url(username, password, BASE_URL).await
98 }
99
100 /// 与 [`PickcatAccound::new`] 相同,但可指定 API 根地址。
101 ///
102 /// 主要用于测试:把 `base_url` 指向 mock server 即可离线验证请求。
103 pub async fn with_base_url(
104 username: &str,
105 password: &str,
106 base_url: &str,
107 ) -> Result<Self, Error> {
108 let client = Client::builder()
109 .connect_timeout(Duration::from_secs(30))
110 .cookie_store(true)
111 .build()?;
112 let dto = LoginDTO {
113 username: username.to_string(),
114 password: password.to_string(),
115 };
116 client
117 .post(format!("{}/api/v1/session", base_url))
118 .json(&dto)
119 .send()
120 .await?;
121 Ok(Self {
122 username: username.to_string(),
123 password: password.to_string(),
124 client: client,
125 base_url: base_url.to_string(),
126 })
127 }
128}