use super::client::DiscourseClient;
use super::error::http_error;
use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct UserSummary {
pub id: i64,
pub username: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub trust_level: Option<u64>,
#[serde(default)]
pub admin: Option<bool>,
#[serde(default)]
pub moderator: Option<bool>,
#[serde(default)]
pub suspended: Option<bool>,
#[serde(default)]
pub silenced: Option<bool>,
#[serde(default)]
pub last_seen_at: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct UserDetail {
pub id: i64,
pub username: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub trust_level: Option<u64>,
#[serde(default)]
pub admin: Option<bool>,
#[serde(default)]
pub moderator: Option<bool>,
#[serde(default)]
pub suspended_till: Option<String>,
#[serde(default)]
pub silenced_till: Option<String>,
#[serde(default)]
pub last_seen_at: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub post_count: Option<u64>,
#[serde(default)]
pub groups: Vec<Value>,
}
impl DiscourseClient {
pub fn admin_list_users(&self, listing: &str, page: u32) -> Result<Vec<UserSummary>> {
let path = format!(
"/admin/users/list/{}.json?show_emails=true&page={}",
listing, page
);
let response = self.get(&path)?;
let status = response.status();
let text = response.text().context("reading user list response")?;
if !status.is_success() {
return Err(http_error("admin user list request", status, &text));
}
let users: Vec<UserSummary> =
serde_json::from_str(&text).context("parsing user list response")?;
Ok(users)
}
pub fn fetch_user_detail(&self, username: &str) -> Result<UserDetail> {
let path = format!("/u/{}.json", username);
let response = self.get(&path)?;
let status = response.status();
let text = response.text().context("reading user detail response")?;
if !status.is_success() {
return Err(http_error("user detail request", status, &text));
}
let value: Value =
serde_json::from_str(&text).context("parsing user detail response")?;
let user = value
.get("user")
.ok_or_else(|| anyhow!("user detail response missing `user` field"))?;
let detail: UserDetail =
serde_json::from_value(user.clone()).context("deserialising user detail")?;
Ok(detail)
}
pub fn suspend_user(&self, user_id: i64, until: &str, reason: &str) -> Result<()> {
let payload = [("suspend_until", until), ("reason", reason)];
self.put_admin_user_action(user_id, "suspend", &payload, "suspend user request")
}
pub fn unsuspend_user(&self, user_id: i64) -> Result<()> {
self.put_admin_user_action(user_id, "unsuspend", &[], "unsuspend user request")
}
pub fn silence_user(&self, user_id: i64, until: &str, reason: &str) -> Result<()> {
let mut payload: Vec<(&str, &str)> = Vec::new();
if !until.is_empty() {
payload.push(("silenced_till", until));
}
if !reason.is_empty() {
payload.push(("reason", reason));
}
self.put_admin_user_action(user_id, "silence", &payload, "silence user request")
}
pub fn unsilence_user(&self, user_id: i64) -> Result<()> {
self.put_admin_user_action(user_id, "unsilence", &[], "unsilence user request")
}
pub fn grant_admin(&self, user_id: i64) -> Result<()> {
self.put_admin_user_action(user_id, "grant_admin", &[], "grant admin request")
}
pub fn revoke_admin(&self, user_id: i64) -> Result<()> {
self.put_admin_user_action(user_id, "revoke_admin", &[], "revoke admin request")
}
pub fn grant_moderation(&self, user_id: i64) -> Result<()> {
self.put_admin_user_action(
user_id,
"grant_moderation",
&[],
"grant moderation request",
)
}
pub fn revoke_moderation(&self, user_id: i64) -> Result<()> {
self.put_admin_user_action(
user_id,
"revoke_moderation",
&[],
"revoke moderation request",
)
}
pub fn create_user(
&self,
email: &str,
username: &str,
password: Option<&str>,
name: Option<&str>,
approve: bool,
) -> Result<i64> {
let mut payload: Vec<(&str, &str)> = vec![
("email", email),
("username", username),
("active", "true"),
];
if approve {
payload.push(("approved", "true"));
}
if let Some(p) = password {
payload.push(("password", p));
}
if let Some(n) = name {
if !n.is_empty() {
payload.push(("name", n));
}
}
let response = self.send_retrying(|| Ok(self.post("/u.json")?.form(&payload)))?;
let status = response.status();
let text = response.text().context("reading user create response")?;
if !status.is_success() {
return Err(http_error("user create request", status, &text));
}
let value: Value =
serde_json::from_str(&text).context("parsing user create response")?;
let id = value
.get("user_id")
.and_then(|v| v.as_i64())
.or_else(|| {
value
.get("user")
.and_then(|u| u.get("id"))
.and_then(|v| v.as_i64())
})
.ok_or_else(|| anyhow!("user create response missing user id: {}", text))?;
Ok(id)
}
pub fn trigger_password_reset(&self, login: &str) -> Result<()> {
let payload = [("login", login)];
let response = self
.send_retrying(|| Ok(self.post("/session/forgot_password.json")?.form(&payload)))?;
let status = response.status();
if !status.is_success() {
let text = response
.text()
.unwrap_or_else(|_| "<failed to read response body>".to_string());
return Err(http_error("password reset request", status, &text));
}
Ok(())
}
pub fn set_user_email(&self, username: &str, email: &str) -> Result<()> {
let path = format!("/u/{}/preferences/email.json", username);
let payload = [("email", email)];
let response = self.send_retrying(|| Ok(self.post(&path)?.form(&payload)))?;
let status = response.status();
if !status.is_success() {
let text = response
.text()
.unwrap_or_else(|_| "<failed to read response body>".to_string());
return Err(http_error("email set request", status, &text));
}
Ok(())
}
fn put_admin_user_action(
&self,
user_id: i64,
action: &str,
payload: &[(&str, &str)],
action_label: &str,
) -> Result<()> {
let path = format!("/admin/users/{}/{}.json", user_id, action);
let response = self.send_retrying(|| {
let rb = self.put(&path)?;
Ok(if payload.is_empty() {
rb
} else {
rb.form(payload)
})
})?;
let status = response.status();
if !status.is_success() {
let text = response
.text()
.unwrap_or_else(|_| "<failed to read response body>".to_string());
return Err(http_error(action_label, status, &text));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{UserDetail, UserSummary};
#[test]
fn user_summary_accepts_negative_system_ids() {
let json = r#"[
{"id": -1, "username": "system", "name": "system",
"email": "no_email"},
{"id": -2, "username": "discobot", "name": "discobot",
"email": "no_email"},
{"id": 42, "username": "alice", "name": "Alice",
"email": "alice@example.com"}
]"#;
let users: Vec<UserSummary> =
serde_json::from_str(json).expect("negative ids must parse");
assert_eq!(users.len(), 3);
assert_eq!(users[0].id, -1);
assert_eq!(users[1].id, -2);
assert_eq!(users[2].id, 42);
assert_eq!(users[0].username, "system");
}
#[test]
fn user_detail_accepts_negative_system_ids() {
let json = r#"{"id": -1, "username": "system"}"#;
let detail: UserDetail = serde_json::from_str(json).expect("must parse");
assert_eq!(detail.id, -1);
assert_eq!(detail.username, "system");
}
}