ACCESS <#channel> LIST|ADD|DEL manages op/voice by account (founder-only changes). Entries are ChannelAccessAdd/Del events, so they replicate and compact like the rest, and join grants +o/+v from the list.
207 lines
8.7 KiB
Rust
207 lines
8.7 KiB
Rust
use crate::engine::db::{ChanError, ChannelInfo, Db};
|
|
use crate::engine::service::{Sender, Service, ServiceCtx};
|
|
|
|
#[path = "mode.rs"]
|
|
mod mode;
|
|
#[path = "access.rs"]
|
|
mod access;
|
|
|
|
pub struct ChanServ {
|
|
pub uid: String,
|
|
}
|
|
|
|
impl Service for ChanServ {
|
|
fn nick(&self) -> &str {
|
|
"ChanServ"
|
|
}
|
|
fn uid(&self) -> &str {
|
|
&self.uid
|
|
}
|
|
fn gecos(&self) -> &str {
|
|
"Channel Services"
|
|
}
|
|
fn manages_channels(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
|
let me = self.uid.as_str();
|
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
|
Some("REGISTER") => {
|
|
let Some(&chan) = args.get(1) else {
|
|
ctx.notice(me, from.uid, "Syntax: REGISTER <#channel>");
|
|
return;
|
|
};
|
|
if !chan.starts_with('#') {
|
|
ctx.notice(me, from.uid, "Channel names start with \x02#\x02.");
|
|
return;
|
|
}
|
|
// The founder is the account the sender is identified to.
|
|
let Some(account) = from.account else {
|
|
ctx.notice(me, from.uid, "You need to be logged in to register a channel. Identify to NickServ first.");
|
|
return;
|
|
};
|
|
match db.register_channel(chan, account) {
|
|
Ok(()) => {
|
|
ctx.channel_mode(me, chan, "+r"); // mark the channel registered
|
|
ctx.notice(me, from.uid, format!("\x02{chan}\x02 is now registered and you are its founder. Enjoy!"));
|
|
}
|
|
Err(ChanError::Exists) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 is already registered. Try \x02INFO {chan}\x02 to see who owns it.")),
|
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
|
}
|
|
}
|
|
Some("INFO") => {
|
|
let Some(&chan) = args.get(1) else {
|
|
ctx.notice(me, from.uid, "Syntax: INFO <#channel>");
|
|
return;
|
|
};
|
|
match db.channel(chan) {
|
|
Some(info) => {
|
|
ctx.notice(me, from.uid, format!("Information for \x02{}\x02:", info.name));
|
|
ctx.notice(me, from.uid, format!(" Founder : \x02{}\x02", info.founder));
|
|
ctx.notice(me, from.uid, format!(" Registered : {}", human_time(info.ts)));
|
|
}
|
|
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
|
|
}
|
|
}
|
|
Some("DROP") => {
|
|
let Some(&chan) = args.get(1) else {
|
|
ctx.notice(me, from.uid, "Syntax: DROP <#channel>");
|
|
return;
|
|
};
|
|
// Read the founder, then drop, without holding the borrow across the mutation.
|
|
let founder = match db.channel(chan) {
|
|
Some(info) => info.founder.clone(),
|
|
None => {
|
|
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
|
return;
|
|
}
|
|
};
|
|
if from.account != Some(founder.as_str()) {
|
|
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can drop it."));
|
|
return;
|
|
}
|
|
match db.drop_channel(chan) {
|
|
Ok(()) => {
|
|
ctx.channel_mode(me, chan, "-r"); // no longer registered
|
|
ctx.notice(me, from.uid, format!("\x02{chan}\x02 has been dropped and is no longer registered."));
|
|
}
|
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
|
}
|
|
}
|
|
Some("MLOCK") => {
|
|
let Some(&chan) = args.get(1) else {
|
|
ctx.notice(me, from.uid, "Syntax: MLOCK <#channel> [modes], e.g. MLOCK #chan +nt-s");
|
|
return;
|
|
};
|
|
// No modes given: show the current lock.
|
|
if args.len() <= 2 {
|
|
match db.channel(chan) {
|
|
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
|
|
Some(info) if info.lock_on.is_empty() && info.lock_off.is_empty() => {
|
|
ctx.notice(me, from.uid, format!("\x02{}\x02 has no mode lock set.", info.name));
|
|
}
|
|
Some(info) => ctx.notice(me, from.uid, format!("Mode lock for \x02{}\x02: \x02{}\x02", info.name, show_mlock(info))),
|
|
}
|
|
return;
|
|
}
|
|
// Setting the lock: founder only.
|
|
let founder = match db.channel(chan) {
|
|
Some(info) => info.founder.clone(),
|
|
None => {
|
|
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
|
return;
|
|
}
|
|
};
|
|
if from.account != Some(founder.as_str()) {
|
|
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can set its mode lock."));
|
|
return;
|
|
}
|
|
let (on, off) = parse_mlock(&args[2..].concat());
|
|
match db.set_mlock(chan, &on, &off) {
|
|
Ok(()) => {
|
|
if let Some(info) = db.channel(chan) {
|
|
ctx.channel_mode(me, chan, &info.lock_modes()); // apply it now
|
|
}
|
|
ctx.notice(me, from.uid, format!("Mode lock for \x02{chan}\x02 updated."));
|
|
}
|
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
|
}
|
|
}
|
|
Some("MODE") => mode::handle(me, from, args, ctx, db),
|
|
Some("ACCESS") => access::handle(me, from, args, ctx, db),
|
|
Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02 <#channel>, \x02INFO\x02 <#channel>, \x02ACCESS\x02 <#channel>, \x02MODE\x02 <#channel> <modes>, \x02MLOCK\x02 <#channel> [modes], \x02DROP\x02 <#channel>."),
|
|
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
|
|
None => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
fn parse_mlock(spec: &str) -> (String, String) {
|
|
let (mut on, mut off) = (String::new(), String::new());
|
|
let mut adding = true;
|
|
for ch in spec.chars() {
|
|
match ch {
|
|
'+' => adding = true,
|
|
'-' => adding = false,
|
|
'r' => {}
|
|
m if m.is_ascii_alphabetic() => {
|
|
on.retain(|c| c != m);
|
|
off.retain(|c| c != m);
|
|
if adding {
|
|
on.push(m);
|
|
} else {
|
|
off.push(m);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
(on, off)
|
|
}
|
|
|
|
// Render a channel's lock as "+on-off" for display.
|
|
fn show_mlock(info: &ChannelInfo) -> String {
|
|
let mut s = String::new();
|
|
if !info.lock_on.is_empty() {
|
|
s.push('+');
|
|
s.push_str(&info.lock_on);
|
|
}
|
|
if !info.lock_off.is_empty() {
|
|
s.push('-');
|
|
s.push_str(&info.lock_off);
|
|
}
|
|
s
|
|
}
|
|
|
|
// Format a Unix timestamp (seconds) as "YYYY-MM-DD HH:MM:SS UTC", using Howard
|
|
// Hinnant's civil-from-days algorithm so no date crate is needed.
|
|
fn human_time(ts: u64) -> String {
|
|
let days = (ts / 86400) as i64;
|
|
let rem = ts % 86400;
|
|
let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60);
|
|
let z = days + 719468;
|
|
let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
|
|
let doe = z - era * 146097;
|
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
let y = yoe + era * 400;
|
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
let mp = (5 * doy + 2) / 153;
|
|
let day = doy - (153 * mp + 2) / 5 + 1;
|
|
let month = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
let year = y + i64::from(month <= 2);
|
|
format!("{year:04}-{month:02}-{day:02} {hh:02}:{mm:02}:{ss:02} UTC")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::human_time;
|
|
|
|
#[test]
|
|
fn formats_unix_time_as_utc() {
|
|
assert_eq!(human_time(0), "1970-01-01 00:00:00 UTC");
|
|
assert_eq!(human_time(1783844590), "2026-07-12 08:23:10 UTC");
|
|
}
|
|
}
|