chanserv: topic, invite, akick, list, status
Adds the remaining moderation and listing commands. Auto-kick masks are stored in the event log and enforced on join: a matching user is banned and kicked. Topic and invite are sourced from the ChanServ pseudoclient.
This commit is contained in:
parent
acd0e60946
commit
ba3803e01c
11 changed files with 369 additions and 7 deletions
52
modules/chanserv/akick.rs
Normal file
52
modules/chanserv/akick.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
|
||||
// AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST
|
||||
// Masks are nick!user@host globs; matching users are banned and kicked on join.
|
||||
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: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | 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) if info.akick.is_empty() => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has an empty auto-kick list.")),
|
||||
Some(info) => {
|
||||
ctx.notice(me, from.uid, format!("Auto-kick list for \x02{}\x02:", info.name));
|
||||
for k in &info.akick {
|
||||
ctx.notice(me, from.uid, format!(" \x02{}\x02 ({})", k.mask, k.reason));
|
||||
}
|
||||
}
|
||||
},
|
||||
Some("ADD") => {
|
||||
let Some(&mask) = args.get(3) else {
|
||||
ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason]");
|
||||
return;
|
||||
};
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
let reason = if args.len() > 4 { args[4..].join(" ") } else { "Auto-kicked".to_string() };
|
||||
match db.akick_add(chan, mask, &reason) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("Added \x02{mask}\x02 to \x02{chan}\x02's auto-kick list.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
Some("DEL") => {
|
||||
let Some(&mask) = args.get(3) else {
|
||||
ctx.notice(me, from.uid, "Syntax: AKICK <#channel> DEL <mask>");
|
||||
return;
|
||||
};
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
match db.akick_del(chan, mask) {
|
||||
Ok(true) => ctx.notice(me, from.uid, format!("Removed \x02{mask}\x02 from \x02{chan}\x02's auto-kick list.")),
|
||||
Ok(false) => ctx.notice(me, from.uid, format!("\x02{mask}\x02 isn't on \x02{chan}\x02's auto-kick list.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
_ => ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST"),
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,16 @@ mod kick;
|
|||
mod ban;
|
||||
#[path = "unban.rs"]
|
||||
mod unban;
|
||||
#[path = "topic.rs"]
|
||||
mod topic;
|
||||
#[path = "invite.rs"]
|
||||
mod invite;
|
||||
#[path = "akick.rs"]
|
||||
mod akick;
|
||||
#[path = "list.rs"]
|
||||
mod list;
|
||||
#[path = "status.rs"]
|
||||
mod status;
|
||||
|
||||
pub struct ChanServ {
|
||||
pub uid: String,
|
||||
|
|
@ -146,7 +156,12 @@ impl Service for ChanServ {
|
|||
Some("KICK") => kick::handle(me, from, args, ctx, net, db),
|
||||
Some("BAN") => ban::handle(me, from, args, ctx, net, db),
|
||||
Some("UNBAN") => unban::handle(me, from, args, ctx, net, db),
|
||||
Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02, \x02INFO\x02, \x02ACCESS\x02, \x02OP\x02/\x02DEOP\x02, \x02VOICE\x02/\x02DEVOICE\x02, \x02KICK\x02, \x02BAN\x02/\x02UNBAN\x02, \x02MODE\x02, \x02MLOCK\x02, \x02DROP\x02 — each takes a <#channel>."),
|
||||
Some("TOPIC") => topic::handle(me, from, args, ctx, db),
|
||||
Some("INVITE") => invite::handle(me, from, args, ctx, net, db),
|
||||
Some("AKICK") => akick::handle(me, from, args, ctx, db),
|
||||
Some("STATUS") => status::handle(me, from, args, ctx, net, db),
|
||||
Some("LIST") => list::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, \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(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
|
||||
None => {}
|
||||
}
|
||||
|
|
|
|||
26
modules/chanserv/invite.rs
Normal file
26
modules/chanserv/invite.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
use crate::engine::state::Network;
|
||||
|
||||
// INVITE <#channel> [nick]: invite a user (self if no nick) into the channel.
|
||||
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: INVITE <#channel> [nick]");
|
||||
return;
|
||||
};
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
let target = match args.get(2) {
|
||||
Some(&nick) => match net.uid_by_nick(nick) {
|
||||
Some(u) => u.to_string(),
|
||||
None => {
|
||||
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here."));
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => from.uid.to_string(),
|
||||
};
|
||||
ctx.invite(me, &target, chan);
|
||||
ctx.notice(me, from.uid, format!("Invited to \x02{chan}\x02."));
|
||||
}
|
||||
16
modules/chanserv/list.rs
Normal file
16
modules/chanserv/list.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
|
||||
// LIST: show all registered channels.
|
||||
pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &Db) {
|
||||
let mut names: Vec<&str> = db.channels().map(|c| c.name.as_str()).collect();
|
||||
if names.is_empty() {
|
||||
ctx.notice(me, from.uid, "No channels are registered.");
|
||||
return;
|
||||
}
|
||||
names.sort_unstable();
|
||||
ctx.notice(me, from.uid, format!("Registered channels ({}):", names.len()));
|
||||
for n in names {
|
||||
ctx.notice(me, from.uid, format!(" \x02{n}\x02"));
|
||||
}
|
||||
}
|
||||
29
modules/chanserv/status.rs
Normal file
29
modules/chanserv/status.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
use crate::engine::state::Network;
|
||||
|
||||
// STATUS <#channel> [nick]: show a user's access level (self if no nick).
|
||||
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: STATUS <#channel> [nick]");
|
||||
return;
|
||||
};
|
||||
let Some(info) = db.channel(chan) else {
|
||||
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
||||
return;
|
||||
};
|
||||
let (label, account) = match args.get(2) {
|
||||
Some(&nick) => (nick.to_string(), net.uid_by_nick(nick).and_then(|u| net.account_of(u)).map(str::to_string)),
|
||||
None => (from.nick.to_string(), from.account.map(str::to_string)),
|
||||
};
|
||||
let level = match account.as_deref() {
|
||||
None => "none (not logged in)",
|
||||
Some(a) if info.founder.eq_ignore_ascii_case(a) => "founder",
|
||||
Some(a) => match info.join_mode(a) {
|
||||
Some("+o") => "op",
|
||||
Some("+v") => "voice",
|
||||
_ => "none",
|
||||
},
|
||||
};
|
||||
ctx.notice(me, from.uid, format!("\x02{label}\x02 access on \x02{chan}\x02: \x02{level}\x02"));
|
||||
}
|
||||
16
modules/chanserv/topic.rs
Normal file
16
modules/chanserv/topic.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::service::{Sender, ServiceCtx};
|
||||
|
||||
// TOPIC <#channel> <text>: set the channel topic (empty clears it).
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &Db) {
|
||||
let Some(&chan) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: TOPIC <#channel> <text>");
|
||||
return;
|
||||
};
|
||||
if !super::require_op(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
let text = if args.len() > 2 { args[2..].join(" ") } else { String::new() };
|
||||
ctx.topic(me, chan, &text);
|
||||
ctx.notice(me, from.uid, format!("Topic for \x02{chan}\x02 updated."));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue