chanserv: support param mode locks (+f flood, +j joinflood)
Some checks failed
CI / check (push) Has been cancelled
Some checks failed
CI / check (push) Has been cancelled
Mode locks were char-only, so Anope FLOOD/JOINFLOOD locks were dropped on migration. Carry a value per locked param-mode through the event log, MLOCK parsing/display, drift enforcement, compaction, and the Anope importer, so a migrated network keeps its flood protection verbatim.
This commit is contained in:
parent
3f97a66dbb
commit
9d94dd5eb0
9 changed files with 168 additions and 26 deletions
|
|
@ -752,6 +752,9 @@ pub struct ChannelView {
|
||||||
// Mode-lock: chars services keep set / unset (besides the implicit +r).
|
// Mode-lock: chars services keep set / unset (besides the implicit +r).
|
||||||
pub lock_on: String,
|
pub lock_on: String,
|
||||||
pub lock_off: String,
|
pub lock_off: String,
|
||||||
|
// Param values for locked param-modes (+f flood, +j joinflood), in the order
|
||||||
|
// their mode chars appear in `lock_on`.
|
||||||
|
pub lock_params: Vec<(char, String)>,
|
||||||
pub access: Vec<ChanAccessView>,
|
pub access: Vec<ChanAccessView>,
|
||||||
pub akick: Vec<ChanAkickView>,
|
pub akick: Vec<ChanAkickView>,
|
||||||
pub desc: String,
|
pub desc: String,
|
||||||
|
|
@ -856,6 +859,13 @@ impl ChannelView {
|
||||||
s.push('-');
|
s.push('-');
|
||||||
s.push_str(&self.lock_off);
|
s.push_str(&self.lock_off);
|
||||||
}
|
}
|
||||||
|
// Param modes take their argument as a trailing token, in mode-char order.
|
||||||
|
for ch in self.lock_on.chars() {
|
||||||
|
if let Some((_, param)) = self.lock_params.iter().find(|(c, _)| *c == ch) {
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1059,7 +1069,7 @@ pub trait Store {
|
||||||
fn session_exceptions(&self) -> Vec<(String, u32, String)>;
|
fn session_exceptions(&self) -> Vec<(String, u32, String)>;
|
||||||
fn register_channel(&mut self, name: &str, founder: &str) -> Result<(), ChanError>;
|
fn register_channel(&mut self, name: &str, founder: &str) -> Result<(), ChanError>;
|
||||||
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, params: Vec<(char, String)>) -> 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_url(&mut self, channel: &str, url: &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_email(&mut self, channel: &str, email: &str) -> Result<(), ChanError>;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
|
||||||
ctx.notice(me, from.uid, "You must be the founder of both channels.");
|
ctx.notice(me, from.uid, "You must be the founder of both channels.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let _ = db.set_mlock(dest, &sinfo.lock_on, &sinfo.lock_off);
|
let _ = db.set_mlock(dest, &sinfo.lock_on, &sinfo.lock_off, sinfo.lock_params.clone());
|
||||||
for a in &sinfo.access {
|
for a in &sinfo.access {
|
||||||
let _ = db.access_add(dest, &a.account, &a.level);
|
let _ = db.access_add(dest, &a.account, &a.level);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,8 +247,8 @@ impl Service for ChanServ {
|
||||||
if suspended_block(me, from, chan, ctx, db) {
|
if suspended_block(me, from, chan, ctx, db) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let (on, off) = parse_mlock(&args[2..].concat());
|
let (on, off, params) = parse_mlock(&args[2..]);
|
||||||
match db.set_mlock(chan, &on, &off) {
|
match db.set_mlock(chan, &on, &off, params) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Some(info) = db.channel(chan) {
|
if let Some(info) = db.channel(chan) {
|
||||||
ctx.channel_mode(me, chan, &info.lock_modes()); // apply it now
|
ctx.channel_mode(me, chan, &info.lock_modes()); // apply it now
|
||||||
|
|
@ -369,10 +369,21 @@ fn peace_blocks(me: &str, from: &Sender, chan: &str, target_uid: &str, ctx: &mut
|
||||||
|
|
||||||
// Parse a mode spec like "+nt-s" into (locked-on, locked-off) mode chars. `r` is
|
// Parse a mode spec like "+nt-s" into (locked-on, locked-off) mode chars. `r` is
|
||||||
// implicit for a registered channel and is ignored here.
|
// implicit for a registered channel and is ignored here.
|
||||||
fn parse_mlock(spec: &str) -> (String, String) {
|
// Locked add-modes that carry a param (+f flood, +j joinflood): a param is
|
||||||
|
// consumed from the trailing tokens, IRC MODE order.
|
||||||
|
fn is_param_mode(c: char) -> bool {
|
||||||
|
matches!(c, 'f' | 'j')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse `+ntf-s <param...>` into (on, off, params). Tokens after the mode string
|
||||||
|
// supply, in order, a value for each added param mode.
|
||||||
|
fn parse_mlock(tokens: &[&str]) -> (String, String, Vec<(char, String)>) {
|
||||||
let (mut on, mut off) = (String::new(), String::new());
|
let (mut on, mut off) = (String::new(), String::new());
|
||||||
|
let mut params: Vec<(char, String)> = Vec::new();
|
||||||
let mut adding = true;
|
let mut adding = true;
|
||||||
for ch in spec.chars() {
|
let modes = tokens.first().copied().unwrap_or("");
|
||||||
|
let mut rest = tokens.iter().skip(1);
|
||||||
|
for ch in modes.chars() {
|
||||||
match ch {
|
match ch {
|
||||||
'+' => adding = true,
|
'+' => adding = true,
|
||||||
'-' => adding = false,
|
'-' => adding = false,
|
||||||
|
|
@ -380,8 +391,17 @@ fn parse_mlock(spec: &str) -> (String, String) {
|
||||||
m if m.is_ascii_alphabetic() => {
|
m if m.is_ascii_alphabetic() => {
|
||||||
on.retain(|c| c != m);
|
on.retain(|c| c != m);
|
||||||
off.retain(|c| c != m);
|
off.retain(|c| c != m);
|
||||||
|
params.retain(|(c, _)| *c != m);
|
||||||
if adding {
|
if adding {
|
||||||
on.push(m);
|
on.push(m);
|
||||||
|
if is_param_mode(m) {
|
||||||
|
if let Some(&p) = rest.next() {
|
||||||
|
params.push((m, p.to_string()));
|
||||||
|
} else {
|
||||||
|
// No value supplied: drop the mode rather than lock a paramless +f.
|
||||||
|
on.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
off.push(m);
|
off.push(m);
|
||||||
}
|
}
|
||||||
|
|
@ -389,10 +409,10 @@ fn parse_mlock(spec: &str) -> (String, String) {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(on, off)
|
(on, off, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render a channel's lock as "+on-off" for display.
|
// Render a channel's lock as "+on-off <params>" for display.
|
||||||
fn show_mlock(info: &ChannelView) -> String {
|
fn show_mlock(info: &ChannelView) -> String {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
if !info.lock_on.is_empty() {
|
if !info.lock_on.is_empty() {
|
||||||
|
|
@ -403,6 +423,45 @@ fn show_mlock(info: &ChannelView) -> String {
|
||||||
s.push('-');
|
s.push('-');
|
||||||
s.push_str(&info.lock_off);
|
s.push_str(&info.lock_off);
|
||||||
}
|
}
|
||||||
|
for (_, param) in &info.lock_params {
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(param);
|
||||||
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::parse_mlock;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_plain_modes() {
|
||||||
|
let (on, off, params) = parse_mlock(&["+nt-s"]);
|
||||||
|
assert_eq!(on, "nt");
|
||||||
|
assert_eq!(off, "s");
|
||||||
|
assert!(params.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captures_param_mode_value() {
|
||||||
|
let (on, off, params) = parse_mlock(&["+ntf", "mute:7:8s"]);
|
||||||
|
assert_eq!(on, "ntf");
|
||||||
|
assert_eq!(off, "");
|
||||||
|
assert_eq!(params, vec![('f', "mute:7:8s".to_string())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_param_modes_consume_tokens_in_order() {
|
||||||
|
let (on, _off, params) = parse_mlock(&["+fj", "mute:7:8s", "10:6"]);
|
||||||
|
assert_eq!(on, "fj");
|
||||||
|
assert_eq!(params, vec![('f', "mute:7:8s".to_string()), ('j', "10:6".to_string())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_mode_without_value_is_dropped() {
|
||||||
|
let (on, _off, params) = parse_mlock(&["+ntf"]);
|
||||||
|
assert_eq!(on, "nt", "paramless +f is not locked");
|
||||||
|
assert!(params.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,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(), 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 });
|
self.channels.insert(k, ChannelInfo { name: name.to_string(), founder: founder.to_string(), ts, lock_on: String::new(), lock_off: String::new(), lock_params: Vec::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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,15 +45,21 @@ impl Db {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the mode-lock (chars to keep set / unset) for a registered channel.
|
/// Set the mode-lock (chars to keep set / unset) for a registered channel.
|
||||||
|
#[cfg(test)]
|
||||||
pub fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError> {
|
pub fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError> {
|
||||||
|
self.set_mlock_params(name, on, off, Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mlock_params(&mut self, name: &str, on: &str, off: &str, params: Vec<(char, String)>) -> Result<(), ChanError> {
|
||||||
let k = key(name);
|
let k = key(name);
|
||||||
if !self.channels.contains_key(&k) {
|
if !self.channels.contains_key(&k) {
|
||||||
return Err(ChanError::NoChannel);
|
return Err(ChanError::NoChannel);
|
||||||
}
|
}
|
||||||
self.log
|
self.log
|
||||||
.append(Event::ChannelMlock { name: name.to_string(), on: on.to_string(), off: off.to_string() })
|
.append(Event::ChannelMlock { name: name.to_string(), on: on.to_string(), off: off.to_string(), params: params.clone() })
|
||||||
.map_err(|_| ChanError::Internal)?;
|
.map_err(|_| ChanError::Internal)?;
|
||||||
let c = self.channels.get_mut(&k).unwrap();
|
let c = self.channels.get_mut(&k).unwrap();
|
||||||
|
c.lock_params = params;
|
||||||
c.lock_on = on.to_string();
|
c.lock_on = on.to_string();
|
||||||
c.lock_off = off.to_string();
|
c.lock_off = off.to_string();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ pub enum Event {
|
||||||
NickUngrouped { nick: String },
|
NickUngrouped { nick: String },
|
||||||
ChannelRegistered { name: String, founder: String, ts: u64 },
|
ChannelRegistered { name: String, founder: String, ts: u64 },
|
||||||
ChannelDropped { name: String },
|
ChannelDropped { name: String },
|
||||||
ChannelMlock { name: String, on: String, off: String },
|
ChannelMlock { name: String, on: String, off: String, #[serde(default)] params: Vec<(char, String)> },
|
||||||
ChannelAccessAdd { channel: String, account: String, level: String },
|
ChannelAccessAdd { channel: String, account: String, level: String },
|
||||||
ChannelAccessDel { channel: String, account: String },
|
ChannelAccessDel { channel: String, account: String },
|
||||||
ChannelAkickAdd { channel: String, mask: String, reason: String },
|
ChannelAkickAdd { channel: String, mask: String, reason: String },
|
||||||
|
|
@ -429,15 +429,16 @@ pub(crate) fn apply(accounts: &mut HashMap<String, Account>, channels: &mut Hash
|
||||||
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(), 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 });
|
channels.insert(key(&name), ChannelInfo { name, founder, ts, lock_on: String::new(), lock_off: String::new(), lock_params: Vec::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 } => {
|
Event::ChannelDropped { name } => {
|
||||||
channels.remove(&key(&name));
|
channels.remove(&key(&name));
|
||||||
}
|
}
|
||||||
Event::ChannelMlock { name, on, off } => {
|
Event::ChannelMlock { name, on, off, params } => {
|
||||||
if let Some(c) = channels.get_mut(&key(&name)) {
|
if let Some(c) = channels.get_mut(&key(&name)) {
|
||||||
c.lock_on = on;
|
c.lock_on = on;
|
||||||
c.lock_off = off;
|
c.lock_off = off;
|
||||||
|
c.lock_params = params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::ChannelAccessAdd { channel, account, level } => {
|
Event::ChannelAccessAdd { channel, account, level } => {
|
||||||
|
|
|
||||||
|
|
@ -408,6 +408,10 @@ pub struct ChannelInfo {
|
||||||
pub lock_on: String,
|
pub lock_on: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub lock_off: String,
|
pub lock_off: String,
|
||||||
|
// Params for locked +modes that take one (e.g. ('f', "mute:7:8s"), ('j', "10:6")).
|
||||||
|
// Each char here is also in `lock_on`; applied and re-asserted with its param.
|
||||||
|
#[serde(default)]
|
||||||
|
pub lock_params: Vec<(char, String)>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub access: Vec<ChanAccess>,
|
pub access: Vec<ChanAccess>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -613,6 +617,13 @@ impl ChannelInfo {
|
||||||
s.push('-');
|
s.push('-');
|
||||||
s.push_str(&self.lock_off);
|
s.push_str(&self.lock_off);
|
||||||
}
|
}
|
||||||
|
// Params for locked +modes that take one, in the order they appear in lock_on.
|
||||||
|
for ch in self.lock_on.chars() {
|
||||||
|
if let Some((_, p)) = self.lock_params.iter().find(|(c, _)| *c == ch) {
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -647,6 +658,13 @@ impl ChannelInfo {
|
||||||
s.push('-');
|
s.push('-');
|
||||||
s.push_str(&reremove);
|
s.push_str(&reremove);
|
||||||
}
|
}
|
||||||
|
// A re-asserted param mode (e.g. +f) must carry its locked param.
|
||||||
|
for ch in readd.chars() {
|
||||||
|
if let Some((_, p)) = self.lock_params.iter().find(|(c, _)| *c == ch) {
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(s)
|
Some(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1087,7 +1105,7 @@ impl Db {
|
||||||
for c in self.channels.values() {
|
for c in self.channels.values() {
|
||||||
snapshot.push(Event::ChannelRegistered { name: c.name.clone(), founder: c.founder.clone(), ts: c.ts });
|
snapshot.push(Event::ChannelRegistered { name: c.name.clone(), founder: c.founder.clone(), ts: c.ts });
|
||||||
if !c.lock_on.is_empty() || !c.lock_off.is_empty() {
|
if !c.lock_on.is_empty() || !c.lock_off.is_empty() {
|
||||||
snapshot.push(Event::ChannelMlock { name: c.name.clone(), on: c.lock_on.clone(), off: c.lock_off.clone() });
|
snapshot.push(Event::ChannelMlock { name: c.name.clone(), on: c.lock_on.clone(), off: c.lock_off.clone(), params: c.lock_params.clone() });
|
||||||
}
|
}
|
||||||
for a in &c.access {
|
for a in &c.access {
|
||||||
snapshot.push(Event::ChannelAccessAdd { channel: c.name.clone(), account: a.account.clone(), level: a.level.clone() });
|
snapshot.push(Event::ChannelAccessAdd { channel: c.name.clone(), account: a.account.clone(), level: a.level.clone() });
|
||||||
|
|
|
||||||
|
|
@ -371,8 +371,8 @@ impl Store for Db {
|
||||||
fn drop_channel(&mut self, name: &str) -> Result<(), ChanError> {
|
fn drop_channel(&mut self, name: &str) -> Result<(), ChanError> {
|
||||||
Db::drop_channel(self, name)
|
Db::drop_channel(self, name)
|
||||||
}
|
}
|
||||||
fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError> {
|
fn set_mlock(&mut self, name: &str, on: &str, off: &str, params: Vec<(char, String)>) -> Result<(), ChanError> {
|
||||||
Db::set_mlock(self, name, on, off)
|
Db::set_mlock_params(self, name, on, off, params)
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|
@ -560,6 +560,7 @@ fn channel_view(c: &ChannelInfo) -> ChannelView {
|
||||||
ts: c.ts,
|
ts: c.ts,
|
||||||
lock_on: c.lock_on.clone(),
|
lock_on: c.lock_on.clone(),
|
||||||
lock_off: c.lock_off.clone(),
|
lock_off: c.lock_off.clone(),
|
||||||
|
lock_params: c.lock_params.clone(),
|
||||||
access: c.access.iter().map(|a| ChanAccessView { account: a.account.clone(), level: a.level.clone() }).collect(),
|
access: c.access.iter().map(|a| ChanAccessView { account: a.account.clone(), level: a.level.clone() }).collect(),
|
||||||
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(),
|
||||||
|
|
|
||||||
|
|
@ -438,6 +438,27 @@
|
||||||
assert_eq!(db.channel("#chan").unwrap().lock_modes(), "+rnt-s", "survives reopen");
|
assert_eq!(db.channel("#chan").unwrap().lock_modes(), "+rnt-s", "survives reopen");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A param mode lock (+f flood) carries its value through render, enforcement,
|
||||||
|
// and a reopen — the Anope FLOOD/JOINFLOOD migration must not lose it.
|
||||||
|
#[test]
|
||||||
|
fn param_mode_lock_carries_its_value() {
|
||||||
|
let p = tmp("mlock_param");
|
||||||
|
let mut db = Db::open(&p, "local");
|
||||||
|
db.register_channel("#chan", "alice").unwrap();
|
||||||
|
db.set_mlock_params("#chan", "ntf", "", vec![('f', "mute:7:8s".to_string())]).unwrap();
|
||||||
|
|
||||||
|
let info = db.channel("#chan").unwrap();
|
||||||
|
assert_eq!(info.lock_modes(), "+rntf mute:7:8s");
|
||||||
|
// Removing the locked param mode re-asserts it with its value.
|
||||||
|
assert_eq!(info.enforce("-f"), Some("+f mute:7:8s".to_string()));
|
||||||
|
|
||||||
|
drop(db);
|
||||||
|
let db = Db::open(&p, "local");
|
||||||
|
let info = db.channel("#chan").unwrap();
|
||||||
|
assert_eq!(info.lock_params, vec![('f', "mute:7:8s".to_string())], "param survives reopen");
|
||||||
|
assert_eq!(info.lock_modes(), "+rntf mute:7:8s");
|
||||||
|
}
|
||||||
|
|
||||||
// Access list drives join modes, is case-insensitive, and persists.
|
// Access list drives join modes, is case-insensitive, and persists.
|
||||||
#[test]
|
#[test]
|
||||||
fn access_list_grants_join_modes() {
|
fn access_list_grants_join_modes() {
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ const SERVICE_NICKS: &[&str] = &[
|
||||||
"groupserv", "statserv", "infoserv", "global", "chanfix", "diceserv", "reportserv",
|
"groupserv", "statserv", "infoserv", "global", "chanfix", "diceserv", "reportserv",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Anope ModeLock name -> InspIRCd mode char, for the param-less locks we can carry.
|
// Anope ModeLock name -> InspIRCd mode char, for the param-less locks.
|
||||||
// Param modes (FLOOD/JOINFLOOD) are skipped: Echo's mlock is char-only.
|
// Param modes (FLOOD/JOINFLOOD) are handled separately by param_mode_char.
|
||||||
fn mode_char(name: &str) -> Option<char> {
|
fn mode_char(name: &str) -> Option<char> {
|
||||||
Some(match name {
|
Some(match name {
|
||||||
"NOEXTERNAL" => 'n',
|
"NOEXTERNAL" => 'n',
|
||||||
|
|
@ -56,6 +56,16 @@ fn mode_char(name: &str) -> Option<char> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Param-carrying mode locks: the ircd mode takes an argument, so the lock keeps
|
||||||
|
// the stored value and re-asserts `+<char> <value>`.
|
||||||
|
fn param_mode_char(name: &str) -> Option<char> {
|
||||||
|
Some(match name {
|
||||||
|
"FLOOD" => 'f',
|
||||||
|
"JOINFLOOD" => 'j',
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// A non-empty, non-"None" string field (Anope stores genuine text as strings).
|
// A non-empty, non-"None" string field (Anope stores genuine text as strings).
|
||||||
fn field<'a>(v: &'a Value, k: &str) -> Option<&'a str> {
|
fn field<'a>(v: &'a Value, k: &str) -> Option<&'a str> {
|
||||||
v.get(k).and_then(Value::as_str).filter(|s| !s.is_empty() && *s != "None")
|
v.get(k).and_then(Value::as_str).filter(|s| !s.is_empty() && *s != "None")
|
||||||
|
|
@ -249,20 +259,35 @@ pub fn import_anope(anope_path: &str, out_path: &str, node: &str) -> std::io::Re
|
||||||
sum.access += 1;
|
sum.access += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode locks: aggregate the param-less +modes per channel.
|
// Mode locks: aggregate the +modes per channel. Param modes (+f flood,
|
||||||
let mut locks: HashMap<String, String> = HashMap::new();
|
// +j joinflood) carry their stored value so the lock re-enforces verbatim.
|
||||||
|
let mut locks: HashMap<String, (String, Vec<(char, String)>)> = HashMap::new();
|
||||||
for ml in records(&data, "ModeLock") {
|
for ml in records(&data, "ModeLock") {
|
||||||
let (Some(chan), Some(name)) = (field(ml, "ci"), field(ml, "name")) else { continue };
|
let (Some(chan), Some(name)) = (field(ml, "ci"), field(ml, "name")) else { continue };
|
||||||
if !flag(ml, "set") {
|
if !flag(ml, "set") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match mode_char(name) {
|
if let Some(ch) = mode_char(name) {
|
||||||
Some(ch) => locks.entry(chan.to_string()).or_default().push(ch),
|
locks.entry(chan.to_string()).or_default().0.push(ch);
|
||||||
None => sum.skipped.push(format!("modelock {chan} {name}: unmapped/param mode")),
|
} else if let Some(ch) = param_mode_char(name) {
|
||||||
|
let Some(param) = field(ml, "param") else {
|
||||||
|
sum.skipped.push(format!("modelock {chan} {name}: param mode with no value"));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let entry = locks.entry(chan.to_string()).or_default();
|
||||||
|
entry.0.push(ch);
|
||||||
|
entry.1.push((ch, param.to_string()));
|
||||||
|
} else {
|
||||||
|
sum.skipped.push(format!("modelock {chan} {name}: unmapped mode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (chan, on) in &locks {
|
for (chan, (on, params)) in &locks {
|
||||||
db.migrate_append(Event::ChannelMlock { name: chan.clone(), on: on.clone(), off: String::new() })?;
|
db.migrate_append(Event::ChannelMlock {
|
||||||
|
name: chan.clone(),
|
||||||
|
on: on.clone(),
|
||||||
|
off: String::new(),
|
||||||
|
params: params.clone(),
|
||||||
|
})?;
|
||||||
sum.mlocked += 1;
|
sum.mlocked += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,7 +374,8 @@ mod tests {
|
||||||
let chan = db.channel("#chan").expect("channel migrated");
|
let chan = db.channel("#chan").expect("channel migrated");
|
||||||
assert_eq!(chan.founder, "alice");
|
assert_eq!(chan.founder, "alice");
|
||||||
assert_eq!(chan.desc, "a place");
|
assert_eq!(chan.desc, "a place");
|
||||||
assert_eq!(chan.lock_on, "nt", "param-less mlocks kept, FLOOD skipped");
|
assert_eq!(chan.lock_on, "ntf", "param-less and param mlocks both kept");
|
||||||
|
assert_eq!(chan.lock_params, vec![('f', "5:10".to_string())], "FLOOD param carried");
|
||||||
assert_eq!(chan.assigned_bot.as_deref(), Some("Botty"));
|
assert_eq!(chan.assigned_bot.as_deref(), Some("Botty"));
|
||||||
assert!(chan.settings.keeptopic && chan.settings.peace, "channel SET flags migrated");
|
assert!(chan.settings.keeptopic && chan.settings.peace, "channel SET flags migrated");
|
||||||
assert!(!chan.settings.signkick, "unset flags stay default (PERSIST has no equivalent, ignored)");
|
assert!(!chan.settings.signkick, "unset flags stay default (PERSIST has no equivalent, ignored)");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue