chanserv: channel SUSPEND / UNSUSPEND (oper-gated)

Freezes a channel gated on Priv::Suspend: a typed Suspension on ChannelInfo
(event-logged, snapshotted, lazy expiry — same shape as the account one, no
Anope Extensible/Checker or expiry timer). While suspended, ChanServ refuses
all management (guards in require_op/require_founder and the founder-inline
SET/DROP/MLOCK), the engine skips Join enforcement (no auto-op/akick/entrymsg),
and suspending kicks everyone out; INFO shows it. parse_duration moved into the
SDK so both SUSPEND commands share it. Data + full-flow tests.
This commit is contained in:
Jean Chevronnet 2026-07-13 04:17:17 +00:00
parent bbbe2c6cb8
commit cb081a2e95
No known key found for this signature in database
8 changed files with 241 additions and 18 deletions

View file

@ -1,4 +1,4 @@
use fedserv_api::{NetView, Priv, Sender, ServiceCtx, Store};
use fedserv_api::{parse_duration, NetView, Priv, Sender, ServiceCtx, Store};
use std::time::{SystemTime, UNIX_EPOCH};
// SUSPEND <account> [+expiry] [reason] / UNSUSPEND <account>: freeze or unfreeze
@ -49,19 +49,6 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
}
}
// Parse a duration like "30d", "12h", "45m", "90s", or a bare number of seconds.
fn parse_duration(s: &str) -> Option<u64> {
let (num, mult) = match s.chars().last()? {
'd' | 'D' => (&s[..s.len() - 1], 86_400),
'h' | 'H' => (&s[..s.len() - 1], 3_600),
'm' | 'M' => (&s[..s.len() - 1], 60),
's' | 'S' => (&s[..s.len() - 1], 1),
c if c.is_ascii_digit() => (s, 1),
_ => return None,
};
num.parse::<u64>().ok().map(|n| n * mult)
}
fn now_unix() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}