chanserv: entrymsg, enforce, getkey, seen, clone, aop/sop/vop
Tracks channel membership (join/part/kick), the current key, and per-nick last-seen in the network view. Entry messages greet joiners; enforce re-applies the lock, access modes and auto-kicks to everyone present; getkey reports the key; seen reports last activity; clone copies a channel's settings; aop/sop/vop are tiered shortcuts over the access list.
This commit is contained in:
parent
674f543b40
commit
e8b55bdb27
12 changed files with 479 additions and 26 deletions
|
|
@ -26,6 +26,18 @@ mod list;
|
|||
mod status;
|
||||
#[path = "set.rs"]
|
||||
mod set;
|
||||
#[path = "entrymsg.rs"]
|
||||
mod entrymsg;
|
||||
#[path = "getkey.rs"]
|
||||
mod getkey;
|
||||
#[path = "seen.rs"]
|
||||
mod seen;
|
||||
#[path = "enforce.rs"]
|
||||
mod enforce;
|
||||
#[path = "clone.rs"]
|
||||
mod clone;
|
||||
#[path = "xop.rs"]
|
||||
mod xop;
|
||||
|
||||
pub struct ChanServ {
|
||||
pub uid: String,
|
||||
|
|
@ -167,13 +179,36 @@ impl Service for ChanServ {
|
|||
Some("STATUS") => status::handle(me, from, args, ctx, net, db),
|
||||
Some("LIST") => list::handle(me, from, args, ctx, db),
|
||||
Some("SET") => set::handle(me, from, args, ctx, db),
|
||||
Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02, \x02INFO\x02, \x02LIST\x02, \x02SET\x02, \x02ACCESS\x02, \x02STATUS\x02, \x02OP\x02/\x02DEOP\x02, \x02VOICE\x02/\x02DEVOICE\x02, \x02KICK\x02, \x02BAN\x02/\x02UNBAN\x02, \x02AKICK\x02, \x02TOPIC\x02, \x02INVITE\x02, \x02MODE\x02, \x02MLOCK\x02, \x02DROP\x02."),
|
||||
Some("ENTRYMSG") => entrymsg::handle(me, from, args, ctx, db),
|
||||
Some("GETKEY") => getkey::handle(me, from, args, ctx, net, db),
|
||||
Some("SEEN") => seen::handle(me, from, args, ctx, net),
|
||||
Some("ENFORCE") => enforce::handle(me, from, args, ctx, net, db),
|
||||
Some("CLONE") => clone::handle(me, from, args, ctx, db),
|
||||
Some("AOP") => xop::handle(me, from, "AOP", "op", args, ctx, db),
|
||||
Some("SOP") => xop::handle(me, from, "SOP", "op", args, ctx, db),
|
||||
Some("VOP") => xop::handle(me, from, "VOP", "voice", args, ctx, db),
|
||||
Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02, \x02INFO\x02, \x02LIST\x02, \x02SET\x02, \x02ACCESS\x02, \x02AOP\x02/\x02SOP\x02/\x02VOP\x02, \x02STATUS\x02, \x02OP\x02/\x02DEOP\x02, \x02VOICE\x02/\x02DEVOICE\x02, \x02KICK\x02, \x02BAN\x02/\x02UNBAN\x02, \x02AKICK\x02, \x02ENFORCE\x02, \x02TOPIC\x02, \x02ENTRYMSG\x02, \x02INVITE\x02, \x02GETKEY\x02, \x02SEEN\x02, \x02CLONE\x02, \x02MODE\x02, \x02MLOCK\x02, \x02DROP\x02."),
|
||||
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// True if `from` is the channel's founder; otherwise notices why and returns false.
|
||||
fn require_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &Db) -> bool {
|
||||
match db.channel(chan) {
|
||||
None => {
|
||||
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
||||
false
|
||||
}
|
||||
Some(info) if from.account == Some(info.founder.as_str()) => true,
|
||||
_ => {
|
||||
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can do that."));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// True if `from` is the founder or an access-list op of `chan`; else notices why.
|
||||
fn require_op(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &Db) -> bool {
|
||||
match (from.account, db.channel(chan)) {
|
||||
|
|
|
|||
32
modules/chanserv/clone.rs
Normal file
32
modules/chanserv/clone.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
|
||||
// CLONE <source> <target>: copy a channel's settings (mode lock, access,
|
||||
// auto-kick, description, entry message) into another. Founder of both.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
||||
let (Some(&src), Some(&dest)) = (args.get(1), args.get(2)) else {
|
||||
ctx.notice(me, from.uid, "Syntax: CLONE <source> <target>");
|
||||
return;
|
||||
};
|
||||
let (Some(sinfo), Some(dinfo)) = (db.channel(src).cloned(), db.channel(dest).cloned()) else {
|
||||
ctx.notice(me, from.uid, "Both channels must be registered.");
|
||||
return;
|
||||
};
|
||||
if from.account != Some(sinfo.founder.as_str()) || from.account != Some(dinfo.founder.as_str()) {
|
||||
ctx.notice(me, from.uid, "You must be the founder of both channels.");
|
||||
return;
|
||||
}
|
||||
let _ = db.set_mlock(dest, &sinfo.lock_on, &sinfo.lock_off);
|
||||
for a in &sinfo.access {
|
||||
let _ = db.access_add(dest, &a.account, &a.level);
|
||||
}
|
||||
for k in &sinfo.akick {
|
||||
let _ = db.akick_add(dest, &k.mask, &k.reason);
|
||||
}
|
||||
let _ = db.set_desc(dest, &sinfo.desc);
|
||||
let _ = db.set_entrymsg(dest, &sinfo.entrymsg);
|
||||
if let Some(info) = db.channel(dest) {
|
||||
ctx.channel_mode(me, dest, &info.lock_modes());
|
||||
}
|
||||
ctx.notice(me, from.uid, format!("Copied \x02{src}\x02's settings to \x02{dest}\x02."));
|
||||
}
|
||||
35
modules/chanserv/enforce.rs
Normal file
35
modules/chanserv/enforce.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
use crate::engine::state::Network;
|
||||
|
||||
// ENFORCE <#channel>: re-apply the channel's settings to everyone present —
|
||||
// the mode lock, access status modes, and the auto-kick list.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) {
|
||||
let Some(&chan) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: ENFORCE <#channel>");
|
||||
return;
|
||||
};
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
let Some(info) = db.channel(chan).cloned() else {
|
||||
return;
|
||||
};
|
||||
ctx.channel_mode(me, chan, &info.lock_modes());
|
||||
let members: Vec<String> = net.channel_members(chan).map(str::to_string).collect();
|
||||
for uid in members {
|
||||
match net.account_of(&uid).and_then(|a| info.join_mode(a)) {
|
||||
Some(m) => ctx.channel_mode(me, chan, &format!("{m} {uid}")),
|
||||
None => {
|
||||
let nick = net.nick_of(&uid).unwrap_or("*");
|
||||
let host = net.host_of(&uid).unwrap_or("*");
|
||||
if let Some(k) = info.akick_match(&format!("{nick}!*@{host}")) {
|
||||
ctx.channel_mode(me, chan, &format!("+b {}", k.mask));
|
||||
let reason = if k.reason.is_empty() { "You are banned from this channel." } else { &k.reason };
|
||||
ctx.kick(me, chan, &uid, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.notice(me, from.uid, format!("Re-applied \x02{chan}\x02's settings to everyone present."));
|
||||
}
|
||||
36
modules/chanserv/entrymsg.rs
Normal file
36
modules/chanserv/entrymsg.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
|
||||
// ENTRYMSG <#channel> [CLEAR | <text>]: message noticed to users as they join.
|
||||
// With no argument, show the current message.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
||||
let Some(&chan) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: ENTRYMSG <#channel> [CLEAR | <text>]");
|
||||
return;
|
||||
};
|
||||
match args.get(2) {
|
||||
None => match db.channel(chan) {
|
||||
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
|
||||
Some(info) if info.entrymsg.is_empty() => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no entry message.")),
|
||||
Some(info) => ctx.notice(me, from.uid, format!("Entry message for \x02{chan}\x02: {}", info.entrymsg)),
|
||||
},
|
||||
Some(&kw) if kw.eq_ignore_ascii_case("CLEAR") => {
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
match db.set_entrymsg(chan, "") {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("Entry message for \x02{chan}\x02 cleared.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
match db.set_entrymsg(chan, &args[2..].join(" ")) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("Entry message for \x02{chan}\x02 updated.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
modules/chanserv/getkey.rs
Normal file
19
modules/chanserv/getkey.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
use crate::engine::state::Network;
|
||||
|
||||
// GETKEY <#channel>: report the channel key (+k), for ops who need to let
|
||||
// someone in.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) {
|
||||
let Some(&chan) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: GETKEY <#channel>");
|
||||
return;
|
||||
};
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
match net.channel_key(chan) {
|
||||
Some(key) => ctx.notice(me, from.uid, format!("Key for \x02{chan}\x02 is \x02{key}\x02.")),
|
||||
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no key set.")),
|
||||
}
|
||||
}
|
||||
18
modules/chanserv/seen.rs
Normal file
18
modules/chanserv/seen.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
use crate::engine::state::Network;
|
||||
|
||||
// SEEN <nick>: when a nick was last seen, and doing what.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network) {
|
||||
let Some(&nick) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: SEEN <nick>");
|
||||
return;
|
||||
};
|
||||
if net.uid_by_nick(nick).is_some() {
|
||||
ctx.notice(me, from.uid, format!("\x02{nick}\x02 is currently online."));
|
||||
return;
|
||||
}
|
||||
match net.last_seen(nick) {
|
||||
Some(s) => ctx.notice(me, from.uid, format!("\x02{}\x02 was last seen {} ({}).", s.nick, super::human_time(s.ts), s.what)),
|
||||
None => ctx.notice(me, from.uid, format!("I have no record of \x02{nick}\x02.")),
|
||||
}
|
||||
}
|
||||
51
modules/chanserv/xop.rs
Normal file
51
modules/chanserv/xop.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
|
||||
// AOP/SOP/VOP <#channel> ADD <account> | DEL <account> | LIST — tiered
|
||||
// shortcuts over the access list. `level` is the access level they map to
|
||||
// ("op" for AOP/SOP, "voice" for VOP); `word` is what the user typed.
|
||||
pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
||||
let Some(&chan) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST"));
|
||||
return;
|
||||
};
|
||||
match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||
None | Some("LIST") => match db.channel(chan) {
|
||||
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
|
||||
Some(info) => {
|
||||
ctx.notice(me, from.uid, format!("{word} list for \x02{}\x02:", info.name));
|
||||
for a in info.access.iter().filter(|a| a.level == level) {
|
||||
ctx.notice(me, from.uid, format!(" \x02{}\x02", a.account));
|
||||
}
|
||||
}
|
||||
},
|
||||
Some("ADD") => {
|
||||
let Some(&account) = args.get(3) else {
|
||||
ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account>"));
|
||||
return;
|
||||
};
|
||||
if !super::require_founder(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
match db.access_add(chan, account, level) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("Added \x02{account}\x02 to \x02{chan}\x02's {word} list.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
Some("DEL") => {
|
||||
let Some(&account) = args.get(3) else {
|
||||
ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> DEL <account>"));
|
||||
return;
|
||||
};
|
||||
if !super::require_founder(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
match db.access_del(chan, account) {
|
||||
Ok(true) => ctx.notice(me, from.uid, format!("Removed \x02{account}\x02 from \x02{chan}\x02's {word} list.")),
|
||||
Ok(false) => ctx.notice(me, from.uid, format!("\x02{account}\x02 has no access to \x02{chan}\x02.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
_ => ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST")),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue