1use super::client::DiscourseClient;
2use super::error::http_error;
3use anyhow::{Context, Result, anyhow};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Deserialize, Serialize, Clone)]
13pub struct UserSummary {
14 pub id: i64,
15 pub username: String,
16 #[serde(default)]
17 pub name: Option<String>,
18 #[serde(default)]
19 pub email: Option<String>,
20 #[serde(default)]
21 pub trust_level: Option<u64>,
22 #[serde(default)]
23 pub admin: Option<bool>,
24 #[serde(default)]
25 pub moderator: Option<bool>,
26 #[serde(default)]
27 pub suspended: Option<bool>,
28 #[serde(default)]
29 pub silenced: Option<bool>,
30 #[serde(default)]
31 pub last_seen_at: Option<String>,
32 #[serde(default)]
33 pub created_at: Option<String>,
34}
35
36#[derive(Debug, Deserialize, Serialize, Clone)]
40pub struct UserDetail {
41 pub id: i64,
42 pub username: String,
43 #[serde(default)]
44 pub name: Option<String>,
45 #[serde(default)]
46 pub email: Option<String>,
47 #[serde(default)]
48 pub trust_level: Option<u64>,
49 #[serde(default)]
50 pub admin: Option<bool>,
51 #[serde(default)]
52 pub moderator: Option<bool>,
53 #[serde(default)]
54 pub suspended_till: Option<String>,
55 #[serde(default)]
56 pub silenced_till: Option<String>,
57 #[serde(default)]
58 pub last_seen_at: Option<String>,
59 #[serde(default)]
60 pub created_at: Option<String>,
61 #[serde(default)]
62 pub post_count: Option<u64>,
63 #[serde(default)]
64 pub groups: Vec<Value>,
65}
66
67impl DiscourseClient {
68 pub fn admin_list_users(&self, listing: &str, page: u32) -> Result<Vec<UserSummary>> {
73 let path = format!(
74 "/admin/users/list/{}.json?show_emails=true&page={}",
75 listing, page
76 );
77 let response = self.get(&path)?;
78 let status = response.status();
79 let text = response.text().context("reading user list response")?;
80 if !status.is_success() {
81 return Err(http_error("admin user list request", status, &text));
82 }
83 let users: Vec<UserSummary> =
84 serde_json::from_str(&text).context("parsing user list response")?;
85 Ok(users)
86 }
87
88 pub fn fetch_user_detail(&self, username: &str) -> Result<UserDetail> {
90 let path = format!("/u/{}.json", username);
91 let response = self.get(&path)?;
92 let status = response.status();
93 let text = response.text().context("reading user detail response")?;
94 if !status.is_success() {
95 return Err(http_error("user detail request", status, &text));
96 }
97 let value: Value =
98 serde_json::from_str(&text).context("parsing user detail response")?;
99 let user = value
100 .get("user")
101 .ok_or_else(|| anyhow!("user detail response missing `user` field"))?;
102 let detail: UserDetail =
103 serde_json::from_value(user.clone()).context("deserialising user detail")?;
104 Ok(detail)
105 }
106
107 pub fn suspend_user(&self, user_id: i64, until: &str, reason: &str) -> Result<()> {
111 let payload = [("suspend_until", until), ("reason", reason)];
112 self.put_admin_user_action(user_id, "suspend", &payload, "suspend user request")
113 }
114
115 pub fn unsuspend_user(&self, user_id: i64) -> Result<()> {
117 self.put_admin_user_action(user_id, "unsuspend", &[], "unsuspend user request")
118 }
119
120 pub fn silence_user(&self, user_id: i64, until: &str, reason: &str) -> Result<()> {
123 let mut payload: Vec<(&str, &str)> = Vec::new();
124 if !until.is_empty() {
125 payload.push(("silenced_till", until));
126 }
127 if !reason.is_empty() {
128 payload.push(("reason", reason));
129 }
130 self.put_admin_user_action(user_id, "silence", &payload, "silence user request")
131 }
132
133 pub fn unsilence_user(&self, user_id: i64) -> Result<()> {
135 self.put_admin_user_action(user_id, "unsilence", &[], "unsilence user request")
136 }
137
138 pub fn grant_admin(&self, user_id: i64) -> Result<()> {
140 self.put_admin_user_action(user_id, "grant_admin", &[], "grant admin request")
141 }
142
143 pub fn revoke_admin(&self, user_id: i64) -> Result<()> {
145 self.put_admin_user_action(user_id, "revoke_admin", &[], "revoke admin request")
146 }
147
148 pub fn grant_moderation(&self, user_id: i64) -> Result<()> {
150 self.put_admin_user_action(
151 user_id,
152 "grant_moderation",
153 &[],
154 "grant moderation request",
155 )
156 }
157
158 pub fn revoke_moderation(&self, user_id: i64) -> Result<()> {
160 self.put_admin_user_action(
161 user_id,
162 "revoke_moderation",
163 &[],
164 "revoke moderation request",
165 )
166 }
167
168 pub fn create_user(
173 &self,
174 email: &str,
175 username: &str,
176 password: Option<&str>,
177 name: Option<&str>,
178 approve: bool,
179 ) -> Result<i64> {
180 let mut payload: Vec<(&str, &str)> = vec![
181 ("email", email),
182 ("username", username),
183 ("active", "true"),
184 ];
185 if approve {
186 payload.push(("approved", "true"));
187 }
188 if let Some(p) = password {
189 payload.push(("password", p));
190 }
191 if let Some(n) = name {
192 if !n.is_empty() {
193 payload.push(("name", n));
194 }
195 }
196 let response = self.send_retrying(|| Ok(self.post("/u.json")?.form(&payload)))?;
197 let status = response.status();
198 let text = response.text().context("reading user create response")?;
199 if !status.is_success() {
200 return Err(http_error("user create request", status, &text));
201 }
202 let value: Value =
203 serde_json::from_str(&text).context("parsing user create response")?;
204 let id = value
207 .get("user_id")
208 .and_then(|v| v.as_i64())
209 .or_else(|| {
210 value
211 .get("user")
212 .and_then(|u| u.get("id"))
213 .and_then(|v| v.as_i64())
214 })
215 .ok_or_else(|| anyhow!("user create response missing user id: {}", text))?;
216 Ok(id)
217 }
218
219 pub fn trigger_password_reset(&self, login: &str) -> Result<()> {
223 let payload = [("login", login)];
224 let response = self
225 .send_retrying(|| Ok(self.post("/session/forgot_password.json")?.form(&payload)))?;
226 let status = response.status();
227 if !status.is_success() {
228 let text = response
229 .text()
230 .unwrap_or_else(|_| "<failed to read response body>".to_string());
231 return Err(http_error("password reset request", status, &text));
232 }
233 Ok(())
234 }
235
236 pub fn set_user_email(&self, username: &str, email: &str) -> Result<()> {
238 let path = format!("/u/{}/preferences/email.json", username);
239 let payload = [("email", email)];
240 let response = self.send_retrying(|| Ok(self.post(&path)?.form(&payload)))?;
241 let status = response.status();
242 if !status.is_success() {
243 let text = response
244 .text()
245 .unwrap_or_else(|_| "<failed to read response body>".to_string());
246 return Err(http_error("email set request", status, &text));
247 }
248 Ok(())
249 }
250
251 fn put_admin_user_action(
252 &self,
253 user_id: i64,
254 action: &str,
255 payload: &[(&str, &str)],
256 action_label: &str,
257 ) -> Result<()> {
258 let path = format!("/admin/users/{}/{}.json", user_id, action);
259 let response = self.send_retrying(|| {
260 let rb = self.put(&path)?;
261 Ok(if payload.is_empty() {
262 rb
263 } else {
264 rb.form(payload)
265 })
266 })?;
267 let status = response.status();
268 if !status.is_success() {
269 let text = response
270 .text()
271 .unwrap_or_else(|_| "<failed to read response body>".to_string());
272 return Err(http_error(action_label, status, &text));
273 }
274 Ok(())
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::{UserDetail, UserSummary};
281
282 #[test]
288 fn user_summary_accepts_negative_system_ids() {
289 let json = r#"[
290 {"id": -1, "username": "system", "name": "system",
291 "email": "no_email"},
292 {"id": -2, "username": "discobot", "name": "discobot",
293 "email": "no_email"},
294 {"id": 42, "username": "alice", "name": "Alice",
295 "email": "alice@example.com"}
296 ]"#;
297 let users: Vec<UserSummary> =
298 serde_json::from_str(json).expect("negative ids must parse");
299 assert_eq!(users.len(), 3);
300 assert_eq!(users[0].id, -1);
301 assert_eq!(users[1].id, -2);
302 assert_eq!(users[2].id, 42);
303 assert_eq!(users[0].username, "system");
304 }
305
306 #[test]
307 fn user_detail_accepts_negative_system_ids() {
308 let json = r#"{"id": -1, "username": "system"}"#;
309 let detail: UserDetail = serde_json::from_str(json).expect("must parse");
310 assert_eq!(detail.id, -1);
311 assert_eq!(detail.username, "system");
312 }
313}