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
18#[derive(Debug, Deserialize, Serialize, Clone)]
20pub struct PmTopicSummary {
21 pub id: u64,
22 #[serde(default)]
23 pub title: Option<String>,
24 #[serde(default)]
25 pub slug: Option<String>,
26 #[serde(default)]
27 pub posts_count: Option<u64>,
28 #[serde(default)]
29 pub last_posted_at: Option<String>,
30 #[serde(default)]
31 pub last_poster_username: Option<String>,
32 #[serde(default)]
33 pub unread: Option<u64>,
34}
35
36impl DiscourseClient {
37 pub fn fetch_topic(&self, topic_id: u64, include_raw: bool) -> Result<TopicResponse> {
39 let path = if include_raw {
40 format!("/t/{}.json?include_raw=1", topic_id)
41 } else {
42 format!("/t/{}.json", topic_id)
43 };
44 let response = self.get(&path)?;
45 let status = response.status();
46 let text = response.text().context("reading topic response body")?;
47 if !status.is_success() {
48 return Err(http_error("topic request", status, &text));
49 }
50 let body: TopicResponse = serde_json::from_str(&text).context("parsing topic json")?;
51 Ok(body)
52 }
53
54 pub fn fetch_post_raw(&self, post_id: u64) -> Result<Option<String>> {
56 Ok(self.fetch_post(post_id)?.raw)
57 }
58
59 pub fn fetch_post(&self, post_id: u64) -> Result<PostInfo> {
61 let path = format!("/posts/{}.json?include_raw=1", post_id);
62 let response = self.get(&path)?;
63 let status = response.status();
64 let text = response.text().context("reading post response body")?;
65 if !status.is_success() {
66 return Err(http_error("post request", status, &text));
67 }
68 let info: PostInfo = serde_json::from_str(&text).context("parsing post response")?;
69 Ok(info)
70 }
71
72 pub fn delete_post(&self, post_id: u64) -> Result<()> {
74 let path = format!("/posts/{}.json", post_id);
75 let response = self.send_retrying(|| Ok(self.delete_builder(&path)?))?;
76 let status = response.status();
77 if !status.is_success() {
78 let text = response
79 .text()
80 .unwrap_or_else(|_| "<failed to read response body>".to_string());
81 return Err(http_error("delete post request", status, &text));
82 }
83 Ok(())
84 }
85
86 pub fn move_posts(
92 &self,
93 source_topic_id: u64,
94 post_ids: &[u64],
95 dest_topic_id: u64,
96 ) -> Result<String> {
97 if post_ids.is_empty() {
98 return Err(anyhow!("no post IDs supplied to move"));
99 }
100 let dest = dest_topic_id.to_string();
101 let path = format!("/t/{}/move-posts.json", source_topic_id);
102 let mut payload: Vec<(String, String)> = Vec::new();
103 payload.push(("destination_topic_id".to_string(), dest.clone()));
104 for id in post_ids {
105 payload.push(("post_ids[]".to_string(), id.to_string()));
106 }
107 let response = self.send_retrying(|| Ok(self.post(&path)?.form(&payload)))?;
108 let status = response.status();
109 let text = response.text().context("reading move-posts response")?;
110 if !status.is_success() {
111 return Err(http_error("move posts request", status, &text));
112 }
113 let value: Value =
114 serde_json::from_str(&text).context("parsing move-posts response")?;
115 let url = value
116 .get("url")
117 .and_then(|v| v.as_str())
118 .map(|s| s.to_string())
119 .unwrap_or_else(|| format!("/t/{}", dest));
120 Ok(url)
121 }
122
123 pub fn update_post(&self, post_id: u64, raw: &str) -> Result<()> {
125 let path = format!("/posts/{}.json", post_id);
126 let payload = [("post[raw]", raw)];
127 let response = self.send_retrying(|| Ok(self.put(&path)?.form(&payload)))?;
128 let status = response.status();
129 if !status.is_success() {
130 let text = response
131 .text()
132 .unwrap_or_else(|_| "<failed to read response body>".to_string());
133 return Err(http_error("update post request", status, &text));
134 }
135 Ok(())
136 }
137
138 pub fn create_topic(&self, category_id: u64, title: &str, raw: &str) -> Result<u64> {
140 let category = category_id.to_string();
141 let payload = [("title", title), ("raw", raw), ("category", &category)];
142 let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
143 let status = response.status();
144 let text = response.text().context("reading create response body")?;
145 if !status.is_success() {
146 return Err(http_error("create topic request", status, &text));
147 }
148 let body: CreatePostResponse =
149 serde_json::from_str(&text).context("parsing create topic response")?;
150 Ok(body.topic_id)
151 }
152
153 pub fn create_private_message(
157 &self,
158 recipients: &[String],
159 title: &str,
160 raw: &str,
161 ) -> Result<u64> {
162 let recipients_csv = recipients.join(",");
163 let payload = [
164 ("title", title),
165 ("raw", raw),
166 ("archetype", "private_message"),
167 ("target_recipients", recipients_csv.as_str()),
168 ];
169 let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
170 let status = response.status();
171 let text = response.text().context("reading PM create response body")?;
172 if !status.is_success() {
173 return Err(http_error("create PM request", status, &text));
174 }
175 let body: CreatePostResponse =
176 serde_json::from_str(&text).context("parsing PM create response")?;
177 Ok(body.topic_id)
178 }
179
180 pub fn list_private_messages(
184 &self,
185 username: &str,
186 direction: &str,
187 ) -> Result<Vec<PmTopicSummary>> {
188 let path = match direction {
189 "inbox" => format!("/topics/private-messages/{}.json", username),
190 "sent" => format!("/topics/private-messages-sent/{}.json", username),
191 "archive" => format!("/topics/private-messages-archive/{}.json", username),
192 "unread" => format!("/topics/private-messages-unread/{}.json", username),
193 "new" => format!("/topics/private-messages-new/{}.json", username),
194 other => format!("/topics/private-messages-{}/{}.json", other, username),
195 };
196 let response = self.get(&path)?;
197 let status = response.status();
198 let text = response.text().context("reading PM list response")?;
199 if !status.is_success() {
200 return Err(http_error("PM list request", status, &text));
201 }
202 let value: Value = serde_json::from_str(&text).context("parsing PM list response")?;
203 let topics = value
204 .get("topic_list")
205 .and_then(|tl| tl.get("topics"))
206 .and_then(|t| t.as_array())
207 .map(|arr| {
208 arr.iter()
209 .filter_map(|v| serde_json::from_value::<PmTopicSummary>(v.clone()).ok())
210 .collect()
211 })
212 .unwrap_or_default();
213 Ok(topics)
214 }
215
216 pub fn create_post(&self, topic_id: u64, raw: &str) -> Result<u64> {
218 let topic = topic_id.to_string();
219 let payload = [("topic_id", topic.as_str()), ("raw", raw)];
220 let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
221 let status = response.status();
222 let text = response.text().context("reading create response body")?;
223 if !status.is_success() {
224 return Err(http_error("create post request", status, &text));
225 }
226 let body: CreatePostResponse =
227 serde_json::from_str(&text).context("parsing create post response")?;
228 Ok(body.id)
229 }
230}