diff --git a/api/src/lib.rs b/api/src/lib.rs index 5af7c7c..6dbde30 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -744,6 +744,8 @@ pub struct ChannelView { pub entrymsg: String, // Channel homepage URL, shown in INFO (empty = none). pub url: String, + // Channel contact email, shown in INFO (empty = none). + pub email: String, // ChanServ SET options. pub signkick: bool, pub private: bool, @@ -1044,6 +1046,7 @@ pub trait Store { 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_url(&mut self, channel: &str, url: &str) -> Result<(), ChanError>; + fn set_channel_email(&mut self, channel: &str, email: &str) -> Result<(), ChanError>; fn set_channel_setting(&mut self, channel: &str, setting: ChanSetting, on: bool) -> Result<(), ChanError>; fn set_kicker(&mut self, channel: &str, kicker: Kicker, on: bool) -> Result<(), ChanError>; fn set_caps_kicker(&mut self, channel: &str, caps_min: u16, caps_percent: u16) -> Result<(), ChanError>; diff --git a/modules/chanserv/src/lib.rs b/modules/chanserv/src/lib.rs index 922a130..14d7c6d 100644 --- a/modules/chanserv/src/lib.rs +++ b/modules/chanserv/src/lib.rs @@ -51,7 +51,7 @@ const TOPICS: &[HelpEntry] = &[ HelpEntry { cmd: "REGISTER", summary: "register a channel", detail: "Syntax: \x02REGISTER <#channel>\x02\nRegisters a channel to you. You must currently hold ops in it." }, HelpEntry { cmd: "INFO", summary: "show channel information", detail: "Syntax: \x02INFO <#channel>\x02\nShows a channel's registration and settings." }, HelpEntry { cmd: "LIST", summary: "list registered channels", detail: "Syntax: \x02LIST\x02\nLists registered channels." }, - HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER | DESC | URL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." }, + HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER | DESC | URL [address] | EMAIL [address] | SUCCESSOR |OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." }, HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD | DEL \x02\nManages the channel access list." }, HelpEntry { cmd: "FLAGS", summary: "granular per-account flags", detail: "Syntax: \x02FLAGS <#channel> [account [+/-flags]]\x02\nViews or changes granular per-account channel flags." }, HelpEntry { cmd: "AOP/SOP/VOP", summary: "tiered access shortcuts", detail: "Syntax: \x02AOP|SOP|VOP <#channel> ADD | DEL | LIST\x02\nTiered shortcuts over ACCESS: AOP and SOP grant op, VOP grants voice." }, @@ -160,6 +160,9 @@ impl Service for ChanServ { if !info.url.is_empty() { ctx.notice(me, from.uid, format!(" URL : {}", info.url)); } + if !info.email.is_empty() { + ctx.notice(me, from.uid, format!(" Email : {}", info.email)); + } ctx.notice(me, from.uid, format!(" Registered : {}", echo_api::human_time(info.ts))); if let Some(s) = db.channel_suspension(chan) { ctx.notice(me, from.uid, format!(" Suspended : by \x02{}\x02 — {}", s.by, s.reason)); diff --git a/modules/chanserv/src/set.rs b/modules/chanserv/src/set.rs index 745432d..18f36c9 100644 --- a/modules/chanserv/src/set.rs +++ b/modules/chanserv/src/set.rs @@ -72,6 +72,14 @@ 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."), } } + Some("EMAIL") => { + let email = args.get(3).copied().unwrap_or(""); + match db.set_channel_email(chan, email) { + Ok(()) if email.is_empty() => ctx.notice(me, from.uid, format!("Contact email for \x02{chan}\x02 cleared.")), + Ok(()) => ctx.notice(me, from.uid, format!("Contact email for \x02{chan}\x02 updated.")), + Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), + } + } Some("SIGNKICK") => toggle(me, from, ctx, db, chan, ChanSetting::SignKick, args.get(3).copied()), Some("PRIVATE") => toggle(me, from, ctx, db, chan, ChanSetting::Private, args.get(3).copied()), Some("PEACE") => toggle(me, from, ctx, db, chan, ChanSetting::Peace, args.get(3).copied()), @@ -80,7 +88,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: Some("AUTOOP") => toggle(me, from, ctx, db, chan, ChanSetting::AutoOp, args.get(3).copied()), Some("KEEPTOPIC") => toggle(me, from, ctx, db, chan, ChanSetting::KeepTopic, args.get(3).copied()), Some("TOPICLOCK") => toggle(me, from, ctx, db, chan, ChanSetting::TopicLock, args.get(3).copied()), - _ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER | SUCCESSOR |OFF | DESC | URL [address] | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF} | PEACE {ON|OFF} | SECUREOPS {ON|OFF} | RESTRICTED {ON|OFF} | AUTOOP {ON|OFF} | KEEPTOPIC {ON|OFF} | TOPICLOCK {ON|OFF}"), + _ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER | SUCCESSOR |OFF | DESC | URL [address] | EMAIL [address] | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF} | PEACE {ON|OFF} | SECUREOPS {ON|OFF} | RESTRICTED {ON|OFF} | AUTOOP {ON|OFF} | KEEPTOPIC {ON|OFF} | TOPICLOCK {ON|OFF}"), } } diff --git a/src/engine/db/channel.rs b/src/engine/db/channel.rs index 6c99899..90913d7 100644 --- a/src/engine/db/channel.rs +++ b/src/engine/db/channel.rs @@ -11,7 +11,7 @@ impl Db { self.log .append(Event::ChannelRegistered { name: name.to_string(), founder: founder.to_string(), ts }) .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(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), settings: ChanSettings::default(), topic: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None }); + 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(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), email: String::new(), settings: ChanSettings::default(), topic: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None }); Ok(()) } @@ -199,6 +199,18 @@ impl Db { Ok(()) } + pub fn set_channel_email(&mut self, channel: &str, email: &str) -> Result<(), ChanError> { + let k = key(channel); + if !self.channels.contains_key(&k) { + return Err(ChanError::NoChannel); + } + self.log + .append(Event::ChannelEmailSet { channel: channel.to_string(), email: email.to_string() }) + .map_err(|_| ChanError::Internal)?; + self.channels.get_mut(&k).unwrap().email = email.to_string(); + 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); diff --git a/src/engine/db/event.rs b/src/engine/db/event.rs index a165431..efd4e59 100644 --- a/src/engine/db/event.rs +++ b/src/engine/db/event.rs @@ -49,6 +49,7 @@ pub enum Event { ChannelDescSet { channel: String, desc: String }, ChannelEntryMsgSet { channel: String, msg: String }, ChannelUrlSet { channel: String, url: String }, + ChannelEmailSet { channel: String, email: String }, ChannelSettingsSet { channel: String, settings: ChanSettings }, ChannelKickerSet { channel: String, kickers: KickerSettings }, ChannelBadwordsSet { channel: String, badwords: Vec }, @@ -210,6 +211,7 @@ impl Event { | Event::ChannelDescSet { .. } | Event::ChannelEntryMsgSet { .. } | Event::ChannelUrlSet { .. } + | Event::ChannelEmailSet { .. } | Event::ChannelSettingsSet { .. } | Event::ChannelKickerSet { .. } | Event::ChannelBadwordsSet { .. } @@ -395,7 +397,7 @@ pub(crate) fn apply(accounts: &mut HashMap, channels: &mut Hash grouped.remove(&key(&nick)); } 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(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), settings: ChanSettings::default(), topic: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None }); + channels.insert(key(&name), ChannelInfo { name, founder, ts, lock_on: String::new(), lock_off: String::new(), access: Vec::new(), akick: Vec::new(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), email: String::new(), settings: ChanSettings::default(), topic: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None }); } Event::ChannelDropped { name } => { channels.remove(&key(&name)); @@ -448,6 +450,11 @@ pub(crate) fn apply(accounts: &mut HashMap, channels: &mut Hash c.url = url; } } + Event::ChannelEmailSet { channel, email } => { + if let Some(c) = channels.get_mut(&key(&channel)) { + c.email = email; + } + } Event::ChannelSettingsSet { channel, settings } => { if let Some(c) = channels.get_mut(&key(&channel)) { c.settings = settings; diff --git a/src/engine/db/mod.rs b/src/engine/db/mod.rs index fd57e67..670943d 100644 --- a/src/engine/db/mod.rs +++ b/src/engine/db/mod.rs @@ -422,6 +422,9 @@ pub struct ChannelInfo { // Channel homepage URL, shown in INFO (empty = none). #[serde(default)] pub url: String, + // Channel contact email, shown in INFO (empty = none). + #[serde(default)] + pub email: String, // On/off options set via ChanServ SET. #[serde(default)] pub settings: ChanSettings, diff --git a/src/engine/db/store.rs b/src/engine/db/store.rs index 3601097..b808f9a 100644 --- a/src/engine/db/store.rs +++ b/src/engine/db/store.rs @@ -377,6 +377,9 @@ impl Store for Db { fn set_url(&mut self, channel: &str, url: &str) -> Result<(), ChanError> { Db::set_url(self, channel, url) } + fn set_channel_email(&mut self, channel: &str, email: &str) -> Result<(), ChanError> { + Db::set_channel_email(self, channel, email) + } fn set_channel_setting(&mut self, channel: &str, setting: ChanSetting, on: bool) -> Result<(), ChanError> { Db::set_channel_setting(self, channel, setting, on) } @@ -559,6 +562,7 @@ fn channel_view(c: &ChannelInfo) -> ChannelView { desc: c.desc.clone(), entrymsg: c.entrymsg.clone(), url: c.url.clone(), + email: c.email.clone(), signkick: c.settings.signkick, private: c.settings.private, peace: c.settings.peace, diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 62ffa2f..6f2deed 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1186,7 +1186,7 @@ fn audit_summary(event: &db::Event) -> Option { AjoinAdded { .. } | AjoinRemoved { .. } | AccountGreetSet { .. } | AccountAutoOpSet { .. } | AccountKillSet { .. } | AccountHideStatusSet { .. } | VhostRequested { .. } | VhostRequestCleared { .. } | MemoSent { .. } | MemoRead { .. } | MemoDeleted { .. } | MemoIgnoreAdd { .. } | MemoIgnoreDel { .. } | MemoPrefsSet { .. } - | ChannelMlock { .. } | ChannelDescSet { .. } | ChannelEntryMsgSet { .. } | ChannelUrlSet { .. } | ChannelSettingsSet { .. } + | ChannelMlock { .. } | ChannelDescSet { .. } | ChannelEntryMsgSet { .. } | ChannelUrlSet { .. } | ChannelEmailSet { .. } | ChannelSettingsSet { .. } | ChannelKickerSet { .. } | ChannelBadwordsSet { .. } | ChannelTriggersSet { .. } | ChannelTopicSet { .. } | AccountSeen { .. } | ChannelUsed { .. } | AccountExpiryWarned { .. } | ChannelExpiryWarned { .. } => return None, diff --git a/src/engine/tests.rs b/src/engine/tests.rs index fdbe60b..f69c9df 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -4204,6 +4204,12 @@ assert!(notice(&to_cs(&mut e, "000AAAAAB", "SET #c URL"), "cleared")); assert!(!notice(&to_cs(&mut e, "000AAAAAB", "INFO #c"), "example.org"), "cleared URL no longer shows"); + // Contact email is stored, shows in INFO, and clears when set empty. + assert!(notice(&to_cs(&mut e, "000AAAAAB", "SET #c EMAIL staff@example.org"), "updated")); + assert!(notice(&to_cs(&mut e, "000AAAAAB", "INFO #c"), "staff@example.org")); + assert!(notice(&to_cs(&mut e, "000AAAAAB", "SET #c EMAIL"), "cleared")); + assert!(!notice(&to_cs(&mut e, "000AAAAAB", "INFO #c"), "staff@example.org"), "cleared email no longer shows"); + // Transfer to a non-account is refused. assert!(notice(&to_cs(&mut e, "000AAAAAB", "SET #c FOUNDER nobody"), "isn't a registered account")); diff --git a/src/grpc.rs b/src/grpc.rs index 8ac5f7c..7c5774a 100644 --- a/src/grpc.rs +++ b/src/grpc.rs @@ -152,6 +152,7 @@ fn to_wire(entry: &LogEntry) -> Option { | Event::ChannelAkickDel { .. } | Event::ChannelEntryMsgSet { .. } | Event::ChannelUrlSet { .. } + | Event::ChannelEmailSet { .. } | Event::ChannelSuccessorSet { .. } | Event::ChannelSettingsSet { .. } | Event::ChannelKickerSet { .. }