Skip to main content

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}