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_topic_all_posts(&self, topic_id: u64) -> Result<TopicResponse> {
63 let mut topic = self.fetch_topic(topic_id, true)?;
64
65 let have: std::collections::HashSet<u64> =
67 topic.post_stream.posts.iter().map(|p| p.id).collect();
68 let missing: Vec<u64> = topic
69 .post_stream
70 .stream
71 .iter()
72 .copied()
73 .filter(|id| !have.contains(id))
74 .collect();
75
76 for chunk in missing.chunks(20) {
78 let query: Vec<String> = chunk
79 .iter()
80 .map(|id| format!("post_ids[]={}", id))
81 .collect();
82 let path = format!(
83 "/t/{}/posts.json?include_raw=1&{}",
84 topic_id,
85 query.join("&")
86 );
87 let response = self.get(&path)?;
88 let status = response.status();
89 let text = response.text().context("reading topic posts response body")?;
90 if !status.is_success() {
91 return Err(http_error("topic posts request", status, &text));
92 }
93 let body: TopicResponse = serde_json::from_str(&text)
94 .context("parsing topic posts response")?;
95 topic.post_stream.posts.extend(body.post_stream.posts);
96 }
97
98 if !topic.post_stream.stream.is_empty() {
100 let order: std::collections::HashMap<u64, usize> = topic
101 .post_stream
102 .stream
103 .iter()
104 .enumerate()
105 .map(|(i, id)| (*id, i))
106 .collect();
107 topic
108 .post_stream
109 .posts
110 .sort_by_key(|p| order.get(&p.id).copied().unwrap_or(usize::MAX));
111 }
112
113 Ok(topic)
114 }
115
116 pub fn fetch_post_raw(&self, post_id: u64) -> Result<Option<String>> {
118 Ok(self.fetch_post(post_id)?.raw)
119 }
120
121 pub fn fetch_post(&self, post_id: u64) -> Result<PostInfo> {
123 let path = format!("/posts/{}.json?include_raw=1", post_id);
124 let response = self.get(&path)?;
125 let status = response.status();
126 let text = response.text().context("reading post response body")?;
127 if !status.is_success() {
128 return Err(http_error("post request", status, &text));
129 }
130 let info: PostInfo = serde_json::from_str(&text).context("parsing post response")?;
131 Ok(info)
132 }
133
134 pub fn delete_post(&self, post_id: u64) -> Result<()> {
136 let path = format!("/posts/{}.json", post_id);
137 let response = self.send_retrying(|| Ok(self.delete_builder(&path)?))?;
138 let status = response.status();
139 if !status.is_success() {
140 let text = response
141 .text()
142 .unwrap_or_else(|_| "<failed to read response body>".to_string());
143 return Err(http_error("delete post request", status, &text));
144 }
145 Ok(())
146 }
147
148 pub fn move_posts(
154 &self,
155 source_topic_id: u64,
156 post_ids: &[u64],
157 dest_topic_id: u64,
158 ) -> Result<String> {
159 if post_ids.is_empty() {
160 return Err(anyhow!("no post IDs supplied to move"));
161 }
162 let dest = dest_topic_id.to_string();
163 let path = format!("/t/{}/move-posts.json", source_topic_id);
164 let mut payload: Vec<(String, String)> = Vec::new();
165 payload.push(("destination_topic_id".to_string(), dest.clone()));
166 for id in post_ids {
167 payload.push(("post_ids[]".to_string(), id.to_string()));
168 }
169 let response = self.send_retrying(|| Ok(self.post(&path)?.form(&payload)))?;
170 let status = response.status();
171 let text = response.text().context("reading move-posts response")?;
172 if !status.is_success() {
173 return Err(http_error("move posts request", status, &text));
174 }
175 let value: Value =
176 serde_json::from_str(&text).context("parsing move-posts response")?;
177 let url = value
178 .get("url")
179 .and_then(|v| v.as_str())
180 .map(|s| s.to_string())
181 .unwrap_or_else(|| format!("/t/{}", dest));
182 Ok(url)
183 }
184
185 pub fn update_post(&self, post_id: u64, raw: &str) -> Result<()> {
187 let path = format!("/posts/{}.json", post_id);
188 let payload = [("post[raw]", raw)];
189 let response = self.send_retrying(|| Ok(self.put(&path)?.form(&payload)))?;
190 let status = response.status();
191 if !status.is_success() {
192 let text = response
193 .text()
194 .unwrap_or_else(|_| "<failed to read response body>".to_string());
195 return Err(http_error("update post request", status, &text));
196 }
197 Ok(())
198 }
199
200 pub fn create_topic(&self, category_id: u64, title: &str, raw: &str) -> Result<u64> {
202 let category = category_id.to_string();
203 let payload = [("title", title), ("raw", raw), ("category", &category)];
204 let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
205 let status = response.status();
206 let text = response.text().context("reading create response body")?;
207 if !status.is_success() {
208 return Err(http_error("create topic request", status, &text));
209 }
210 let body: CreatePostResponse =
211 serde_json::from_str(&text).context("parsing create topic response")?;
212 Ok(body.topic_id)
213 }
214
215 pub fn create_private_message(
219 &self,
220 recipients: &[String],
221 title: &str,
222 raw: &str,
223 ) -> Result<u64> {
224 let recipients_csv = recipients.join(",");
225 let payload = [
226 ("title", title),
227 ("raw", raw),
228 ("archetype", "private_message"),
229 ("target_recipients", recipients_csv.as_str()),
230 ];
231 let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
232 let status = response.status();
233 let text = response.text().context("reading PM create response body")?;
234 if !status.is_success() {
235 return Err(http_error("create PM request", status, &text));
236 }
237 let body: CreatePostResponse =
238 serde_json::from_str(&text).context("parsing PM create response")?;
239 Ok(body.topic_id)
240 }
241
242 pub fn list_private_messages(
246 &self,
247 username: &str,
248 direction: &str,
249 ) -> Result<Vec<PmTopicSummary>> {
250 let path = match direction {
251 "inbox" => format!("/topics/private-messages/{}.json", username),
252 "sent" => format!("/topics/private-messages-sent/{}.json", username),
253 "archive" => format!("/topics/private-messages-archive/{}.json", username),
254 "unread" => format!("/topics/private-messages-unread/{}.json", username),
255 "new" => format!("/topics/private-messages-new/{}.json", username),
256 other => format!("/topics/private-messages-{}/{}.json", other, username),
257 };
258 let response = self.get(&path)?;
259 let status = response.status();
260 let text = response.text().context("reading PM list response")?;
261 if !status.is_success() {
262 return Err(http_error("PM list request", status, &text));
263 }
264 let value: Value = serde_json::from_str(&text).context("parsing PM list response")?;
265 let topics = value
266 .get("topic_list")
267 .and_then(|tl| tl.get("topics"))
268 .and_then(|t| t.as_array())
269 .map(|arr| {
270 arr.iter()
271 .filter_map(|v| serde_json::from_value::<PmTopicSummary>(v.clone()).ok())
272 .collect()
273 })
274 .unwrap_or_default();
275 Ok(topics)
276 }
277
278 pub fn create_post(&self, topic_id: u64, raw: &str) -> Result<u64> {
280 let topic = topic_id.to_string();
281 let payload = [("topic_id", topic.as_str()), ("raw", raw)];
282 let response = self.send_retrying(|| Ok(self.post("/posts.json")?.form(&payload)))?;
283 let status = response.status();
284 let text = response.text().context("reading create response body")?;
285 if !status.is_success() {
286 return Err(http_error("create post request", status, &text));
287 }
288 let body: CreatePostResponse =
289 serde_json::from_str(&text).context("parsing create post response")?;
290 Ok(body.id)
291 }
292}