Skip to main content

dsc/api/
topics.rs

1use super::client::DiscourseClient;
2use super::error::http_error;
3use super::models::{CreatePostResponse, TopicResponse};
4use anyhow::{Context, Result, anyhow};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Debug, Deserialize, Serialize, Clone)]
9pub struct PostInfo {
10    pub id: u64,
11    pub topic_id: u64,
12    #[serde(default)]
13    pub post_number: Option<u64>,
14    #[serde(default)]
15    pub raw: Option<String>,
16}
17
18impl DiscourseClient {
19    /// Fetch a topic by ID.
20    pub fn fetch_topic(&self, topic_id: u64, include_raw: bool) -> Result<TopicResponse> {
21        let path = if include_raw {
22            format!("/t/{}.json?include_raw=1", topic_id)
23        } else {
24            format!("/t/{}.json", topic_id)
25        };
26        let response = self.get(&path)?;
27        let status = response.status();
28        let text = response.text().context("reading topic response body")?;
29        if !status.is_success() {
30            return Err(http_error("topic request", status, &text));
31        }
32        let body: TopicResponse = serde_json::from_str(&text).context("parsing topic json")?;
33        Ok(body)
34    }
35
36    /// Fetch a post by ID and return its raw content.
37    pub fn fetch_post_raw(&self, post_id: u64) -> Result<Option<String>> {
38        Ok(self.fetch_post(post_id)?.raw)
39    }
40
41    /// Fetch a post's metadata (id, topic_id, post_number, raw).
42    pub fn fetch_post(&self, post_id: u64) -> Result<PostInfo> {
43        let path = format!("/posts/{}.json?include_raw=1", post_id);
44        let response = self.get(&path)?;
45        let status = response.status();
46        let text = response.text().context("reading post response body")?;
47        if !status.is_success() {
48            return Err(http_error("post request", status, &text));
49        }
50        let info: PostInfo = serde_json::from_str(&text).context("parsing post response")?;
51        Ok(info)
52    }
53
54    /// Soft-delete a post by ID (DELETE /posts/:id.json).
55    pub fn delete_post(&self, post_id: u64) -> Result<()> {
56        let path = format!("/posts/{}.json", post_id);
57        let response = self.send_retrying(|| Ok(self.delete_builder(&path)?))?;
58        let status = response.status();
59        if !status.is_success() {
60            let text = response
61                .text()
62                .unwrap_or_else(|_| "<failed to read response body>".to_string());
63            return Err(http_error("delete post request", status, &text));
64        }
65        Ok(())
66    }
67
68    /// Move one or more posts from their current topic to another topic.
69    ///
70    /// `source_topic_id` is the topic the posts currently live in.
71    /// `post_ids` are the post IDs to move. `dest_topic_id` is where they land.
72    /// Returns the new URL of the moved posts' topic.
73    pub fn move_posts(
74        &self,
75        source_topic_id: u64,
76        post_ids: &[u64],
77        dest_topic_id: u64,
78    ) -> Result<String> {
79        if post_ids.is_empty() {
80            return Err(anyhow!("no post IDs supplied to move"));
81        }
82        let dest = dest_topic_id.to_string();
83        let path = format!("/t/{}/move-posts.json", source_topic_id);
84        let mut payload: Vec<(String, String)> = Vec::new();
85        payload.push(("destination_topic_id".to_string(), dest.clone()));
86        for id in post_ids {
87            payload.push(("post_ids[]".to_string(), id.to_string()));
88        }
89        let response = self.send_retrying(|| Ok(self.post(&path)?.form(&payload)))?;
90        let status = response.status();
91        let text = response.text().context("reading move-posts response")?;
92        if !status.is_success() {
93            return Err(http_error("move posts request", status, &text));
94        }
95        let value: Value =
96            serde_json::from_str(&text).context("parsing move-posts response")?;
97        let url = value
98            .get("url")
99            .and_then(|v| v.as_str())
100            .map(|s| s.to_string())
101            .unwrap_or_else(|| format!("/t/{}", dest));
102        Ok(url)
103    }
104
105    /// Update a post by ID.
106    pub fn update_post(&self, post_id: u64, raw: &str) -> Result<()> {
107        let path = format!("/posts/{}.json", post_id);
108        let payload = [("post[raw]", raw)];
109        let response = self.send_retrying(|| Ok(self.put(&path)?.form(&payload)))?;
110        let status = response.status();
111        if !status.is_success() {
112            let text = response
113                .text()
114                .unwrap_or_else(|_| "<failed to read response body>".to_string());
115            return Err(http_error("update post request", status, &text));
116        }
117        Ok(())
118    }
119
120    /// Create a new topic in a category.
121    pub fn create_topic(&self, category_id: u64, title: &str, raw: &str) -> Result<u64> {
122        let category = category_id.to_string();
123        let payload = [("title", title), ("raw", raw), ("category", &category)];
124        let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
125        let status = response.status();
126        let text = response.text().context("reading create response body")?;
127        if !status.is_success() {
128            return Err(http_error("create topic request", status, &text));
129        }
130        let body: CreatePostResponse =
131            serde_json::from_str(&text).context("parsing create topic response")?;
132        Ok(body.topic_id)
133    }
134
135    /// Create a reply post in a topic.
136    pub fn create_post(&self, topic_id: u64, raw: &str) -> Result<u64> {
137        let topic = topic_id.to_string();
138        let payload = [("topic_id", topic.as_str()), ("raw", raw)];
139        let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
140        let status = response.status();
141        let text = response.text().context("reading create response body")?;
142        if !status.is_success() {
143            return Err(http_error("create post request", status, &text));
144        }
145        let body: CreatePostResponse =
146            serde_json::from_str(&text).context("parsing create post response")?;
147        Ok(body.id)
148    }
149}