Skip to main content

threads_rs/api/
replies.rs

1use std::collections::HashMap;
2
3use crate::client::Client;
4use crate::constants;
5use crate::error;
6use crate::http::RequestBody;
7use crate::types::{
8    ImagePostContent, PendingRepliesOptions, Post, PostId, RepliesOptions, RepliesResponse,
9    TextPostContent, VideoPostContent,
10};
11use crate::validation;
12
13impl Client {
14    /// Get replies to a post.
15    pub async fn get_replies(
16        &self,
17        post_id: &PostId,
18        opts: Option<&RepliesOptions>,
19    ) -> crate::Result<RepliesResponse> {
20        if !post_id.is_valid() {
21            return Err(error::new_validation_error(
22                0,
23                constants::ERR_EMPTY_POST_ID,
24                "",
25                "post_id",
26            ));
27        }
28
29        if let Some(opts) = opts {
30            validation::validate_replies_options(opts)?;
31        }
32
33        let token = self.access_token().await;
34        let mut params = HashMap::new();
35        params.insert("fields".into(), constants::REPLY_FIELDS.into());
36
37        if let Some(opts) = opts {
38            if let Some(limit) = opts.limit {
39                params.insert("limit".into(), limit.to_string());
40            }
41            if let Some(ref before) = opts.before {
42                params.insert("before".into(), before.clone());
43            }
44            if let Some(ref after) = opts.after {
45                params.insert("after".into(), after.clone());
46            }
47            if let Some(reverse) = opts.reverse {
48                params.insert("reverse".into(), reverse.to_string());
49            }
50        }
51
52        let path = format!("/{}/replies", post_id);
53        let resp = self.http_client.get(&path, params, &token).await?;
54        resp.json()
55    }
56
57    /// Get the full conversation thread for a post.
58    pub async fn get_conversation(
59        &self,
60        post_id: &PostId,
61        opts: Option<&RepliesOptions>,
62    ) -> crate::Result<RepliesResponse> {
63        if !post_id.is_valid() {
64            return Err(error::new_validation_error(
65                0,
66                constants::ERR_EMPTY_POST_ID,
67                "",
68                "post_id",
69            ));
70        }
71
72        if let Some(opts) = opts {
73            validation::validate_replies_options(opts)?;
74        }
75
76        let token = self.access_token().await;
77        let mut params = HashMap::new();
78        params.insert("fields".into(), constants::REPLY_FIELDS.into());
79
80        if let Some(opts) = opts {
81            if let Some(limit) = opts.limit {
82                params.insert("limit".into(), limit.to_string());
83            }
84            if let Some(ref before) = opts.before {
85                params.insert("before".into(), before.clone());
86            }
87            if let Some(ref after) = opts.after {
88                params.insert("after".into(), after.clone());
89            }
90            if let Some(reverse) = opts.reverse {
91                params.insert("reverse".into(), reverse.to_string());
92            }
93        }
94
95        let path = format!("/{}/conversation", post_id);
96        let resp = self.http_client.get(&path, params, &token).await?;
97        resp.json()
98    }
99
100    /// Get pending replies awaiting moderation.
101    pub async fn get_pending_replies(
102        &self,
103        post_id: &PostId,
104        opts: Option<&PendingRepliesOptions>,
105    ) -> crate::Result<RepliesResponse> {
106        if !post_id.is_valid() {
107            return Err(error::new_validation_error(
108                0,
109                constants::ERR_EMPTY_POST_ID,
110                "",
111                "post_id",
112            ));
113        }
114
115        if let Some(opts) = opts {
116            validation::validate_pending_replies_options(opts)?;
117        }
118
119        let token = self.access_token().await;
120        let mut params = HashMap::new();
121        params.insert("fields".into(), constants::PENDING_REPLY_FIELDS.into());
122
123        if let Some(opts) = opts {
124            if let Some(limit) = opts.limit {
125                params.insert("limit".into(), limit.to_string());
126            }
127            if let Some(ref before) = opts.before {
128                params.insert("before".into(), before.clone());
129            }
130            if let Some(ref after) = opts.after {
131                params.insert("after".into(), after.clone());
132            }
133            if let Some(reverse) = opts.reverse {
134                params.insert("reverse".into(), reverse.to_string());
135            }
136            if let Some(ref status) = opts.approval_status {
137                params.insert(
138                    "approval_status".into(),
139                    serde_json::to_string(status)
140                        .unwrap_or_default()
141                        .trim_matches('"')
142                        .to_owned(),
143                );
144            }
145        }
146
147        let path = format!("/{}/pending_replies", post_id);
148        let resp = self.http_client.get(&path, params, &token).await?;
149        resp.json()
150    }
151
152    /// Approve a pending reply.
153    pub async fn approve_pending_reply(&self, reply_id: &PostId) -> crate::Result<()> {
154        if !reply_id.is_valid() {
155            return Err(error::new_validation_error(
156                0,
157                constants::ERR_EMPTY_POST_ID,
158                "",
159                "reply_id",
160            ));
161        }
162
163        let token = self.access_token().await;
164        let mut params = HashMap::new();
165        params.insert("approve".into(), "true".into());
166
167        let path = format!("/{}/manage_pending_reply", reply_id);
168        let body = RequestBody::Form(params);
169        self.http_client.post(&path, Some(body), &token).await?;
170        Ok(())
171    }
172
173    /// Ignore a pending reply.
174    pub async fn ignore_pending_reply(&self, reply_id: &PostId) -> crate::Result<()> {
175        if !reply_id.is_valid() {
176            return Err(error::new_validation_error(
177                0,
178                constants::ERR_EMPTY_POST_ID,
179                "",
180                "reply_id",
181            ));
182        }
183
184        let token = self.access_token().await;
185        let mut params = HashMap::new();
186        params.insert("approve".into(), "false".into());
187
188        let path = format!("/{}/manage_pending_reply", reply_id);
189        let body = RequestBody::Form(params);
190        self.http_client.post(&path, Some(body), &token).await?;
191        Ok(())
192    }
193
194    /// Hide a reply.
195    pub async fn hide_reply(&self, reply_id: &PostId) -> crate::Result<()> {
196        if !reply_id.is_valid() {
197            return Err(error::new_validation_error(
198                0,
199                constants::ERR_EMPTY_POST_ID,
200                "",
201                "reply_id",
202            ));
203        }
204
205        let token = self.access_token().await;
206        let mut params = HashMap::new();
207        params.insert("hide".into(), "true".into());
208
209        let path = format!("/{}/manage_reply", reply_id);
210        let body = RequestBody::Form(params);
211        self.http_client.post(&path, Some(body), &token).await?;
212        Ok(())
213    }
214
215    /// Reply to a post with text.
216    ///
217    /// Convenience wrapper: builds a `TextPostContent` with `reply_to_id` set
218    /// and delegates to `create_text_post`.
219    pub async fn reply_to_post(&self, post_id: &PostId, text: &str) -> crate::Result<Post> {
220        if !post_id.is_valid() {
221            return Err(error::new_validation_error(
222                0,
223                constants::ERR_EMPTY_POST_ID,
224                "",
225                "post_id",
226            ));
227        }
228
229        let content = TextPostContent {
230            text: text.to_owned(),
231            reply_to_id: Some(post_id.clone()),
232            link_attachment: None,
233            poll_attachment: None,
234            reply_control: None,
235            topic_tag: None,
236            allowlisted_country_codes: None,
237            location_id: None,
238            auto_publish_text: false,
239            quoted_post_id: None,
240            text_entities: None,
241            text_attachment: None,
242            gif_attachment: None,
243            is_ghost_post: false,
244            enable_reply_approvals: false,
245        };
246        self.create_text_post(&content).await
247    }
248
249    /// Create a text reply.
250    ///
251    /// Validates that `reply_to_id` is set, then delegates to `create_text_post`.
252    /// Set `apply_reply_delay` to `true` to apply the API-recommended 10-second
253    /// delay before creating the reply.
254    pub async fn create_reply(
255        &self,
256        content: &TextPostContent,
257        apply_reply_delay: bool,
258    ) -> crate::Result<Post> {
259        if content.reply_to_id.is_none() {
260            return Err(error::new_validation_error(
261                0,
262                "reply_to_id is required for create_reply",
263                "",
264                "reply_to_id",
265            ));
266        }
267
268        if apply_reply_delay {
269            tokio::time::sleep(constants::REPLY_PUBLISH_DELAY).await;
270        }
271        self.create_text_post(content).await
272    }
273
274    /// Create an image reply.
275    ///
276    /// Validates that `reply_to_id` is set, then delegates to `create_image_post`.
277    pub async fn create_image_reply(&self, content: &ImagePostContent) -> crate::Result<Post> {
278        if content.reply_to_id.is_none() {
279            return Err(error::new_validation_error(
280                0,
281                "reply_to_id is required for create_image_reply",
282                "",
283                "reply_to_id",
284            ));
285        }
286
287        self.create_image_post(content).await
288    }
289
290    /// Create a video reply.
291    ///
292    /// Validates that `reply_to_id` is set, then delegates to `create_video_post`.
293    pub async fn create_video_reply(&self, content: &VideoPostContent) -> crate::Result<Post> {
294        if content.reply_to_id.is_none() {
295            return Err(error::new_validation_error(
296                0,
297                "reply_to_id is required for create_video_reply",
298                "",
299                "reply_to_id",
300            ));
301        }
302
303        self.create_video_post(content).await
304    }
305
306    /// Unhide a reply.
307    pub async fn unhide_reply(&self, reply_id: &PostId) -> crate::Result<()> {
308        if !reply_id.is_valid() {
309            return Err(error::new_validation_error(
310                0,
311                constants::ERR_EMPTY_POST_ID,
312                "",
313                "reply_id",
314            ));
315        }
316
317        let token = self.access_token().await;
318        let mut params = HashMap::new();
319        params.insert("hide".into(), "false".into());
320
321        let path = format!("/{}/manage_reply", reply_id);
322        let body = RequestBody::Form(params);
323        self.http_client.post(&path, Some(body), &token).await?;
324        Ok(())
325    }
326}