chanserv: SET SIGNKICK and PRIVATE (typed channel settings)
Introduce a typed ChanSettings struct + ChanSetting enum for ChanServ's on/off options instead of a bag of string flags, so a new option is one field and the compiler proves every site handles it. First two, both functional: SIGNKICK attributes ChanServ kicks with who requested them; PRIVATE hides the channel from LIST. Folded through the event log like the other channel state (node-local, no gossip), shown in INFO. One ChannelSettingsSet event, one Store method.
This commit is contained in:
parent
f388e3d650
commit
75019c867a
7 changed files with 114 additions and 10 deletions
|
|
@ -291,6 +291,16 @@ pub struct ChannelView {
|
||||||
pub akick: Vec<ChanAkickView>,
|
pub akick: Vec<ChanAkickView>,
|
||||||
pub desc: String,
|
pub desc: String,
|
||||||
pub entrymsg: String,
|
pub entrymsg: String,
|
||||||
|
// ChanServ SET options.
|
||||||
|
pub signkick: bool,
|
||||||
|
pub private: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single ChanServ SET option, named for the typed `set_channel_setting` call.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ChanSetting {
|
||||||
|
SignKick,
|
||||||
|
Private,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelView {
|
impl ChannelView {
|
||||||
|
|
@ -405,6 +415,7 @@ pub trait Store {
|
||||||
fn drop_channel(&mut self, name: &str) -> Result<(), ChanError>;
|
fn drop_channel(&mut self, name: &str) -> Result<(), ChanError>;
|
||||||
fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError>;
|
fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError>;
|
||||||
fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError>;
|
fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError>;
|
||||||
|
fn set_channel_setting(&mut self, channel: &str, setting: ChanSetting, on: bool) -> Result<(), ChanError>;
|
||||||
fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError>;
|
fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError>;
|
||||||
fn set_founder(&mut self, channel: &str, account: &str) -> Result<(), ChanError>;
|
fn set_founder(&mut self, channel: &str, account: &str) -> Result<(), ChanError>;
|
||||||
fn access_add(&mut self, channel: &str, account: &str, level: &str) -> Result<(), ChanError>;
|
fn access_add(&mut self, channel: &str, account: &str, level: &str) -> Result<(), ChanError>;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here."));
|
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here."));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let reason = if args.len() > 3 { args[3..].join(" ") } else { "Kicked".to_string() };
|
let mut reason = if args.len() > 3 { args[3..].join(" ") } else { "Kicked".to_string() };
|
||||||
|
// SIGNKICK: attribute the kick to whoever asked for it.
|
||||||
|
if db.channel(chan).is_some_and(|c| c.signkick) {
|
||||||
|
reason = format!("{reason} (requested by {})", from.nick);
|
||||||
|
}
|
||||||
ctx.kick(me, chan, target, &reason);
|
ctx.kick(me, chan, target, &reason);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,12 @@ impl Service for ChanServ {
|
||||||
ctx.notice(me, from.uid, format!(" Description: {}", info.desc));
|
ctx.notice(me, from.uid, format!(" Description: {}", info.desc));
|
||||||
}
|
}
|
||||||
ctx.notice(me, from.uid, format!(" Registered : {}", fedserv_api::human_time(info.ts)));
|
ctx.notice(me, from.uid, format!(" Registered : {}", fedserv_api::human_time(info.ts)));
|
||||||
|
let mut opts = Vec::new();
|
||||||
|
if info.signkick { opts.push("SIGNKICK"); }
|
||||||
|
if info.private { opts.push("PRIVATE"); }
|
||||||
|
if !opts.is_empty() {
|
||||||
|
ctx.notice(me, from.uid, format!(" Options : {}", opts.join(", ")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
|
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ use fedserv_api::{Sender, ServiceCtx};
|
||||||
|
|
||||||
// LIST: show all registered channels.
|
// LIST: show all registered channels.
|
||||||
pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) {
|
pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) {
|
||||||
let mut names: Vec<String> = db.channels().into_iter().map(|c| c.name).collect();
|
// PRIVATE channels are hidden from LIST.
|
||||||
|
let mut names: Vec<String> = db.channels().into_iter().filter(|c| !c.private).map(|c| c.name).collect();
|
||||||
if names.is_empty() {
|
if names.is_empty() {
|
||||||
ctx.notice(me, from.uid, "No channels are registered.");
|
ctx.notice(me, from.uid, "No channels are registered.");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use fedserv_api::Store;
|
use fedserv_api::{ChanSetting, Sender, ServiceCtx, Store};
|
||||||
use fedserv_api::{Sender, ServiceCtx};
|
|
||||||
|
|
||||||
// SET <#channel> FOUNDER <account> | DESC <text>: founder-only channel settings.
|
// SET <#channel> FOUNDER <account> | DESC <text>: founder-only channel settings.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
|
|
@ -40,6 +39,24 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text>"),
|
Some("SIGNKICK") => toggle(me, from, ctx, db, chan, ChanSetting::SignKick, args.get(3).copied(), "SIGNKICK"),
|
||||||
|
Some("PRIVATE") => toggle(me, from, ctx, db, chan, ChanSetting::Private, args.get(3).copied(), "PRIVATE"),
|
||||||
|
_ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text> | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip one on/off channel option, reporting the new state.
|
||||||
|
fn toggle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store, chan: &str, setting: ChanSetting, arg: Option<&str>, name: &str) {
|
||||||
|
let on = match arg.map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
Some("ON") => true,
|
||||||
|
Some("OFF") => false,
|
||||||
|
_ => {
|
||||||
|
ctx.notice(me, from.uid, format!("Syntax: SET <#channel> {name} {{ON|OFF}}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match db.set_channel_setting(chan, setting, on) {
|
||||||
|
Ok(()) => ctx.notice(me, from.uid, format!("\x02{name}\x02 for \x02{chan}\x02 is now \x02{}\x02.", if on { "on" } else { "off" })),
|
||||||
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use super::scram::{self, Hash};
|
||||||
// fedserv-api SDK crate; re-exported so the engine keeps naming them locally and
|
// fedserv-api SDK crate; re-exported so the engine keeps naming them locally and
|
||||||
// modules importing `crate::engine::db::{ChanError, ...}` are unaffected.
|
// modules importing `crate::engine::db::{ChanError, ...}` are unaffected.
|
||||||
pub use fedserv_api::{
|
pub use fedserv_api::{
|
||||||
AccountView, AjoinView, ChanAccessView, ChanAkickView, ChanError, ChannelView, CertError, CodeKind, RegError, Store,
|
AccountView, AjoinView, ChanAccessView, ChanAkickView, ChanError, ChanSetting, ChannelView, CertError, CodeKind, RegError, Store,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -82,6 +82,7 @@ pub enum Event {
|
||||||
ChannelFounderSet { channel: String, founder: String },
|
ChannelFounderSet { channel: String, founder: String },
|
||||||
ChannelDescSet { channel: String, desc: String },
|
ChannelDescSet { channel: String, desc: String },
|
||||||
ChannelEntryMsgSet { channel: String, msg: String },
|
ChannelEntryMsgSet { channel: String, msg: String },
|
||||||
|
ChannelSettingsSet { channel: String, settings: ChanSettings },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether an event replicates across the federation. Account identity is Global
|
// Whether an event replicates across the federation. Account identity is Global
|
||||||
|
|
@ -117,7 +118,8 @@ impl Event {
|
||||||
| Event::ChannelAkickDel { .. }
|
| Event::ChannelAkickDel { .. }
|
||||||
| Event::ChannelFounderSet { .. }
|
| Event::ChannelFounderSet { .. }
|
||||||
| Event::ChannelDescSet { .. }
|
| Event::ChannelDescSet { .. }
|
||||||
| Event::ChannelEntryMsgSet { .. } => Scope::Local,
|
| Event::ChannelEntryMsgSet { .. }
|
||||||
|
| Event::ChannelSettingsSet { .. } => Scope::Local,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +147,18 @@ pub struct AjoinEntry {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A channel's on/off options (ChanServ SET). Typed, not a bag of string flags,
|
||||||
|
// so a new option is one field and the compiler proves every place handles it.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||||
|
pub struct ChanSettings {
|
||||||
|
// Append "(requested by <nick>)" to ChanServ KICK reasons.
|
||||||
|
#[serde(default)]
|
||||||
|
pub signkick: bool,
|
||||||
|
// Hide the channel from ChanServ LIST.
|
||||||
|
#[serde(default)]
|
||||||
|
pub private: bool,
|
||||||
|
}
|
||||||
|
|
||||||
// A registered channel and who owns it.
|
// A registered channel and who owns it.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ChannelInfo {
|
pub struct ChannelInfo {
|
||||||
|
|
@ -166,6 +180,9 @@ pub struct ChannelInfo {
|
||||||
// Message noticed to users as they join.
|
// Message noticed to users as they join.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub entrymsg: String,
|
pub entrymsg: String,
|
||||||
|
// On/off options set via ChanServ SET.
|
||||||
|
#[serde(default)]
|
||||||
|
pub settings: ChanSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelInfo {
|
impl ChannelInfo {
|
||||||
|
|
@ -569,6 +586,9 @@ impl Db {
|
||||||
if !c.entrymsg.is_empty() {
|
if !c.entrymsg.is_empty() {
|
||||||
snapshot.push(Event::ChannelEntryMsgSet { channel: c.name.clone(), msg: c.entrymsg.clone() });
|
snapshot.push(Event::ChannelEntryMsgSet { channel: c.name.clone(), msg: c.entrymsg.clone() });
|
||||||
}
|
}
|
||||||
|
if c.settings.signkick || c.settings.private {
|
||||||
|
snapshot.push(Event::ChannelSettingsSet { channel: c.name.clone(), settings: c.settings });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (nick, account) in &self.grouped {
|
for (nick, account) in &self.grouped {
|
||||||
snapshot.push(Event::NickGrouped { nick: nick.clone(), account: account.clone() });
|
snapshot.push(Event::NickGrouped { nick: nick.clone(), account: account.clone() });
|
||||||
|
|
@ -951,7 +971,7 @@ impl Db {
|
||||||
self.log
|
self.log
|
||||||
.append(Event::ChannelRegistered { name: name.to_string(), founder: founder.to_string(), ts })
|
.append(Event::ChannelRegistered { name: name.to_string(), founder: founder.to_string(), ts })
|
||||||
.map_err(|_| ChanError::Internal)?;
|
.map_err(|_| ChanError::Internal)?;
|
||||||
self.channels.insert(k, ChannelInfo { name: name.to_string(), founder: founder.to_string(), ts, lock_on: String::new(), lock_off: String::new(), access: Vec::new(), akick: Vec::new(), desc: String::new(), entrymsg: String::new() });
|
self.channels.insert(k, ChannelInfo { name: name.to_string(), founder: founder.to_string(), ts, lock_on: String::new(), lock_off: String::new(), access: Vec::new(), akick: Vec::new(), desc: String::new(), entrymsg: String::new(), settings: ChanSettings::default() });
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1087,6 +1107,22 @@ impl Db {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn one ChanServ SET option on or off for a channel.
|
||||||
|
pub fn set_channel_setting(&mut self, channel: &str, setting: ChanSetting, on: bool) -> Result<(), ChanError> {
|
||||||
|
let k = key(channel);
|
||||||
|
let Some(c) = self.channels.get(&k) else { return Err(ChanError::NoChannel) };
|
||||||
|
let mut settings = c.settings;
|
||||||
|
match setting {
|
||||||
|
ChanSetting::SignKick => settings.signkick = on,
|
||||||
|
ChanSetting::Private => settings.private = on,
|
||||||
|
}
|
||||||
|
self.log
|
||||||
|
.append(Event::ChannelSettingsSet { channel: channel.to_string(), settings })
|
||||||
|
.map_err(|_| ChanError::Internal)?;
|
||||||
|
self.channels.get_mut(&k).unwrap().settings = settings;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Set `channel`'s entry message (empty clears it).
|
/// Set `channel`'s entry message (empty clears it).
|
||||||
pub fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError> {
|
pub fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError> {
|
||||||
let k = key(channel);
|
let k = key(channel);
|
||||||
|
|
@ -1188,7 +1224,7 @@ fn apply(accounts: &mut HashMap<String, Account>, channels: &mut HashMap<String,
|
||||||
grouped.remove(&key(&nick));
|
grouped.remove(&key(&nick));
|
||||||
}
|
}
|
||||||
Event::ChannelRegistered { name, founder, ts } => {
|
Event::ChannelRegistered { name, founder, ts } => {
|
||||||
channels.insert(key(&name), ChannelInfo { name, founder, ts, lock_on: String::new(), lock_off: String::new(), access: Vec::new(), akick: Vec::new(), desc: String::new(), entrymsg: String::new() });
|
channels.insert(key(&name), ChannelInfo { name, founder, ts, lock_on: String::new(), lock_off: String::new(), access: Vec::new(), akick: Vec::new(), desc: String::new(), entrymsg: String::new(), settings: ChanSettings::default() });
|
||||||
}
|
}
|
||||||
Event::ChannelDropped { name } => {
|
Event::ChannelDropped { name } => {
|
||||||
channels.remove(&key(&name));
|
channels.remove(&key(&name));
|
||||||
|
|
@ -1231,6 +1267,11 @@ fn apply(accounts: &mut HashMap<String, Account>, channels: &mut HashMap<String,
|
||||||
c.desc = desc;
|
c.desc = desc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Event::ChannelSettingsSet { channel, settings } => {
|
||||||
|
if let Some(c) = channels.get_mut(&key(&channel)) {
|
||||||
|
c.settings = settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::ChannelEntryMsgSet { channel, msg } => {
|
Event::ChannelEntryMsgSet { channel, msg } => {
|
||||||
if let Some(c) = channels.get_mut(&key(&channel)) {
|
if let Some(c) = channels.get_mut(&key(&channel)) {
|
||||||
c.entrymsg = msg;
|
c.entrymsg = msg;
|
||||||
|
|
@ -1402,6 +1443,9 @@ impl Store for Db {
|
||||||
fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError> {
|
fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError> {
|
||||||
Db::set_desc(self, channel, desc)
|
Db::set_desc(self, channel, desc)
|
||||||
}
|
}
|
||||||
|
fn set_channel_setting(&mut self, channel: &str, setting: ChanSetting, on: bool) -> Result<(), ChanError> {
|
||||||
|
Db::set_channel_setting(self, channel, setting, on)
|
||||||
|
}
|
||||||
fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError> {
|
fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError> {
|
||||||
Db::set_entrymsg(self, channel, msg)
|
Db::set_entrymsg(self, channel, msg)
|
||||||
}
|
}
|
||||||
|
|
@ -1433,6 +1477,8 @@ fn channel_view(c: &ChannelInfo) -> ChannelView {
|
||||||
akick: c.akick.iter().map(|k| ChanAkickView { mask: k.mask.clone(), reason: k.reason.clone() }).collect(),
|
akick: c.akick.iter().map(|k| ChanAkickView { mask: k.mask.clone(), reason: k.reason.clone() }).collect(),
|
||||||
desc: c.desc.clone(),
|
desc: c.desc.clone(),
|
||||||
entrymsg: c.entrymsg.clone(),
|
entrymsg: c.entrymsg.clone(),
|
||||||
|
signkick: c.settings.signkick,
|
||||||
|
private: c.settings.private,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1549,6 +1595,24 @@ mod tests {
|
||||||
assert_eq!(db.ajoin_list("alice")[0].channel, "#dev");
|
assert_eq!(db.ajoin_list("alice")[0].channel, "#dev");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Channel SET options default off, toggle, and replay from the log.
|
||||||
|
#[test]
|
||||||
|
fn channel_settings_toggle_and_persist() {
|
||||||
|
let p = tmp("chansettings");
|
||||||
|
{
|
||||||
|
let mut db = Db::open(&p, "N1");
|
||||||
|
db.register_channel("#c", "boss").unwrap();
|
||||||
|
assert!(!db.channel("#c").unwrap().settings.signkick, "defaults off");
|
||||||
|
db.set_channel_setting("#c", ChanSetting::SignKick, true).unwrap();
|
||||||
|
db.set_channel_setting("#c", ChanSetting::Private, true).unwrap();
|
||||||
|
db.set_channel_setting("#c", ChanSetting::Private, false).unwrap();
|
||||||
|
let s = db.channel("#c").unwrap().settings;
|
||||||
|
assert!(s.signkick && !s.private, "one flag on, the other flipped back off");
|
||||||
|
}
|
||||||
|
let s = Db::open(&p, "N1").channel("#c").unwrap().settings;
|
||||||
|
assert!(s.signkick && !s.private, "settings replay from the log");
|
||||||
|
}
|
||||||
|
|
||||||
// A wrong code is tolerated a few times, then the code is burned so it can't
|
// A wrong code is tolerated a few times, then the code is burned so it can't
|
||||||
// be ground down online even though it is short.
|
// be ground down online even though it is short.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,8 @@ fn to_wire(entry: &LogEntry) -> Option<ReplicationEvent> {
|
||||||
| Event::ChannelAccessDel { .. }
|
| Event::ChannelAccessDel { .. }
|
||||||
| Event::ChannelAkickAdd { .. }
|
| Event::ChannelAkickAdd { .. }
|
||||||
| Event::ChannelAkickDel { .. }
|
| Event::ChannelAkickDel { .. }
|
||||||
| Event::ChannelEntryMsgSet { .. } => return None,
|
| Event::ChannelEntryMsgSet { .. }
|
||||||
|
| Event::ChannelSettingsSet { .. } => return None,
|
||||||
};
|
};
|
||||||
Some(ReplicationEvent { origin: entry.origin().to_string(), seq: entry.seq(), lamport: entry.lamport(), kind: Some(kind) })
|
Some(ReplicationEvent { origin: entry.origin().to_string(), seq: entry.seq(), lamport: entry.lamport(), kind: Some(kind) })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue