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)]
9pub struct UserSummary {
10 pub id: u64,
11 pub username: String,
12 #[serde(default)]
13 pub name: Option<String>,
14 #[serde(default)]
15 pub email: Option<String>,
16 #[serde(default)]
17 pub trust_level: Option<u64>,
18 #[serde(default)]
19 pub admin: Option<bool>,
20 #[serde(default)]
21 pub moderator: Option<bool>,
22 #[serde(default)]
23 pub suspended: Option<bool>,
24 #[serde(default)]
25 pub silenced: Option<bool>,
26 #[serde(default)]
27 pub last_seen_at: Option<String>,
28 #[serde(default)]
29 pub created_at: Option<String>,
30}
31
32#[derive(Debug, Deserialize, Serialize, Clone)]
34pub struct UserDetail {
35 pub id: u64,
36 pub username: String,
37 #[serde(default)]
38 pub name: Option<String>,
39 #[serde(default)]
40 pub email: Option<String>,
41 #[serde(default)]
42 pub trust_level: Option<u64>,
43 #[serde(default)]
44 pub admin: Option<bool>,
45 #[serde(default)]
46 pub moderator: Option<bool>,
47 #[serde(default)]
48 pub suspended_till: Option<String>,
49 #[serde(default)]
50 pub silenced_till: Option<String>,
51 #[serde(default)]
52 pub last_seen_at: Option<String>,
53 #[serde(default)]
54 pub created_at: Option<String>,
55 #[serde(default)]
56 pub post_count: Option<u64>,
57 #[serde(default)]
58 pub groups: Vec<Value>,
59}
60
61impl DiscourseClient {
62 pub fn admin_list_users(&self, listing: &str, page: u32) -> Result<Vec<UserSummary>> {
67 let path = format!(
68 "/admin/users/list/{}.json?show_emails=true&page={}",
69 listing, page
70 );
71 let response = self.get(&path)?;
72 let status = response.status();
73 let text = response.text().context("reading user list response")?;
74 if !status.is_success() {
75 return Err(http_error("admin user list request", status, &text));
76 }
77 let users: Vec<UserSummary> =
78 serde_json::from_str(&text).context("parsing user list response")?;
79 Ok(users)
80 }
81
82 pub fn fetch_user_detail(&self, username: &str) -> Result<UserDetail> {
84 let path = format!("/u/{}.json", username);
85 let response = self.get(&path)?;
86 let status = response.status();
87 let text = response.text().context("reading user detail response")?;
88 if !status.is_success() {
89 return Err(http_error("user detail request", status, &text));
90 }
91 let value: Value =
92 serde_json::from_str(&text).context("parsing user detail response")?;
93 let user = value
94 .get("user")
95 .ok_or_else(|| anyhow!("user detail response missing `user` field"))?;
96 let detail: UserDetail =
97 serde_json::from_value(user.clone()).context("deserialising user detail")?;
98 Ok(detail)
99 }
100
101 pub fn suspend_user(&self, user_id: u64, until: &str, reason: &str) -> Result<()> {
105 let payload = [("suspend_until", until), ("reason", reason)];
106 self.put_admin_user_action(user_id, "suspend", &payload, "suspend user request")
107 }
108
109 pub fn unsuspend_user(&self, user_id: u64) -> Result<()> {
111 self.put_admin_user_action(user_id, "unsuspend", &[], "unsuspend user request")
112 }
113
114 pub fn silence_user(&self, user_id: u64, until: &str, reason: &str) -> Result<()> {
117 let mut payload: Vec<(&str, &str)> = Vec::new();
118 if !until.is_empty() {
119 payload.push(("silenced_till", until));
120 }
121 if !reason.is_empty() {
122 payload.push(("reason", reason));
123 }
124 self.put_admin_user_action(user_id, "silence", &payload, "silence user request")
125 }
126
127 pub fn unsilence_user(&self, user_id: u64) -> Result<()> {
129 self.put_admin_user_action(user_id, "unsilence", &[], "unsilence user request")
130 }
131
132 pub fn grant_admin(&self, user_id: u64) -> Result<()> {
134 self.put_admin_user_action(user_id, "grant_admin", &[], "grant admin request")
135 }
136
137 pub fn revoke_admin(&self, user_id: u64) -> Result<()> {
139 self.put_admin_user_action(user_id, "revoke_admin", &[], "revoke admin request")
140 }
141
142 pub fn grant_moderation(&self, user_id: u64) -> Result<()> {
144 self.put_admin_user_action(
145 user_id,
146 "grant_moderation",
147 &[],
148 "grant moderation request",
149 )
150 }
151
152 pub fn revoke_moderation(&self, user_id: u64) -> Result<()> {
154 self.put_admin_user_action(
155 user_id,
156 "revoke_moderation",
157 &[],
158 "revoke moderation request",
159 )
160 }
161
162 pub fn create_user(
167 &self,
168 email: &str,
169 username: &str,
170 password: Option<&str>,
171 name: Option<&str>,
172 approve: bool,
173 ) -> Result<u64> {
174 let mut payload: Vec<(&str, &str)> = vec![
175 ("email", email),
176 ("username", username),
177 ("active", "true"),
178 ];
179 if approve {
180 payload.push(("approved", "true"));
181 }
182 if let Some(p) = password {
183 payload.push(("password", p));
184 }
185 if let Some(n) = name {
186 if !n.is_empty() {
187 payload.push(("name", n));
188 }
189 }
190 let response = self.send_retrying(|| Ok(self.post("/u.json")?.form(&payload)))?;
191 let status = response.status();
192 let text = response.text().context("reading user create response")?;
193 if !status.is_success() {
194 return Err(http_error("user create request", status, &text));
195 }
196 let value: Value =
197 serde_json::from_str(&text).context("parsing user create response")?;
198 let id = value
201 .get("user_id")
202 .and_then(|v| v.as_u64())
203 .or_else(|| {
204 value
205 .get("user")
206 .and_then(|u| u.get("id"))
207 .and_then(|v| v.as_u64())
208 })
209 .ok_or_else(|| anyhow!("user create response missing user id: {}", text))?;
210 Ok(id)
211 }
212
213 pub fn trigger_password_reset(&self, login: &str) -> Result<()> {
217 let payload = [("login", login)];
218 let response = self
219 .send_retrying(|| Ok(self.post("/session/forgot_password.json")?.form(&payload)))?;
220 let status = response.status();
221 if !status.is_success() {
222 let text = response
223 .text()
224 .unwrap_or_else(|_| "<failed to read response body>".to_string());
225 return Err(http_error("password reset request", status, &text));
226 }
227 Ok(())
228 }
229
230 pub fn set_user_email(&self, username: &str, email: &str) -> Result<()> {
232 let path = format!("/u/{}/preferences/email.json", username);
233 let payload = [("email", email)];
234 let response = self.send_retrying(|| Ok(self.post(&path)?.form(&payload)))?;
235 let status = response.status();
236 if !status.is_success() {
237 let text = response
238 .text()
239 .unwrap_or_else(|_| "<failed to read response body>".to_string());
240 return Err(http_error("email set request", status, &text));
241 }
242 Ok(())
243 }
244
245 fn put_admin_user_action(
246 &self,
247 user_id: u64,
248 action: &str,
249 payload: &[(&str, &str)],
250 action_label: &str,
251 ) -> Result<()> {
252 let path = format!("/admin/users/{}/{}.json", user_id, action);
253 let response = self.send_retrying(|| {
254 let rb = self.put(&path)?;
255 Ok(if payload.is_empty() {
256 rb
257 } else {
258 rb.form(payload)
259 })
260 })?;
261 let status = response.status();
262 if !status.is_success() {
263 let text = response
264 .text()
265 .unwrap_or_else(|_| "<failed to read response body>".to_string());
266 return Err(http_error(action_label, status, &text));
267 }
268 Ok(())
269 }
270}