Skip to main content

pickcat_api_collection/
user.rs

1//! 用户章节:资料、邮箱、关注/粉丝、主题/回帖、徽章、动态、等级、配额等。
2//!
3//! 参见 <https://pickcat-docs.xiaole6324.fun/user.html>。
4
5use crate::dto::user::*;
6use crate::{Error, PickcatAccound};
7
8/// User-chapter endpoints from the docs
9/// (<https://pickcat-docs.xiaole6324.fun/user.html>): profile, content,
10/// badges, activities, level and account quotas.
11///
12/// Registration / email verification and the exam flow live in [`crate::auth`].
13pub trait UserProfileBehavior {
14    /// `GET /api/v1/users/{userId}` — public, works with or without a session.
15    fn get_user_information(
16        &self,
17        user_id: &str,
18    ) -> impl std::future::Future<Output = Result<GetUserInformationDTO, Error>> + Send;
19
20    /// `PATCH /api/v1/users/{userId}` — requires a session; only the current
21    /// user may edit their own profile. `None` fields are left unchanged.
22    fn update_user_profile(
23        &self,
24        user_id: &str,
25        bio: Option<&str>,
26    ) -> impl std::future::Future<Output = Result<GetUserInformationDTO, Error>> + Send;
27
28    /// `GET /api/v1/users/{userId}/email` — requires a session and only works
29    /// for the current user.
30    fn get_user_email(
31        &self,
32        user_id: &str,
33    ) -> impl std::future::Future<Output = Result<GetUserEmailDTO, Error>> + Send;
34
35    /// `GET /api/v1/users/{userId}/following` — public, cursor-paginated.
36    fn get_user_following(
37        &self,
38        user_id: &str,
39        limit: Option<u32>,
40        cursor: Option<&str>,
41    ) -> impl std::future::Future<Output = Result<CursorListDTO<GetUserInformationDTO>, Error>> + Send;
42
43    /// `GET /api/v1/users/{userId}/followers` — public, cursor-paginated.
44    fn get_user_followers(
45        &self,
46        user_id: &str,
47        limit: Option<u32>,
48        cursor: Option<&str>,
49    ) -> impl std::future::Future<Output = Result<CursorListDTO<GetUserInformationDTO>, Error>> + Send;
50
51    /// `GET /api/v1/users/{userId}/topics` — public, cursor-paginated.
52    fn get_user_topics(
53        &self,
54        user_id: &str,
55        limit: Option<u32>,
56    ) -> impl std::future::Future<Output = Result<UserSubjectsDTO, Error>> + Send;
57
58    /// `GET /api/v1/users/{userId}/posts?role=reply` — public, cursor-paginated.
59    fn get_user_posts(
60        &self,
61        user_id: &str,
62        role: Option<&str>,
63        limit: Option<u32>,
64    ) -> impl std::future::Future<Output = Result<UserRepliesDTO, Error>> + Send;
65
66    /// `GET /api/v1/users/{userId}/featured-topics` — public, no pagination.
67    fn get_user_featured_topics(
68        &self,
69        user_id: &str,
70    ) -> impl std::future::Future<Output = Result<UserFeaturedTopicsDTO, Error>> + Send;
71
72    /// `GET /api/v1/users/{userId}/topic-collections` — public, paginated.
73    ///
74    /// Item shape is not documented yet, so items are kept as raw JSON.
75    fn get_user_topic_collections(
76        &self,
77        user_id: &str,
78        limit: Option<u32>,
79    ) -> impl std::future::Future<Output = Result<CursorListDTO<serde_json::Value>, Error>> + Send;
80
81    /// `GET /api/v1/users/{userId}/badges` — public.
82    fn get_user_badges(
83        &self,
84        user_id: &str,
85    ) -> impl std::future::Future<Output = Result<UserBadgesDTO, Error>> + Send;
86
87    /// `GET /api/v1/users/{userId}/badge-display` — public.
88    fn get_user_badge_display(
89        &self,
90        user_id: &str,
91    ) -> impl std::future::Future<Output = Result<BadgeDisplayDTO, Error>> + Send;
92
93    /// `GET /api/v1/user-badge-displays?userIds=a,b` — public bulk lookup.
94    fn get_user_badge_displays(
95        &self,
96        user_ids: &[String],
97    ) -> impl std::future::Future<Output = Result<UserBadgeDisplaysDTO, Error>> + Send;
98
99    /// `GET /api/v1/users/{userId}/profile-activities` — public, paginated.
100    fn get_user_profile_activities(
101        &self,
102        user_id: &str,
103        year: Option<u32>,
104        limit: Option<u32>,
105    ) -> impl std::future::Future<Output = Result<CursorListDTO<ProfileActivityDTO>, Error>> + Send;
106
107    /// `GET /api/v1/profile-activities` — requires a session.
108    fn get_my_profile_activities(
109        &self,
110        year: Option<u32>,
111        limit: Option<u32>,
112    ) -> impl std::future::Future<Output = Result<CursorListDTO<ProfileActivityDTO>, Error>> + Send;
113
114    /// `GET /api/v1/users/{userId}/level-contributions/{year}` — public.
115    fn get_user_level_contributions(
116        &self,
117        user_id: &str,
118        year: u32,
119    ) -> impl std::future::Future<Output = Result<LevelContributionsDTO, Error>> + Send;
120
121    /// `GET /api/v1/level-contributions/{year}` — requires a session.
122    fn get_my_level_contributions(
123        &self,
124        year: u32,
125    ) -> impl std::future::Future<Output = Result<LevelContributionsDTO, Error>> + Send;
126
127    /// `GET /api/v1/level-progress` — requires a session.
128    fn get_level_progress(
129        &self,
130    ) -> impl std::future::Future<Output = Result<LevelProgressDTO, Error>> + Send;
131
132    /// `GET /api/v1/content-length-limit` — requires a session.
133    fn get_content_length_limit(
134        &self,
135    ) -> impl std::future::Future<Output = Result<ContentLengthLimitDTO, Error>> + Send;
136
137    /// `GET /api/v1/topic-collection-usage` — requires a session.
138    fn get_topic_collection_usage(
139        &self,
140    ) -> impl std::future::Future<Output = Result<TopicCollectionUsageDTO, Error>> + Send;
141
142    /// `GET /api/v1/file-storage` — requires a session.
143    fn get_file_storage(
144        &self,
145    ) -> impl std::future::Future<Output = Result<FileStorageDTO, Error>> + Send;
146
147    /// `GET /api/v1/bookmarks` — requires a session, paginated.
148    ///
149    /// Item shape is not documented yet, so items are kept as raw JSON.
150    fn get_bookmarks(
151        &self,
152        limit: Option<u32>,
153    ) -> impl std::future::Future<Output = Result<CursorListDTO<serde_json::Value>, Error>> + Send;
154
155    /// `GET /api/v1/avatar-presets` — public.
156    fn get_avatar_presets(
157        &self,
158    ) -> impl std::future::Future<Output = Result<AvatarPresetsDTO, Error>> + Send;
159}
160
161/// Adds `limit` / `cursor`-style query parameters only when present.
162fn push_query(params: &mut Vec<(&str, String)>, key: &'static str, value: Option<String>) {
163    if let Some(value) = value {
164        params.push((key, value));
165    }
166}
167
168impl UserProfileBehavior for PickcatAccound {
169    async fn get_user_information(&self, user_id: &str) -> Result<GetUserInformationDTO, Error> {
170        Ok(self
171            .client
172            .get(format!("{}/api/v1/users/{}", self.base_url, user_id))
173            .send()
174            .await?
175            .json::<GetUserInformationDTO>()
176            .await?)
177    }
178
179    async fn update_user_profile(
180        &self,
181        user_id: &str,
182        bio: Option<&str>,
183    ) -> Result<GetUserInformationDTO, Error> {
184        let dto = UpdateUserProfileDTO {
185            bio: bio.map(str::to_string),
186        };
187        Ok(self
188            .client
189            .patch(format!("{}/api/v1/users/{}", self.base_url, user_id))
190            .json(&dto)
191            .send()
192            .await?
193            .json::<GetUserInformationDTO>()
194            .await?)
195    }
196
197    async fn get_user_email(&self, user_id: &str) -> Result<GetUserEmailDTO, Error> {
198        Ok(self
199            .client
200            .get(format!("{}/api/v1/users/{}/email", self.base_url, user_id))
201            .send()
202            .await?
203            .json::<GetUserEmailDTO>()
204            .await?)
205    }
206
207    async fn get_user_following(
208        &self,
209        user_id: &str,
210        limit: Option<u32>,
211        cursor: Option<&str>,
212    ) -> Result<CursorListDTO<GetUserInformationDTO>, Error> {
213        let mut params: Vec<(&str, String)> = Vec::new();
214        push_query(&mut params, "limit", limit.map(|v| v.to_string()));
215        push_query(&mut params, "cursor", cursor.map(str::to_string));
216
217        let mut request = self.client.get(format!(
218            "{}/api/v1/users/{}/following",
219            self.base_url, user_id
220        ));
221        if !params.is_empty() {
222            request = request.query(&params);
223        }
224        Ok(request
225            .send()
226            .await?
227            .json::<CursorListDTO<GetUserInformationDTO>>()
228            .await?)
229    }
230
231    async fn get_user_followers(
232        &self,
233        user_id: &str,
234        limit: Option<u32>,
235        cursor: Option<&str>,
236    ) -> Result<CursorListDTO<GetUserInformationDTO>, Error> {
237        let mut params: Vec<(&str, String)> = Vec::new();
238        push_query(&mut params, "limit", limit.map(|v| v.to_string()));
239        push_query(&mut params, "cursor", cursor.map(str::to_string));
240
241        let mut request = self.client.get(format!(
242            "{}/api/v1/users/{}/followers",
243            self.base_url, user_id
244        ));
245        if !params.is_empty() {
246            request = request.query(&params);
247        }
248        Ok(request
249            .send()
250            .await?
251            .json::<CursorListDTO<GetUserInformationDTO>>()
252            .await?)
253    }
254
255    async fn get_user_topics(
256        &self,
257        user_id: &str,
258        limit: Option<u32>,
259    ) -> Result<UserSubjectsDTO, Error> {
260        let mut request = self
261            .client
262            .get(format!("{}/api/v1/users/{}/topics", self.base_url, user_id));
263        if let Some(limit) = limit {
264            request = request.query(&[("limit", limit)]);
265        }
266        Ok(request.send().await?.json::<UserSubjectsDTO>().await?)
267    }
268
269    async fn get_user_posts(
270        &self,
271        user_id: &str,
272        role: Option<&str>,
273        limit: Option<u32>,
274    ) -> Result<UserRepliesDTO, Error> {
275        let mut params: Vec<(&str, String)> = Vec::new();
276        push_query(&mut params, "role", role.map(str::to_string));
277        push_query(&mut params, "limit", limit.map(|v| v.to_string()));
278
279        let mut request = self
280            .client
281            .get(format!("{}/api/v1/users/{}/posts", self.base_url, user_id));
282        if !params.is_empty() {
283            request = request.query(&params);
284        }
285        Ok(request.send().await?.json::<UserRepliesDTO>().await?)
286    }
287
288    async fn get_user_featured_topics(
289        &self,
290        user_id: &str,
291    ) -> Result<UserFeaturedTopicsDTO, Error> {
292        Ok(self
293            .client
294            .get(format!(
295                "{}/api/v1/users/{}/featured-topics",
296                self.base_url, user_id
297            ))
298            .send()
299            .await?
300            .json::<UserFeaturedTopicsDTO>()
301            .await?)
302    }
303
304    async fn get_user_topic_collections(
305        &self,
306        user_id: &str,
307        limit: Option<u32>,
308    ) -> Result<CursorListDTO<serde_json::Value>, Error> {
309        let mut request = self.client.get(format!(
310            "{}/api/v1/users/{}/topic-collections",
311            self.base_url, user_id
312        ));
313        if let Some(limit) = limit {
314            request = request.query(&[("limit", limit)]);
315        }
316        Ok(request
317            .send()
318            .await?
319            .json::<CursorListDTO<serde_json::Value>>()
320            .await?)
321    }
322
323    async fn get_user_badges(&self, user_id: &str) -> Result<UserBadgesDTO, Error> {
324        Ok(self
325            .client
326            .get(format!("{}/api/v1/users/{}/badges", self.base_url, user_id))
327            .send()
328            .await?
329            .json::<UserBadgesDTO>()
330            .await?)
331    }
332
333    async fn get_user_badge_display(&self, user_id: &str) -> Result<BadgeDisplayDTO, Error> {
334        Ok(self
335            .client
336            .get(format!(
337                "{}/api/v1/users/{}/badge-display",
338                self.base_url, user_id
339            ))
340            .send()
341            .await?
342            .json::<BadgeDisplayDTO>()
343            .await?)
344    }
345
346    async fn get_user_badge_displays(
347        &self,
348        user_ids: &[String],
349    ) -> Result<UserBadgeDisplaysDTO, Error> {
350        Ok(self
351            .client
352            .get(format!("{}/api/v1/user-badge-displays", self.base_url))
353            .query(&[("userIds", user_ids.join(","))])
354            .send()
355            .await?
356            .json::<UserBadgeDisplaysDTO>()
357            .await?)
358    }
359
360    async fn get_user_profile_activities(
361        &self,
362        user_id: &str,
363        year: Option<u32>,
364        limit: Option<u32>,
365    ) -> Result<CursorListDTO<ProfileActivityDTO>, Error> {
366        let mut params: Vec<(&str, String)> = Vec::new();
367        push_query(&mut params, "year", year.map(|v| v.to_string()));
368        push_query(&mut params, "limit", limit.map(|v| v.to_string()));
369
370        let mut request = self.client.get(format!(
371            "{}/api/v1/users/{}/profile-activities",
372            self.base_url, user_id
373        ));
374        if !params.is_empty() {
375            request = request.query(&params);
376        }
377        Ok(request
378            .send()
379            .await?
380            .json::<CursorListDTO<ProfileActivityDTO>>()
381            .await?)
382    }
383
384    async fn get_my_profile_activities(
385        &self,
386        year: Option<u32>,
387        limit: Option<u32>,
388    ) -> Result<CursorListDTO<ProfileActivityDTO>, Error> {
389        let mut params: Vec<(&str, String)> = Vec::new();
390        push_query(&mut params, "year", year.map(|v| v.to_string()));
391        push_query(&mut params, "limit", limit.map(|v| v.to_string()));
392
393        let mut request = self
394            .client
395            .get(format!("{}/api/v1/profile-activities", self.base_url));
396        if !params.is_empty() {
397            request = request.query(&params);
398        }
399        Ok(request
400            .send()
401            .await?
402            .json::<CursorListDTO<ProfileActivityDTO>>()
403            .await?)
404    }
405
406    async fn get_user_level_contributions(
407        &self,
408        user_id: &str,
409        year: u32,
410    ) -> Result<LevelContributionsDTO, Error> {
411        Ok(self
412            .client
413            .get(format!(
414                "{}/api/v1/users/{}/level-contributions/{}",
415                self.base_url, user_id, year
416            ))
417            .send()
418            .await?
419            .json::<LevelContributionsDTO>()
420            .await?)
421    }
422
423    async fn get_my_level_contributions(&self, year: u32) -> Result<LevelContributionsDTO, Error> {
424        Ok(self
425            .client
426            .get(format!(
427                "{}/api/v1/level-contributions/{}",
428                self.base_url, year
429            ))
430            .send()
431            .await?
432            .json::<LevelContributionsDTO>()
433            .await?)
434    }
435
436    async fn get_level_progress(&self) -> Result<LevelProgressDTO, Error> {
437        Ok(self
438            .client
439            .get(format!("{}/api/v1/level-progress", self.base_url))
440            .send()
441            .await?
442            .json::<LevelProgressDTO>()
443            .await?)
444    }
445
446    async fn get_content_length_limit(&self) -> Result<ContentLengthLimitDTO, Error> {
447        Ok(self
448            .client
449            .get(format!("{}/api/v1/content-length-limit", self.base_url))
450            .send()
451            .await?
452            .json::<ContentLengthLimitDTO>()
453            .await?)
454    }
455
456    async fn get_topic_collection_usage(&self) -> Result<TopicCollectionUsageDTO, Error> {
457        Ok(self
458            .client
459            .get(format!("{}/api/v1/topic-collection-usage", self.base_url))
460            .send()
461            .await?
462            .json::<TopicCollectionUsageDTO>()
463            .await?)
464    }
465
466    async fn get_file_storage(&self) -> Result<FileStorageDTO, Error> {
467        Ok(self
468            .client
469            .get(format!("{}/api/v1/file-storage", self.base_url))
470            .send()
471            .await?
472            .json::<FileStorageDTO>()
473            .await?)
474    }
475
476    async fn get_bookmarks(
477        &self,
478        limit: Option<u32>,
479    ) -> Result<CursorListDTO<serde_json::Value>, Error> {
480        let mut request = self
481            .client
482            .get(format!("{}/api/v1/bookmarks", self.base_url));
483        if let Some(limit) = limit {
484            request = request.query(&[("limit", limit)]);
485        }
486        Ok(request
487            .send()
488            .await?
489            .json::<CursorListDTO<serde_json::Value>>()
490            .await?)
491    }
492
493    async fn get_avatar_presets(&self) -> Result<AvatarPresetsDTO, Error> {
494        Ok(self
495            .client
496            .get(format!("{}/api/v1/avatar-presets", self.base_url))
497            .send()
498            .await?
499            .json::<AvatarPresetsDTO>()
500            .await?)
501    }
502}