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 fn put_admin_user_action(
163 &self,
164 user_id: u64,
165 action: &str,
166 payload: &[(&str, &str)],
167 action_label: &str,
168 ) -> Result<()> {
169 let path = format!("/admin/users/{}/{}.json", user_id, action);
170 let response = self.send_retrying(|| {
171 let rb = self.put(&path)?;
172 Ok(if payload.is_empty() {
173 rb
174 } else {
175 rb.form(payload)
176 })
177 })?;
178 let status = response.status();
179 if !status.is_success() {
180 let text = response
181 .text()
182 .unwrap_or_else(|_| "<failed to read response body>".to_string());
183 return Err(http_error(action_label, status, &text));
184 }
185 Ok(())
186 }
187}