dsc-rs 0.10.15

Discourse CLI tool for managing multiple Discourse forums: track installs, run upgrades over SSH, manage emojis, sync topics and categories as Markdown, and more.
Documentation
use crate::api::DiscourseClient;
use crate::api::GroupSummary;
use crate::cli::{ListFormat, StructuredFormat};
use crate::commands::common::{ensure_api_credentials, not_found, parse_emails, select_discourse};
use crate::config::Config;
use crate::utils::{normalize_baseurl, slugify};
use anyhow::{Context, Result, anyhow};
use std::fs;
use std::io::{self, Read};
use std::path::Path;

pub fn group_list(
    config: &Config,
    discourse_name: &str,
    format: ListFormat,
    verbose: bool,
) -> Result<()> {
    let discourse = select_discourse(config, Some(discourse_name))?;
    ensure_api_credentials(discourse)?;
    let client = DiscourseClient::new(discourse)?;
    let groups = client.fetch_groups()?;
    match format {
        ListFormat::Text => {
            if groups.is_empty() && !verbose {
                println!("No groups found.");
                return Ok(());
            }
            for group in groups {
                let full_name = group.full_name.unwrap_or_else(|| "-".to_string());
                println!("{} - {} ({})", group.id, group.name, full_name);
            }
        }
        ListFormat::Json => {
            let raw = serde_json::to_string_pretty(&groups)?;
            println!("{}", raw);
        }
        ListFormat::Yaml => {
            let raw = serde_yaml::to_string(&groups)?;
            println!("{}", raw);
        }
    }
    Ok(())
}

pub fn group_info(
    config: &Config,
    discourse_name: &str,
    group_id: u64,
    format: StructuredFormat,
) -> Result<()> {
    let discourse = select_discourse(config, Some(discourse_name))?;
    ensure_api_credentials(discourse)?;
    let client = DiscourseClient::new(discourse)?;
    let group_summary = find_group_summary(&client, group_id)?;
    let group = client.fetch_group_detail(group_summary.id, Some(&group_summary.name))?;
    match format {
        StructuredFormat::Json => {
            let raw = serde_json::to_string_pretty(&group)?;
            println!("{}", raw);
        }
        StructuredFormat::Yaml => {
            let raw = serde_yaml::to_string(&group)?;
            println!("{}", raw);
        }
    }
    Ok(())
}

pub fn group_members(
    config: &Config,
    discourse_name: &str,
    group_id: u64,
    format: ListFormat,
) -> Result<()> {
    let discourse = select_discourse(config, Some(discourse_name))?;
    ensure_api_credentials(discourse)?;
    let client = DiscourseClient::new(discourse)?;
    let group_summary = find_group_summary(&client, group_id)?;
    let members = client.fetch_group_members(group_summary.id, Some(&group_summary.name))?;
    match format {
        ListFormat::Text => {
            if members.is_empty() {
                println!("No group members found.");
                return Ok(());
            }
            for member in members {
                let name = member.name.unwrap_or_else(|| "-".to_string());
                println!("{} - {} ({})", member.id, member.username, name);
            }
        }
        ListFormat::Json => {
            let raw = serde_json::to_string_pretty(&members)?;
            println!("{}", raw);
        }
        ListFormat::Yaml => {
            let raw = serde_yaml::to_string(&members)?;
            println!("{}", raw);
        }
    }
    Ok(())
}

pub fn group_copy(
    config: &Config,
    source: &str,
    target: Option<&str>,
    group_id: u64,
    dry_run: bool,
) -> Result<()> {
    let source_discourse = select_discourse(config, Some(source))?;
    let target_discourse_name = target.unwrap_or(source);
    let target_discourse = select_discourse(config, Some(target_discourse_name))?;

    ensure_api_credentials(source_discourse)?;
    ensure_api_credentials(target_discourse)?;

    let source_client = DiscourseClient::new(source_discourse)?;
    let group_summary = find_group_summary(&source_client, group_id)?;
    let mut group =
        source_client.fetch_group_detail(group_summary.id, Some(&group_summary.name))?;
    group.name = format!("{}-copy", slugify(&group.name));
    if let Some(full_name) = group.full_name.clone() {
        group.full_name = Some(format!("Copy of {}", full_name));
    }

    if dry_run {
        println!(
            "[dry-run] would create group \"{}\" on {}",
            group.name, target_discourse.name
        );
        return Ok(());
    }

    let target_client = DiscourseClient::new(target_discourse)?;
    let new_id = target_client.create_group(&group)?;
    let url = format!(
        "{}/g/{}/{}",
        normalize_baseurl(&target_discourse.baseurl),
        group.name,
        new_id
    );
    println!("{}", url);
    Ok(())
}

fn find_group_summary(client: &DiscourseClient, group_id: u64) -> Result<GroupSummary> {
    let groups = client.fetch_groups()?;
    groups
        .into_iter()
        .find(|item| item.id == group_id)
        .ok_or_else(|| not_found("group", group_id))
}

pub fn group_add(
    config: &Config,
    discourse_name: &str,
    group_id: u64,
    local_path: Option<&Path>,
    notify: bool,
    dry_run: bool,
) -> Result<()> {
    let discourse = select_discourse(config, Some(discourse_name))?;
    ensure_api_credentials(discourse)?;
    let client = DiscourseClient::new(discourse)?;

    let raw = read_email_source(local_path)?;
    let emails = parse_emails(&raw);
    if emails.is_empty() {
        return Err(anyhow!("no email addresses found in input"));
    }

    if dry_run {
        println!(
            "[dry-run] {}: would add {} email(s) to group {} (notify={})",
            discourse.name,
            emails.len(),
            group_id,
            notify
        );
        for email in &emails {
            println!("  {}", email);
        }
        return Ok(());
    }

    let outcome = client.add_group_members_by_email(group_id, &emails, notify)?;
    println!(
        "Requested {} email(s); Discourse reported {} username(s) added",
        emails.len(),
        outcome.added_usernames.len()
    );
    if !outcome.added_usernames.is_empty() {
        for username in &outcome.added_usernames {
            println!("  + {}", username);
        }
    }
    if !outcome.errors.is_empty() {
        eprintln!("Server notes:");
        for msg in &outcome.errors {
            eprintln!("  - {}", msg);
        }
    }
    Ok(())
}

fn read_email_source(local_path: Option<&Path>) -> Result<String> {
    let from_stdin = match local_path {
        None => true,
        Some(p) => p.as_os_str() == "-",
    };
    if from_stdin {
        let mut buf = String::new();
        io::stdin()
            .read_to_string(&mut buf)
            .context("reading emails from stdin")?;
        Ok(buf)
    } else {
        let path = local_path.unwrap();
        fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))
    }
}