mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
166 lines
5.8 KiB
Rust
166 lines
5.8 KiB
Rust
//! `buzz moderation` — community moderation queue, enforcement, and audit.
|
||
//!
|
||
//! Mutations (`ban`/`unban`/`timeout`/`untimeout`/`resolve`) are signed
|
||
//! command events (kinds 9040–9044) submitted via `POST /events`, mirroring
|
||
//! the NIP-43 relay-admin 9030-series: the relay validates, authorizes
|
||
//! (owner/admin only), and executes them directly — they are never stored.
|
||
//!
|
||
//! Reads (`reports`/`restricted`/`audit`) hit dedicated mod-only,
|
||
//! NIP-98-authed relay endpoints under `/moderation/*`, because reports and
|
||
//! audit rows are structured queue rows, not public nostr events — serving
|
||
//! them over a REQ filter would mean synthesizing fake events and threading a
|
||
//! privileged authz check into the public read path.
|
||
//!
|
||
//! The community (tenant) is selected by the relay host — moderation commands
|
||
//! carry no channel scope.
|
||
|
||
use nostr::Timestamp;
|
||
|
||
use crate::client::{normalize_write_response, BuzzClient};
|
||
use crate::error::CliError;
|
||
use crate::validate::validate_hex64;
|
||
use crate::{ModerationCmd, OutputFormat};
|
||
|
||
/// Resolve `--expires-in <secs>` / `--expires-at <unix>` into an absolute
|
||
/// unix-seconds expiry. At most one may be set (enforced by clap).
|
||
fn resolve_expiry(expires_in: Option<u64>, expires_at: Option<u64>) -> Option<u64> {
|
||
match (expires_in, expires_at) {
|
||
(Some(secs), _) => Some(Timestamp::now().as_secs() + secs),
|
||
(None, Some(ts)) => Some(ts),
|
||
(None, None) => None,
|
||
}
|
||
}
|
||
|
||
async fn cmd_ban(
|
||
client: &BuzzClient,
|
||
pubkey: &str,
|
||
expires_in: Option<u64>,
|
||
expires_at: Option<u64>,
|
||
reason: Option<&str>,
|
||
) -> Result<(), CliError> {
|
||
validate_hex64(pubkey)?;
|
||
let expiry = resolve_expiry(expires_in, expires_at);
|
||
let builder = buzz_sdk::build_moderation_ban(pubkey, expiry, reason)
|
||
.map_err(|e| CliError::Usage(format!("invalid ban: {e}")))?;
|
||
let event = client.sign_event(builder)?;
|
||
let resp = client.submit_event(event).await?;
|
||
println!("{}", normalize_write_response(&resp));
|
||
Ok(())
|
||
}
|
||
|
||
async fn cmd_unban(client: &BuzzClient, pubkey: &str) -> Result<(), CliError> {
|
||
validate_hex64(pubkey)?;
|
||
let builder = buzz_sdk::build_moderation_unban(pubkey)
|
||
.map_err(|e| CliError::Usage(format!("invalid unban: {e}")))?;
|
||
let event = client.sign_event(builder)?;
|
||
let resp = client.submit_event(event).await?;
|
||
println!("{}", normalize_write_response(&resp));
|
||
Ok(())
|
||
}
|
||
|
||
async fn cmd_timeout(
|
||
client: &BuzzClient,
|
||
pubkey: &str,
|
||
expires_in: Option<u64>,
|
||
expires_at: Option<u64>,
|
||
reason: Option<&str>,
|
||
) -> Result<(), CliError> {
|
||
validate_hex64(pubkey)?;
|
||
let expiry = resolve_expiry(expires_in, expires_at)
|
||
.ok_or_else(|| CliError::Usage("timeout requires --expires-in or --expires-at".into()))?;
|
||
let builder = buzz_sdk::build_moderation_timeout(pubkey, expiry, reason)
|
||
.map_err(|e| CliError::Usage(format!("invalid timeout: {e}")))?;
|
||
let event = client.sign_event(builder)?;
|
||
let resp = client.submit_event(event).await?;
|
||
println!("{}", normalize_write_response(&resp));
|
||
Ok(())
|
||
}
|
||
|
||
async fn cmd_untimeout(client: &BuzzClient, pubkey: &str) -> Result<(), CliError> {
|
||
validate_hex64(pubkey)?;
|
||
let builder = buzz_sdk::build_moderation_untimeout(pubkey)
|
||
.map_err(|e| CliError::Usage(format!("invalid untimeout: {e}")))?;
|
||
let event = client.sign_event(builder)?;
|
||
let resp = client.submit_event(event).await?;
|
||
println!("{}", normalize_write_response(&resp));
|
||
Ok(())
|
||
}
|
||
|
||
async fn cmd_resolve(
|
||
client: &BuzzClient,
|
||
report: &str,
|
||
status: &str,
|
||
action: &str,
|
||
reason: Option<&str>,
|
||
) -> Result<(), CliError> {
|
||
validate_hex64(report)?;
|
||
let builder = buzz_sdk::build_moderation_resolve_report(report, status, action, reason)
|
||
.map_err(|e| CliError::Usage(format!("invalid resolution: {e}")))?;
|
||
let event = client.sign_event(builder)?;
|
||
let resp = client.submit_event(event).await?;
|
||
println!("{}", normalize_write_response(&resp));
|
||
Ok(())
|
||
}
|
||
|
||
async fn cmd_reports(
|
||
client: &BuzzClient,
|
||
status: Option<&str>,
|
||
limit: i64,
|
||
) -> Result<(), CliError> {
|
||
let mut path = format!("/moderation/reports?limit={limit}");
|
||
if let Some(s) = status {
|
||
path.push_str(&format!("&status={s}"));
|
||
}
|
||
let resp = client.get_authed(&path).await?;
|
||
println!("{resp}");
|
||
Ok(())
|
||
}
|
||
|
||
async fn cmd_restricted(client: &BuzzClient) -> Result<(), CliError> {
|
||
let resp = client.get_authed("/moderation/restricted").await?;
|
||
println!("{resp}");
|
||
Ok(())
|
||
}
|
||
|
||
async fn cmd_audit(client: &BuzzClient, limit: i64) -> Result<(), CliError> {
|
||
let resp = client
|
||
.get_authed(&format!("/moderation/audit?limit={limit}"))
|
||
.await?;
|
||
println!("{resp}");
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn dispatch(
|
||
cmd: ModerationCmd,
|
||
client: &BuzzClient,
|
||
_format: &OutputFormat,
|
||
) -> Result<(), CliError> {
|
||
match cmd {
|
||
ModerationCmd::Reports { status, limit } => {
|
||
cmd_reports(client, status.as_deref(), limit).await
|
||
}
|
||
ModerationCmd::Resolve {
|
||
report,
|
||
status,
|
||
action,
|
||
reason,
|
||
} => cmd_resolve(client, &report, &status, &action, reason.as_deref()).await,
|
||
ModerationCmd::Ban {
|
||
pubkey,
|
||
expires_in,
|
||
expires_at,
|
||
reason,
|
||
} => cmd_ban(client, &pubkey, expires_in, expires_at, reason.as_deref()).await,
|
||
ModerationCmd::Unban { pubkey } => cmd_unban(client, &pubkey).await,
|
||
ModerationCmd::Timeout {
|
||
pubkey,
|
||
expires_in,
|
||
expires_at,
|
||
reason,
|
||
} => cmd_timeout(client, &pubkey, expires_in, expires_at, reason.as_deref()).await,
|
||
ModerationCmd::Untimeout { pubkey } => cmd_untimeout(client, &pubkey).await,
|
||
ModerationCmd::Restricted => cmd_restricted(client).await,
|
||
ModerationCmd::Audit { limit } => cmd_audit(client, limit).await,
|
||
}
|
||
}
|