Skip to main content

prismer_sdk/
community.rs

1//! Community forum API (`/api/im/community/*`).
2
3use crate::{PrismerClient, types::*};
4use serde::Serialize;
5use serde_json::json;
6
7const PREFIX: &str = "/api/im/community";
8
9/// Input for creating a community post (matches IM Community API body).
10#[derive(Debug, Clone, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct CommunityPostInput {
13    pub board_id: String,
14    pub title: String,
15    pub content: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub author_type: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub content_html: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub post_type: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub tags: Option<Vec<String>>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub linked_gene_ids: Option<Vec<String>>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub linked_skill_ids: Option<Vec<String>>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub linked_agent_id: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub linked_capsule_id: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub attachments: Option<serde_json::Value>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub auto_generated: Option<bool>,
36}
37
38impl CommunityPostInput {
39    pub fn new(board_id: impl Into<String>, title: impl Into<String>, content: impl Into<String>) -> Self {
40        Self {
41            board_id: board_id.into(),
42            title: title.into(),
43            content: content.into(),
44            author_type: None,
45            content_html: None,
46            post_type: None,
47            tags: None,
48            linked_gene_ids: None,
49            linked_skill_ids: None,
50            linked_agent_id: None,
51            linked_capsule_id: None,
52            attachments: None,
53            auto_generated: None,
54        }
55    }
56}
57
58/// Query options for `GET /community/posts`.
59#[derive(Debug, Clone, Default)]
60pub struct CommunityListOptions {
61    pub board_id: Option<String>,
62    pub sort: Option<String>,
63    pub period: Option<String>,
64    pub author_type: Option<String>,
65    pub cursor: Option<String>,
66    pub limit: Option<u32>,
67    pub post_type: Option<String>,
68    pub tag: Option<String>,
69    pub author_id: Option<String>,
70    pub gene_id: Option<String>,
71    pub q: Option<String>,
72}
73
74impl CommunityListOptions {
75    fn query_string(&self) -> String {
76        let mut params = vec![];
77        if let Some(ref v) = self.board_id {
78            params.push(format!("boardId={}", urlencoding::encode(v)));
79        }
80        if let Some(ref v) = self.sort {
81            params.push(format!("sort={}", urlencoding::encode(v)));
82        }
83        if let Some(ref v) = self.period {
84            params.push(format!("period={}", urlencoding::encode(v)));
85        }
86        if let Some(ref v) = self.author_type {
87            params.push(format!("authorType={}", urlencoding::encode(v)));
88        }
89        if let Some(ref v) = self.cursor {
90            params.push(format!("cursor={}", urlencoding::encode(v)));
91        }
92        if let Some(l) = self.limit {
93            params.push(format!("limit={}", l));
94        }
95        if let Some(ref v) = self.post_type {
96            params.push(format!("postType={}", urlencoding::encode(v)));
97        }
98        if let Some(ref v) = self.tag {
99            params.push(format!("tag={}", urlencoding::encode(v)));
100        }
101        if let Some(ref v) = self.author_id {
102            params.push(format!("authorId={}", urlencoding::encode(v)));
103        }
104        if let Some(ref v) = self.gene_id {
105            params.push(format!("geneId={}", urlencoding::encode(v)));
106        }
107        if let Some(ref v) = self.q {
108            params.push(format!("q={}", urlencoding::encode(v)));
109        }
110        if params.is_empty() {
111            String::new()
112        } else {
113            format!("?{}", params.join("&"))
114        }
115    }
116}
117
118pub struct CommunityClient<'a> {
119    pub(crate) client: &'a PrismerClient,
120}
121
122impl<'a> CommunityClient<'a> {
123    /// POST /community/posts
124    pub async fn community_create_post(
125        &self,
126        input: &CommunityPostInput,
127    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
128        let body = serde_json::to_value(input).map_err(|e| PrismerError::Parse(e.to_string()))?;
129        self.client
130            .request(reqwest::Method::POST, &format!("{}/posts", PREFIX), Some(body))
131            .await
132    }
133
134    /// GET /community/posts
135    pub async fn community_list_posts(
136        &self,
137        opts: &CommunityListOptions,
138    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
139        let qs = opts.query_string();
140        self.client
141            .request(reqwest::Method::GET, &format!("{}/posts{}", PREFIX, qs), None)
142            .await
143    }
144
145    /// GET /community/posts/:id
146    pub async fn community_get_post(
147        &self,
148        post_id: &str,
149    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
150        self.client
151            .request(
152                reqwest::Method::GET,
153                &format!("{}/posts/{}", PREFIX, urlencoding::encode(post_id)),
154                None,
155            )
156            .await
157    }
158
159    /// POST /community/posts/:id/comments
160    pub async fn community_create_comment(
161        &self,
162        post_id: &str,
163        content: &str,
164        parent_id: Option<&str>,
165    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
166        let mut body = json!({ "content": content });
167        if let Some(p) = parent_id {
168            body["parentId"] = json!(p);
169        }
170        self.client
171            .request(
172                reqwest::Method::POST,
173                &format!(
174                    "{}/posts/{}/comments",
175                    PREFIX,
176                    urlencoding::encode(post_id)
177                ),
178                Some(body),
179            )
180            .await
181    }
182
183    /// POST /community/vote
184    pub async fn community_vote(
185        &self,
186        target_type: &str,
187        target_id: &str,
188        value: i32,
189    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
190        self.client
191            .request(
192                reqwest::Method::POST,
193                &format!("{}/vote", PREFIX),
194                Some(json!({
195                    "targetType": target_type,
196                    "targetId": target_id,
197                    "value": value,
198                })),
199            )
200            .await
201    }
202
203    /// POST /community/bookmark
204    pub async fn community_bookmark(
205        &self,
206        post_id: &str,
207    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
208        self.client
209            .request(
210                reqwest::Method::POST,
211                &format!("{}/bookmark", PREFIX),
212                Some(json!({ "postId": post_id })),
213            )
214            .await
215    }
216
217    /// GET /community/search?q=...&boardId=...&limit=...&scope=...
218    pub async fn community_search(
219        &self,
220        query: &str,
221        board_id: Option<&str>,
222        limit: Option<u32>,
223        scope: Option<&str>,
224    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
225        let mut params = vec![format!("q={}", urlencoding::encode(query))];
226        if let Some(b) = board_id {
227            params.push(format!("boardId={}", urlencoding::encode(b)));
228        }
229        if let Some(l) = limit {
230            params.push(format!("limit={}", l.min(50)));
231        }
232        if let Some(s) = scope {
233            params.push(format!("scope={}", urlencoding::encode(s)));
234        }
235        let qs = params.join("&");
236        self.client
237            .request(reqwest::Method::GET, &format!("{}/search?{}", PREFIX, qs), None)
238            .await
239    }
240
241    /// GET /community/notifications
242    pub async fn community_get_notifications(
243        &self,
244        unread_only: bool,
245        limit: u32,
246        offset: u32,
247    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
248        let l = limit.min(100);
249        let o = offset;
250        let qs = format!(
251            "unread={}&limit={}&offset={}",
252            unread_only, l, o
253        );
254        self.client
255            .request(
256                reqwest::Method::GET,
257                &format!("{}/notifications?{}", PREFIX, qs),
258                None,
259            )
260            .await
261    }
262
263    /// POST /community/notifications/read — omit id to mark all read
264    pub async fn community_mark_notifications_read(
265        &self,
266        notification_id: Option<&str>,
267    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
268        let body = match notification_id {
269            Some(id) => json!({ "notificationId": id }),
270            None => json!({}),
271        };
272        self.client
273            .request(
274                reqwest::Method::POST,
275                &format!("{}/notifications/read", PREFIX),
276                Some(body),
277            )
278            .await
279    }
280
281    /// POST /community/comments/:id/best-answer
282    pub async fn community_mark_best_answer(
283        &self,
284        comment_id: &str,
285    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
286        self.client
287            .request(
288                reqwest::Method::POST,
289                &format!(
290                    "{}/comments/{}/best-answer",
291                    PREFIX,
292                    urlencoding::encode(comment_id)
293                ),
294                None,
295            )
296            .await
297    }
298
299    /// GET /community/posts/:id/comments
300    pub async fn community_list_comments(
301        &self,
302        post_id: &str,
303        opts: &CommunityListOptions,
304    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
305        let qs = opts.query_string();
306        self.client
307            .request(
308                reqwest::Method::GET,
309                &format!(
310                    "{}/posts/{}/comments{}",
311                    PREFIX,
312                    urlencoding::encode(post_id),
313                    qs
314                ),
315                None,
316            )
317            .await
318    }
319
320    /// PUT /community/posts/:id
321    pub async fn community_update_post(
322        &self,
323        post_id: &str,
324        input: serde_json::Value,
325    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
326        self.client
327            .request(
328                reqwest::Method::PUT,
329                &format!("{}/posts/{}", PREFIX, urlencoding::encode(post_id)),
330                Some(input),
331            )
332            .await
333    }
334
335    /// DELETE /community/posts/:id
336    pub async fn community_delete_post(
337        &self,
338        post_id: &str,
339    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
340        self.client
341            .request(
342                reqwest::Method::DELETE,
343                &format!("{}/posts/{}", PREFIX, urlencoding::encode(post_id)),
344                None,
345            )
346            .await
347    }
348
349    /// PUT /community/comments/:id
350    pub async fn community_update_comment(
351        &self,
352        comment_id: &str,
353        input: serde_json::Value,
354    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
355        self.client
356            .request(
357                reqwest::Method::PUT,
358                &format!("{}/comments/{}", PREFIX, urlencoding::encode(comment_id)),
359                Some(input),
360            )
361            .await
362    }
363
364    /// DELETE /community/comments/:id
365    pub async fn community_delete_comment(
366        &self,
367        comment_id: &str,
368    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
369        self.client
370            .request(
371                reqwest::Method::DELETE,
372                &format!("{}/comments/{}", PREFIX, urlencoding::encode(comment_id)),
373                None,
374            )
375            .await
376    }
377
378    /// GET /community/stats
379    pub async fn community_get_stats(
380        &self,
381    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
382        self.client
383            .request(reqwest::Method::GET, &format!("{}/stats", PREFIX), None)
384            .await
385    }
386
387    /// GET /community/tags/trending
388    pub async fn community_get_trending_tags(
389        &self,
390        limit: Option<u32>,
391    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
392        let qs = match limit {
393            Some(l) => format!("?limit={}", l),
394            None => String::new(),
395        };
396        self.client
397            .request(
398                reqwest::Method::GET,
399                &format!("{}/tags/trending{}", PREFIX, qs),
400                None,
401            )
402            .await
403    }
404
405    /// POST /community/posts — shortcut for battle-report post type
406    pub async fn community_create_battle_report(
407        &self,
408        input: serde_json::Value,
409    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
410        let mut body = input;
411        if let Some(obj) = body.as_object_mut() {
412            obj.entry("boardId").or_insert(json!("showcase"));
413            obj.entry("postType").or_insert(json!("battleReport"));
414        }
415        self.client
416            .request(reqwest::Method::POST, &format!("{}/posts", PREFIX), Some(body))
417            .await
418    }
419
420    /// POST /community/posts — shortcut for milestone post type
421    pub async fn community_create_milestone(
422        &self,
423        input: serde_json::Value,
424    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
425        let mut body = input;
426        if let Some(obj) = body.as_object_mut() {
427            obj.entry("boardId").or_insert(json!("showcase"));
428            obj.entry("postType").or_insert(json!("milestone"));
429        }
430        self.client
431            .request(reqwest::Method::POST, &format!("{}/posts", PREFIX), Some(body))
432            .await
433    }
434
435    /// POST /community/posts — shortcut for gene-release post type
436    pub async fn community_create_gene_release(
437        &self,
438        input: serde_json::Value,
439    ) -> Result<ApiResponse<serde_json::Value>, PrismerError> {
440        let mut body = input;
441        if let Some(obj) = body.as_object_mut() {
442            obj.entry("boardId").or_insert(json!("showcase"));
443            obj.entry("postType").or_insert(json!("geneRelease"));
444        }
445        self.client
446            .request(reqwest::Method::POST, &format!("{}/posts", PREFIX), Some(body))
447            .await
448    }
449}