Add InfoServ, unifying network bulletins onto the news store
InfoServ becomes the dedicated home for network information bulletins, backed by the same event-sourced news store OperServ NEWS used — one place to post, not two. POST/LIST/DEL manage public bulletins (shown to every user on connect); OPOST/OLIST/ODEL the oper ones (shown to operators on login). Anyone may LIST the public bulletins; posting/removing is admin- only. The engine's existing connect/login display hooks render them unchanged. The OperServ NEWS command is retired accordingly (the news events and the display path stay). Default service.
This commit is contained in:
parent
1e8a5eab36
commit
8d495a948e
9 changed files with 152 additions and 101 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -331,6 +331,7 @@ dependencies = [
|
||||||
"fedserv-diceserv",
|
"fedserv-diceserv",
|
||||||
"fedserv-example",
|
"fedserv-example",
|
||||||
"fedserv-hostserv",
|
"fedserv-hostserv",
|
||||||
|
"fedserv-infoserv",
|
||||||
"fedserv-inspircd",
|
"fedserv-inspircd",
|
||||||
"fedserv-memoserv",
|
"fedserv-memoserv",
|
||||||
"fedserv-nickserv",
|
"fedserv-nickserv",
|
||||||
|
|
@ -395,6 +396,13 @@ dependencies = [
|
||||||
"fedserv-api",
|
"fedserv-api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fedserv-infoserv"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"fedserv-api",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fedserv-inspircd"
|
name = "fedserv-inspircd"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["api", "inspircd", "chanserv", "nickserv", "example", "botserv", "memoserv", "statserv", "hostserv", "operserv", "diceserv"]
|
members = ["api", "inspircd", "chanserv", "nickserv", "example", "botserv", "memoserv", "statserv", "hostserv", "operserv", "diceserv", "infoserv"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "fedserv"
|
name = "fedserv"
|
||||||
|
|
@ -19,6 +19,7 @@ fedserv-statserv = { path = "statserv" }
|
||||||
fedserv-hostserv = { path = "hostserv" }
|
fedserv-hostserv = { path = "hostserv" }
|
||||||
fedserv-operserv = { path = "operserv" }
|
fedserv-operserv = { path = "operserv" }
|
||||||
fedserv-diceserv = { path = "diceserv" }
|
fedserv-diceserv = { path = "diceserv" }
|
||||||
|
fedserv-infoserv = { path = "infoserv" }
|
||||||
tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "macros", "time", "sync", "process"] }
|
tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "macros", "time", "sync", "process"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
||||||
8
infoserv/Cargo.toml
Normal file
8
infoserv/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "fedserv-infoserv"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
description = "InfoServ: network information bulletins shown on connect (public) and on oper login."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fedserv-api = { path = "../api" }
|
||||||
103
infoserv/src/lib.rs
Normal file
103
infoserv/src/lib.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
//! InfoServ manages the network information bulletins. Public bulletins are
|
||||||
|
//! shown to every user as they connect; oper bulletins are shown to operators
|
||||||
|
//! when they log in (both are displayed by the engine — the same hooks OperServ
|
||||||
|
//! once drove through its NEWS command). This is the dedicated front-end over
|
||||||
|
//! that one shared news store, so there aren't two ways to post the same thing.
|
||||||
|
//!
|
||||||
|
//! POST/LIST/DEL manage public bulletins; OPOST/OLIST/ODEL the oper ones.
|
||||||
|
//! Posting and deleting are admin-only; anyone may LIST the public bulletins,
|
||||||
|
//! and any operator may OLIST.
|
||||||
|
|
||||||
|
use fedserv_api::{NetView, Priv, Sender, Service, ServiceCtx, Store};
|
||||||
|
|
||||||
|
// The two bulletin kinds in the shared news store.
|
||||||
|
const PUBLIC: &str = "logon";
|
||||||
|
const OPER: &str = "oper";
|
||||||
|
|
||||||
|
pub struct InfoServ {
|
||||||
|
pub uid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service for InfoServ {
|
||||||
|
fn nick(&self) -> &str {
|
||||||
|
"InfoServ"
|
||||||
|
}
|
||||||
|
fn uid(&self) -> &str {
|
||||||
|
&self.uid
|
||||||
|
}
|
||||||
|
fn gecos(&self) -> &str {
|
||||||
|
"Information Service"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, _net: &dyn NetView, db: &mut dyn Store) {
|
||||||
|
let me = self.uid.as_str();
|
||||||
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
Some("POST") | Some("ADD") => post(me, from, PUBLIC, &args[1..], ctx, db),
|
||||||
|
Some("OPOST") | Some("OADD") => post(me, from, OPER, &args[1..], ctx, db),
|
||||||
|
Some("DEL") | Some("REMOVE") => del(me, from, PUBLIC, args.get(1).copied(), ctx, db),
|
||||||
|
Some("ODEL") => del(me, from, OPER, args.get(1).copied(), ctx, db),
|
||||||
|
// Public bulletins are, well, public — anyone may list them.
|
||||||
|
Some("LIST") | None => list(me, from, PUBLIC, false, ctx, db),
|
||||||
|
// Oper bulletins are for operators only.
|
||||||
|
Some("OLIST") => list(me, from, OPER, true, ctx, db),
|
||||||
|
Some("HELP") => help(me, from, ctx),
|
||||||
|
Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02LIST\x02 or \x02HELP\x02.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) {
|
||||||
|
ctx.notice(me, from.uid, "InfoServ holds the network's information bulletins. \x02LIST\x02 shows the public ones. Operators: \x02POST\x02 <message> / \x02DEL\x02 <number> (public, shown on connect), \x02OPOST\x02 / \x02OLIST\x02 / \x02ODEL\x02 (oper-only, shown on login).");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post(me: &str, from: &Sender, kind: &str, rest: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
|
if !from.privs.has(Priv::Admin) {
|
||||||
|
ctx.notice(me, from.uid, "Access denied — posting bulletins is for services operators.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let message = rest.join(" ");
|
||||||
|
if message.trim().is_empty() {
|
||||||
|
ctx.notice(me, from.uid, format!("Syntax: {} <message>", if kind == OPER { "OPOST" } else { "POST" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let setter = from.account.unwrap_or(from.nick);
|
||||||
|
db.news_add(kind, &message, setter);
|
||||||
|
let which = if kind == OPER { "oper" } else { "public" };
|
||||||
|
ctx.notice(me, from.uid, format!("Added a \x02{which}\x02 bulletin."));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn del(me: &str, from: &Sender, kind: &str, num: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
|
if !from.privs.has(Priv::Admin) {
|
||||||
|
ctx.notice(me, from.uid, "Access denied — that command is for services operators.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let which = if kind == OPER { "oper" } else { "public" };
|
||||||
|
let Some(n) = num.and_then(|n| n.parse::<usize>().ok()) else {
|
||||||
|
ctx.notice(me, from.uid, format!("Syntax: {} <number>", if kind == OPER { "ODEL" } else { "DEL" }));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match db.news(kind).get(n.wrapping_sub(1)) {
|
||||||
|
Some(item) if n >= 1 => {
|
||||||
|
db.news_del(item.id);
|
||||||
|
ctx.notice(me, from.uid, format!("Removed \x02{which}\x02 bulletin \x02{n}\x02."));
|
||||||
|
}
|
||||||
|
_ => ctx.notice(me, from.uid, format!("There's no \x02{which}\x02 bulletin \x02{n}\x02.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(me: &str, from: &Sender, kind: &str, oper_only: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
|
if oper_only && !from.privs.any() {
|
||||||
|
ctx.notice(me, from.uid, "Access denied — oper bulletins are for services operators.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let items = db.news(kind);
|
||||||
|
let which = if kind == OPER { "oper" } else { "public" };
|
||||||
|
if items.is_empty() {
|
||||||
|
ctx.notice(me, from.uid, format!("There are no {which} bulletins."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i, item) in items.iter().enumerate() {
|
||||||
|
ctx.notice(me, from.uid, format!("{}. {} — by {}", i + 1, item.text, item.setter));
|
||||||
|
}
|
||||||
|
ctx.notice(me, from.uid, format!("End of {which} bulletins ({} shown).", items.len()));
|
||||||
|
}
|
||||||
|
|
@ -23,8 +23,6 @@ mod stats;
|
||||||
mod svs;
|
mod svs;
|
||||||
#[path = "info.rs"]
|
#[path = "info.rs"]
|
||||||
mod info;
|
mod info;
|
||||||
#[path = "news.rs"]
|
|
||||||
mod news;
|
|
||||||
#[path = "oper.rs"]
|
#[path = "oper.rs"]
|
||||||
mod oper;
|
mod oper;
|
||||||
#[path = "session.rs"]
|
#[path = "session.rs"]
|
||||||
|
|
@ -75,7 +73,6 @@ impl Service for OperServ {
|
||||||
Some(cmd) if cmd.eq_ignore_ascii_case("SVSNICK") => svs::nick(me, from, args, ctx, net),
|
Some(cmd) if cmd.eq_ignore_ascii_case("SVSNICK") => svs::nick(me, from, args, ctx, net),
|
||||||
Some(cmd) if cmd.eq_ignore_ascii_case("SVSJOIN") => svs::join(me, from, args, ctx, net),
|
Some(cmd) if cmd.eq_ignore_ascii_case("SVSJOIN") => svs::join(me, from, args, ctx, net),
|
||||||
Some(cmd) if cmd.eq_ignore_ascii_case("INFO") => info::handle(me, from, args, ctx, db),
|
Some(cmd) if cmd.eq_ignore_ascii_case("INFO") => info::handle(me, from, args, ctx, db),
|
||||||
Some(cmd) if cmd.eq_ignore_ascii_case("NEWS") => news::handle(me, from, args, ctx, db),
|
|
||||||
Some(cmd) if cmd.eq_ignore_ascii_case("OPER") => oper::handle(me, from, args, ctx, db),
|
Some(cmd) if cmd.eq_ignore_ascii_case("OPER") => oper::handle(me, from, args, ctx, db),
|
||||||
Some(cmd) if cmd.eq_ignore_ascii_case("SESSION") => session::handle_session(me, from, args, ctx, net),
|
Some(cmd) if cmd.eq_ignore_ascii_case("SESSION") => session::handle_session(me, from, args, ctx, net),
|
||||||
Some(cmd) if cmd.eq_ignore_ascii_case("EXCEPTION") => session::handle_exception(me, from, args, ctx, db),
|
Some(cmd) if cmd.eq_ignore_ascii_case("EXCEPTION") => session::handle_exception(me, from, args, ctx, db),
|
||||||
|
|
@ -91,5 +88,5 @@ impl Service for OperServ {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) {
|
fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) {
|
||||||
ctx.notice(me, from.uid, "OperServ holds network operator tools (Priv::Admin): \x02AKILL\x02 ADD|DEL|LIST (user@host bans), \x02SQLINE\x02 ADD|DEL|LIST (nick bans), \x02SNLINE\x02 ADD|DEL|LIST (realname bans), \x02SHUN\x02 ADD|DEL|LIST (silence a host), \x02CBAN\x02 ADD|DEL|LIST (ban a channel name), \x02CHANKILL\x02 <#chan> (clear a channel), \x02GLOBAL\x02 <message> (announce to everyone), \x02KILL\x02 <nick> [reason] (disconnect a user), \x02KICK\x02 <#chan> <nick> [reason], \x02MODE\x02 <#chan> <modes> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 <nick> <newnick>, \x02SVSJOIN\x02 <nick> <#chan> [key], \x02INFO\x02 ADD|DEL <target> (staff notes), \x02NEWS\x02 ADD|DEL|LIST <LOGON|OPER> (announcements), \x02OPER\x02 ADD|DEL|LIST (runtime operators), \x02SESSION\x02 LIST|VIEW + \x02EXCEPTION\x02 ADD|DEL|LIST (per-IP session limits), \x02JUPE\x02 <server> | DEL | LIST, \x02LOGSEARCH\x02 [pattern] (search the action log), \x02DEFCON\x02 [1-5] (network defence level).");
|
ctx.notice(me, from.uid, "OperServ holds network operator tools (Priv::Admin): \x02AKILL\x02 ADD|DEL|LIST (user@host bans), \x02SQLINE\x02 ADD|DEL|LIST (nick bans), \x02SNLINE\x02 ADD|DEL|LIST (realname bans), \x02SHUN\x02 ADD|DEL|LIST (silence a host), \x02CBAN\x02 ADD|DEL|LIST (ban a channel name), \x02CHANKILL\x02 <#chan> (clear a channel), \x02GLOBAL\x02 <message> (announce to everyone), \x02KILL\x02 <nick> [reason] (disconnect a user), \x02KICK\x02 <#chan> <nick> [reason], \x02MODE\x02 <#chan> <modes> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 <nick> <newnick>, \x02SVSJOIN\x02 <nick> <#chan> [key], \x02INFO\x02 ADD|DEL <target> (staff notes — bulletins are on \x02InfoServ\x02), \x02OPER\x02 ADD|DEL|LIST (runtime operators), \x02SESSION\x02 LIST|VIEW + \x02EXCEPTION\x02 ADD|DEL|LIST (per-IP session limits), \x02JUPE\x02 <server> | DEL | LIST, \x02LOGSEARCH\x02 [pattern] (search the action log), \x02DEFCON\x02 [1-5] (network defence level).");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
use fedserv_api::{Priv, Sender, ServiceCtx, Store};
|
|
||||||
|
|
||||||
// NEWS ADD <LOGON|OPER> <text> | NEWS DEL <LOGON|OPER> <number> | NEWS LIST
|
|
||||||
// [LOGON|OPER]: manage the announcements shown to users on connect (logon) and
|
|
||||||
// to operators on login (oper). Admin-only.
|
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
|
||||||
if !from.privs.has(Priv::Admin) {
|
|
||||||
ctx.notice(me, from.uid, "Access denied — NEWS needs the \x02admin\x02 privilege.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
|
||||||
Some("ADD") => add(me, from, args.get(2).copied(), args.get(3..).unwrap_or(&[]), ctx, db),
|
|
||||||
Some("DEL") | Some("REMOVE") => del(me, from, args.get(2).copied(), args.get(3).copied(), ctx, db),
|
|
||||||
Some("LIST") | Some("VIEW") => list(me, from, args.get(2).copied(), ctx, db),
|
|
||||||
_ => ctx.notice(me, from.uid, "Syntax: NEWS ADD <LOGON|OPER> <text> | NEWS DEL <LOGON|OPER> <number> | NEWS LIST [LOGON|OPER]"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(me: &str, from: &Sender, kind: Option<&str>, text: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
|
||||||
let (Some(kind), false) = (kind.and_then(parse_kind), text.is_empty()) else {
|
|
||||||
ctx.notice(me, from.uid, "Syntax: NEWS ADD <LOGON|OPER> <text>");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let setter = from.account.unwrap_or(from.nick);
|
|
||||||
db.news_add(kind, &text.join(" "), setter);
|
|
||||||
ctx.notice(me, from.uid, format!("Added a \x02{kind}\x02 news item."));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn del(me: &str, from: &Sender, kind: Option<&str>, num: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
|
||||||
let (Some(kind), Some(n)) = (kind.and_then(parse_kind), num.and_then(|n| n.parse::<usize>().ok())) else {
|
|
||||||
ctx.notice(me, from.uid, "Syntax: NEWS DEL <LOGON|OPER> <number>");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
match db.news(kind).get(n.wrapping_sub(1)) {
|
|
||||||
Some(item) if n >= 1 => {
|
|
||||||
db.news_del(item.id);
|
|
||||||
ctx.notice(me, from.uid, format!("Removed \x02{kind}\x02 news item \x02{n}\x02."));
|
|
||||||
}
|
|
||||||
_ => ctx.notice(me, from.uid, format!("There's no \x02{kind}\x02 news item \x02{n}\x02.")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(me: &str, from: &Sender, kind: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
|
||||||
let kinds: &[&str] = match kind.map(|k| k.to_ascii_uppercase()) {
|
|
||||||
Some(k) if k == "LOGON" => &["logon"],
|
|
||||||
Some(k) if k == "OPER" => &["oper"],
|
|
||||||
Some(_) => {
|
|
||||||
ctx.notice(me, from.uid, "Kind must be \x02LOGON\x02 or \x02OPER\x02.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
None => &["logon", "oper"],
|
|
||||||
};
|
|
||||||
let mut any = false;
|
|
||||||
for k in kinds {
|
|
||||||
let items = db.news(k);
|
|
||||||
for (i, item) in items.iter().enumerate() {
|
|
||||||
any = true;
|
|
||||||
ctx.notice(me, from.uid, format!("{}. [\x02{k}\x02] {} — by {}", i + 1, item.text, item.setter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !any {
|
|
||||||
ctx.notice(me, from.uid, "No news items.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_kind(s: &str) -> Option<&'static str> {
|
|
||||||
match s.to_ascii_uppercase().as_str() {
|
|
||||||
"LOGON" => Some("logon"),
|
|
||||||
"OPER" => Some("oper"),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -133,7 +133,7 @@ impl Default for Modules {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_services() -> Vec<String> {
|
fn default_services() -> Vec<String> {
|
||||||
vec!["nickserv".to_string(), "chanserv".to_string(), "botserv".to_string(), "memoserv".to_string(), "statserv".to_string(), "hostserv".to_string(), "operserv".to_string()]
|
vec!["nickserv".to_string(), "chanserv".to_string(), "botserv".to_string(), "memoserv".to_string(), "statserv".to_string(), "hostserv".to_string(), "operserv".to_string(), "infoserv".to_string()]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -4565,12 +4565,12 @@ mod tests {
|
||||||
assert!(denied(&os(&mut e, "000AAAAAP", "STATS")), "expired grant confers nothing");
|
assert!(denied(&os(&mut e, "000AAAAAP", "STATS")), "expired grant confers nothing");
|
||||||
}
|
}
|
||||||
|
|
||||||
// OperServ NEWS: logon news greets everyone on connect, oper news greets an
|
// InfoServ bulletins over the shared news store: public bulletins greet
|
||||||
// operator on login; ADD/DEL/LIST are admin-only.
|
// everyone on connect, oper bulletins greet an operator on login.
|
||||||
#[test]
|
#[test]
|
||||||
fn operserv_news_logon_and_oper() {
|
fn infoserv_bulletins_public_and_oper() {
|
||||||
use fedserv_operserv::OperServ;
|
use fedserv_infoserv::InfoServ;
|
||||||
let path = std::env::temp_dir().join("fedserv-osnews.jsonl");
|
let path = std::env::temp_dir().join("fedserv-infoserv.jsonl");
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
let mut db = Db::open(&path, "42S");
|
let mut db = Db::open(&path, "42S");
|
||||||
db.scram_iterations = 4096;
|
db.scram_iterations = 4096;
|
||||||
|
|
@ -4580,7 +4580,7 @@ mod tests {
|
||||||
let mut e = Engine::new(
|
let mut e = Engine::new(
|
||||||
vec![
|
vec![
|
||||||
Box::new(NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }),
|
Box::new(NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }),
|
||||||
Box::new(OperServ { uid: "42SAAAAAH".into() }),
|
Box::new(InfoServ { uid: "42SAAAAAJ".into() }),
|
||||||
],
|
],
|
||||||
db,
|
db,
|
||||||
);
|
);
|
||||||
|
|
@ -4591,42 +4591,42 @@ mod tests {
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
e.set_irc_out(tx);
|
e.set_irc_out(tx);
|
||||||
let os = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAH".into(), text: t.into() });
|
let is = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAJ".into(), text: t.into() });
|
||||||
let has = |out: &[NetAction], needle: &str| out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains(needle)));
|
let has = |out: &[NetAction], needle: &str| out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains(needle)));
|
||||||
|
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAS".into(), nick: "staff".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAS".into(), nick: "staff".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAS".into(), to: "42SAAAAAA".into(), text: "IDENTIFY password1".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAS".into(), to: "42SAAAAAA".into(), text: "IDENTIFY password1".into() });
|
||||||
|
|
||||||
os(&mut e, "000AAAAAS", "NEWS ADD LOGON Welcome to the network");
|
is(&mut e, "000AAAAAS", "POST Welcome to the network");
|
||||||
os(&mut e, "000AAAAAS", "NEWS ADD OPER Staff meeting at 5");
|
is(&mut e, "000AAAAAS", "OPOST Staff meeting at 5");
|
||||||
|
|
||||||
// A new user is greeted with the logon news on connect, not the oper news.
|
// A new user is greeted with the public bulletin on connect, not the oper one.
|
||||||
let out = e.handle(NetEvent::UserConnect { uid: "000AAAAAP".into(), nick: "plain".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
let out = e.handle(NetEvent::UserConnect { uid: "000AAAAAP".into(), nick: "plain".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::Notice { to, text, .. } if to == "000AAAAAP" && text.contains("Welcome to the network"))), "logon news on connect: {out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::Notice { to, text, .. } if to == "000AAAAAP" && text.contains("Welcome to the network"))), "public bulletin on connect: {out:?}");
|
||||||
assert!(!has(&out, "Staff meeting"), "a plain user doesn't get oper news");
|
assert!(!has(&out, "Staff meeting"), "a plain user doesn't get oper bulletins");
|
||||||
|
|
||||||
// An operator logging in is shown the oper news (over the outbound path).
|
// An operator logging in is shown the oper bulletin (over the outbound path).
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "boss".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "boss".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
||||||
while rx.try_recv().is_ok() {} // drain the connect's logon news
|
while rx.try_recv().is_ok() {} // drain the connect's public bulletin
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY password1".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY password1".into() });
|
||||||
let mut oper_news = false;
|
let mut oper_bulletin = false;
|
||||||
while let Ok(a) = rx.try_recv() {
|
while let Ok(a) = rx.try_recv() {
|
||||||
if let NetAction::Notice { to, text, .. } = a {
|
if let NetAction::Notice { to, text, .. } = a {
|
||||||
if to == "000AAAAAB" && text.contains("Staff meeting at 5") {
|
if to == "000AAAAAB" && text.contains("Staff meeting at 5") {
|
||||||
oper_news = true;
|
oper_bulletin = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(oper_news, "oper news shown to an operator on login");
|
assert!(oper_bulletin, "oper bulletin shown to an operator on login");
|
||||||
|
|
||||||
// LIST shows both; DEL removes the logon item so a later connect is quiet.
|
// LIST shows the public one (to anyone); DEL removes it so a later connect is quiet.
|
||||||
assert!(has(&os(&mut e, "000AAAAAS", "NEWS LIST"), "Welcome to the network"), "list shows logon");
|
assert!(has(&is(&mut e, "000AAAAAP", "LIST"), "Welcome to the network"), "anyone can list public bulletins");
|
||||||
os(&mut e, "000AAAAAS", "NEWS DEL LOGON 1");
|
is(&mut e, "000AAAAAS", "DEL 1");
|
||||||
let out = e.handle(NetEvent::UserConnect { uid: "000AAAAAQ".into(), nick: "late".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
let out = e.handle(NetEvent::UserConnect { uid: "000AAAAAQ".into(), nick: "late".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
||||||
assert!(!has(&out, "Welcome to the network"), "logon news gone after DEL");
|
assert!(!has(&out, "Welcome to the network"), "bulletin gone after DEL");
|
||||||
|
|
||||||
// A non-admin (the unidentified plain user) can't manage news.
|
// A non-admin can't post.
|
||||||
assert!(has(&os(&mut e, "000AAAAAP", "NEWS ADD LOGON sneaky"), "Access denied"), "non-admin refused");
|
assert!(has(&is(&mut e, "000AAAAAP", "POST sneaky"), "Access denied"), "non-admin refused");
|
||||||
}
|
}
|
||||||
|
|
||||||
// OperServ INFO staff notes: an admin annotates an account/channel; the note
|
// OperServ INFO staff notes: an admin annotates an account/channel; the note
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ use fedserv_statserv::StatServ;
|
||||||
use fedserv_hostserv::HostServ;
|
use fedserv_hostserv::HostServ;
|
||||||
use fedserv_operserv::OperServ;
|
use fedserv_operserv::OperServ;
|
||||||
use fedserv_diceserv::DiceServ;
|
use fedserv_diceserv::DiceServ;
|
||||||
|
use fedserv_infoserv::InfoServ;
|
||||||
use fedserv_example::ExampleServ;
|
use fedserv_example::ExampleServ;
|
||||||
use fedserv_inspircd::InspIrcd;
|
use fedserv_inspircd::InspIrcd;
|
||||||
use fedserv_nickserv::NickServ;
|
use fedserv_nickserv::NickServ;
|
||||||
|
|
@ -95,6 +96,11 @@ async fn main() -> Result<()> {
|
||||||
uid: format!("{}AAAAAI", cfg.server.sid),
|
uid: format!("{}AAAAAI", cfg.server.sid),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
if enabled("infoserv") {
|
||||||
|
services.push(Box::new(InfoServ {
|
||||||
|
uid: format!("{}AAAAAJ", cfg.server.sid),
|
||||||
|
}));
|
||||||
|
}
|
||||||
if enabled("example") {
|
if enabled("example") {
|
||||||
services.push(Box::new(ExampleServ {
|
services.push(Box::new(ExampleServ {
|
||||||
uid: format!("{}AAAAAC", cfg.server.sid),
|
uid: format!("{}AAAAAC", cfg.server.sid),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue