infoserv: type the news kind as an enum at the store boundary
All checks were successful
CI / check (push) Successful in 3m53s
All checks were successful
CI / check (push) Successful in 3m53s
This commit is contained in:
parent
ee95225eaf
commit
7a5f1502a4
11 changed files with 49 additions and 31 deletions
|
|
@ -844,6 +844,22 @@ impl XlineKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The two InfoServ news feeds. Stored as the string token; the API is typed.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum NewsKind {
|
||||||
|
Logon, // shown to everyone on connect
|
||||||
|
Oper, // staff news, shown to opers on login
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewsKind {
|
||||||
|
pub fn wire(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Logon => "logon",
|
||||||
|
Self::Oper => "oper",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A registration-ban target for OperServ FORBID. The persistence/gossip layer
|
// A registration-ban target for OperServ FORBID. The persistence/gossip layer
|
||||||
// stores the wire token (`wire()`); the Store API is typed so a call site can't
|
// stores the wire token (`wire()`); the Store API is typed so a call site can't
|
||||||
// pass a mistyped kind like "NCK" (it wouldn't compile).
|
// pass a mistyped kind like "NCK" (it wouldn't compile).
|
||||||
|
|
@ -1251,9 +1267,9 @@ pub trait Store {
|
||||||
fn set_channel_note(&mut self, channel: &str, note: Option<String>) -> bool;
|
fn set_channel_note(&mut self, channel: &str, note: Option<String>) -> bool;
|
||||||
fn channel_note(&self, channel: &str) -> Option<String>;
|
fn channel_note(&self, channel: &str) -> Option<String>;
|
||||||
// News items shown on connect (logon) / login (oper), oper-only to manage.
|
// News items shown on connect (logon) / login (oper), oper-only to manage.
|
||||||
fn news_add(&mut self, kind: &str, text: &str, setter: &str) -> u64;
|
fn news_add(&mut self, kind: NewsKind, text: &str, setter: &str) -> u64;
|
||||||
fn news_del(&mut self, id: u64) -> bool;
|
fn news_del(&mut self, id: u64) -> bool;
|
||||||
fn news(&self, kind: &str) -> Vec<NewsView>;
|
fn news(&self, kind: NewsKind) -> Vec<NewsView>;
|
||||||
// Abuse reports (ReportServ). `report_file` is rate-limited (None = too soon).
|
// Abuse reports (ReportServ). `report_file` is rate-limited (None = too soon).
|
||||||
fn report_file(&mut self, reporter: &str, target: &str, reason: &str) -> Option<u64>;
|
fn report_file(&mut self, reporter: &str, target: &str, reason: &str) -> Option<u64>;
|
||||||
fn report_close(&mut self, id: u64) -> bool;
|
fn report_close(&mut self, id: u64) -> bool;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use echo_api::{Priv, Sender, ServiceCtx, Store};
|
use echo_api::{NewsKind, Priv, Sender, ServiceCtx, Store};
|
||||||
|
|
||||||
// DEL/ODEL <number>: remove a bulletin by its listed position. Admin only.
|
// DEL/ODEL <number>: remove a bulletin by its listed position. Admin only.
|
||||||
pub fn handle(me: &str, from: &Sender, kind: &str, num: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, kind: NewsKind, num: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Admin) {
|
||||||
ctx.notice(me, from.uid, "Access denied — that command is for services operators.");
|
ctx.notice(me, from.uid, "Access denied — that command is for services operators.");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
//! and any operator may OLIST. `lib.rs` holds the dispatcher; each command
|
//! and any operator may OLIST. `lib.rs` holds the dispatcher; each command
|
||||||
//! (parameterised by bulletin kind) lives in its own file.
|
//! (parameterised by bulletin kind) lives in its own file.
|
||||||
|
|
||||||
use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store};
|
use echo_api::{HelpEntry, NetView, NewsKind, Sender, Service, ServiceCtx, Store};
|
||||||
|
|
||||||
#[path = "post.rs"]
|
#[path = "post.rs"]
|
||||||
mod post;
|
mod post;
|
||||||
|
|
@ -19,8 +19,8 @@ mod del;
|
||||||
mod list;
|
mod list;
|
||||||
|
|
||||||
// The two bulletin kinds in the shared news store.
|
// The two bulletin kinds in the shared news store.
|
||||||
const PUBLIC: &str = "logon";
|
const PUBLIC: NewsKind = NewsKind::Logon;
|
||||||
const OPER: &str = "oper";
|
const OPER: NewsKind = NewsKind::Oper;
|
||||||
|
|
||||||
const BLURB: &str = "InfoServ holds the network's information bulletins: public ones show on connect, oper ones on login.";
|
const BLURB: &str = "InfoServ holds the network's information bulletins: public ones show on connect, oper ones on login.";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use echo_api::{Sender, ServiceCtx, Store};
|
use echo_api::{NewsKind, Sender, ServiceCtx, Store};
|
||||||
|
|
||||||
// LIST/OLIST: show the bulletins of a kind. Public is open; oper is oper-only.
|
// LIST/OLIST: show the bulletins of a kind. Public is open; oper is oper-only.
|
||||||
pub fn handle(me: &str, from: &Sender, kind: &str, oper_only: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, kind: NewsKind, oper_only: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
if oper_only && !from.privs.any() {
|
if oper_only && !from.privs.any() {
|
||||||
ctx.notice(me, from.uid, "Access denied — oper bulletins are for services operators.");
|
ctx.notice(me, from.uid, "Access denied — oper bulletins are for services operators.");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use echo_api::{Priv, Sender, ServiceCtx, Store};
|
use echo_api::{NewsKind, Priv, Sender, ServiceCtx, Store};
|
||||||
|
|
||||||
// POST/OPOST <message>: add a bulletin (public or oper). Admin only.
|
// POST/OPOST <message>: add a bulletin (public or oper). Admin only.
|
||||||
pub fn handle(me: &str, from: &Sender, kind: &str, rest: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, kind: NewsKind, rest: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Admin) {
|
||||||
ctx.notice(me, from.uid, "Access denied — posting bulletins is for services operators.");
|
ctx.notice(me, from.uid, "Access denied — posting bulletins is for services operators.");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ pub(crate) use event::{apply, Scope};
|
||||||
// echo-api SDK crate; re-exported so the engine keeps naming them locally and
|
// echo-api SDK crate; re-exported so the engine keeps naming them locally and
|
||||||
// modules importing `crate::engine::db::{ChanError, ...}` are unaffected.
|
// modules importing `crate::engine::db::{ChanError, ...}` are unaffected.
|
||||||
pub use echo_api::{
|
pub use echo_api::{
|
||||||
AccountView, AjoinView, AkillView, BotView, Caps, ForbidKind, ForbidView, GroupView, HelpView, IgnoreView, MemoView, NewsView, Privs, ReportView, SuspensionView, ChanAccessView, ChanAkickView, ChanError, ChanSetting, ChannelView, CertError, CodeKind, Kicker, RegError, Store, TriggerView, VhostView, XlineKind,
|
AccountView, AjoinView, AkillView, BotView, Caps, ForbidKind, ForbidView, GroupView, HelpView, IgnoreView, MemoView, NewsKind, NewsView, Privs, ReportView, SuspensionView, ChanAccessView, ChanAkickView, ChanError, ChanSetting, ChannelView, CertError, CodeKind, Kicker, RegError, Store, TriggerView, VhostView, XlineKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -160,13 +160,14 @@ impl Db {
|
||||||
.map(|g| Privs::from_names(&g.privs))
|
.map(|g| Privs::from_names(&g.privs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a news item of `kind` ("logon"/"oper"). Returns its stable id.
|
/// Add a news item of `kind`. Returns its stable id.
|
||||||
pub fn news_add(&mut self, kind: &str, text: &str, setter: &str) -> u64 {
|
pub fn news_add(&mut self, kind: NewsKind, text: &str, setter: &str) -> u64 {
|
||||||
let id = self.net.news_seq;
|
let id = self.net.news_seq;
|
||||||
let ts = now();
|
let ts = now();
|
||||||
let _ = self.log.append(Event::NewsAdded { id, kind: kind.to_string(), text: text.to_string(), setter: setter.to_string(), ts });
|
let wire = kind.wire();
|
||||||
|
let _ = self.log.append(Event::NewsAdded { id, kind: wire.to_string(), text: text.to_string(), setter: setter.to_string(), ts });
|
||||||
self.net.news_seq = id + 1;
|
self.net.news_seq = id + 1;
|
||||||
self.net.news.push(News { id, kind: kind.to_string(), text: text.to_string(), setter: setter.to_string(), ts });
|
self.net.news.push(News { id, kind: wire.to_string(), text: text.to_string(), setter: setter.to_string(), ts });
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,11 +182,12 @@ impl Db {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The news items of `kind`, oldest first.
|
/// The news items of `kind`, oldest first.
|
||||||
pub fn news(&self, kind: &str) -> Vec<NewsView> {
|
pub fn news(&self, kind: NewsKind) -> Vec<NewsView> {
|
||||||
|
let wire = kind.wire();
|
||||||
self.net
|
self.net
|
||||||
.news
|
.news
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|n| n.kind == kind)
|
.filter(|n| n.kind == wire)
|
||||||
.map(|n| NewsView { id: n.id, text: n.text.clone(), setter: n.setter.clone(), ts: n.ts })
|
.map(|n| NewsView { id: n.id, text: n.text.clone(), setter: n.setter.clone(), ts: n.ts })
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -281,13 +281,13 @@ impl Store for Db {
|
||||||
fn channel_note(&self, channel: &str) -> Option<String> {
|
fn channel_note(&self, channel: &str) -> Option<String> {
|
||||||
Db::channel_note(self, channel)
|
Db::channel_note(self, channel)
|
||||||
}
|
}
|
||||||
fn news_add(&mut self, kind: &str, text: &str, setter: &str) -> u64 {
|
fn news_add(&mut self, kind: NewsKind, text: &str, setter: &str) -> u64 {
|
||||||
Db::news_add(self, kind, text, setter)
|
Db::news_add(self, kind, text, setter)
|
||||||
}
|
}
|
||||||
fn news_del(&mut self, id: u64) -> bool {
|
fn news_del(&mut self, id: u64) -> bool {
|
||||||
Db::news_del(self, id)
|
Db::news_del(self, id)
|
||||||
}
|
}
|
||||||
fn news(&self, kind: &str) -> Vec<NewsView> {
|
fn news(&self, kind: NewsKind) -> Vec<NewsView> {
|
||||||
Db::news(self, kind)
|
Db::news(self, kind)
|
||||||
}
|
}
|
||||||
fn report_file(&mut self, reporter: &str, target: &str, reason: &str) -> Option<u64> {
|
fn report_file(&mut self, reporter: &str, target: &str, reason: &str) -> Option<u64> {
|
||||||
|
|
|
||||||
|
|
@ -568,8 +568,8 @@
|
||||||
vf.sort();
|
vf.sort();
|
||||||
let mut jp = db.jupes();
|
let mut jp = db.jupes();
|
||||||
jp.sort();
|
jp.sort();
|
||||||
let mut nw = db.news("logon");
|
let mut nw = db.news(NewsKind::Logon);
|
||||||
nw.extend(db.news("oper"));
|
nw.extend(db.news(NewsKind::Oper));
|
||||||
let rp = db.reports(false);
|
let rp = db.reports(false);
|
||||||
let hp = db.help_tickets(false);
|
let hp = db.help_tickets(false);
|
||||||
let mut se = db.session_exceptions();
|
let mut se = db.session_exceptions();
|
||||||
|
|
@ -626,8 +626,8 @@
|
||||||
db.set_account_note("bob", Some("watch this one".into()));
|
db.set_account_note("bob", Some("watch this one".into()));
|
||||||
db.set_channel_note("#chan", Some("chan note".into()));
|
db.set_channel_note("#chan", Some("chan note".into()));
|
||||||
db.set_email("carol", Some("c@x.io".into())).unwrap();
|
db.set_email("carol", Some("c@x.io".into())).unwrap();
|
||||||
db.news_add("logon", "welcome all", "oper");
|
db.news_add(NewsKind::Logon, "welcome all", "oper");
|
||||||
db.news_add("oper", "staff notice", "oper");
|
db.news_add(NewsKind::Oper, "staff notice", "oper");
|
||||||
let rid = db.report_file("carol", "bob", "spam").unwrap();
|
let rid = db.report_file("carol", "bob", "spam").unwrap();
|
||||||
db.report_close(rid);
|
db.report_close(rid);
|
||||||
let hid = db.help_request("carol", "help me").unwrap();
|
let hid = db.help_request("carol", "help me").unwrap();
|
||||||
|
|
@ -644,7 +644,7 @@
|
||||||
db.akill_del(XlineKind::Qline, "spam*").unwrap();
|
db.akill_del(XlineKind::Qline, "spam*").unwrap();
|
||||||
db.forbid_add(ForbidKind::Chan, "#evil*", "oper", "bad").unwrap();
|
db.forbid_add(ForbidKind::Chan, "#evil*", "oper", "bad").unwrap();
|
||||||
db.forbid_del(ForbidKind::Chan, "#evil*").unwrap();
|
db.forbid_del(ForbidKind::Chan, "#evil*").unwrap();
|
||||||
let nid = db.news_add("logon", "temp notice", "oper");
|
let nid = db.news_add(NewsKind::Logon, "temp notice", "oper");
|
||||||
db.news_del(nid);
|
db.news_del(nid);
|
||||||
db.certfp_add("bob", "1122334455667788990011223344556677889900").unwrap();
|
db.certfp_add("bob", "1122334455667788990011223344556677889900").unwrap();
|
||||||
db.certfp_del("bob", "1122334455667788990011223344556677889900").unwrap();
|
db.certfp_del("bob", "1122334455667788990011223344556677889900").unwrap();
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::proto::{AuthThen, NetAction, NetEvent, RegReply};
|
use crate::proto::{AuthThen, NetAction, NetEvent, RegReply};
|
||||||
use db::{Db, LogEntry, RegError};
|
use db::{Db, LogEntry, NewsKind, RegError};
|
||||||
use scram::Verifier;
|
use scram::Verifier;
|
||||||
use echo_api::Privs;
|
use echo_api::Privs;
|
||||||
use service::{Sender, Service, ServiceCtx};
|
use service::{Sender, Service, ServiceCtx};
|
||||||
|
|
@ -754,7 +754,7 @@ impl Engine {
|
||||||
|
|
||||||
// The news items of `kind` as server-sourced notices to `uid`, each tagged
|
// The news items of `kind` as server-sourced notices to `uid`, each tagged
|
||||||
// with `label` so it reads as an announcement. Empty if we have no SID yet.
|
// with `label` so it reads as an announcement. Empty if we have no SID yet.
|
||||||
fn news_notices(&self, kind: &str, label: &str, uid: &str) -> Vec<NetAction> {
|
fn news_notices(&self, kind: NewsKind, label: &str, uid: &str) -> Vec<NetAction> {
|
||||||
if self.sid.is_empty() {
|
if self.sid.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
@ -1100,7 +1100,7 @@ impl Engine {
|
||||||
// Greet with the logon news, and — separately — prompt-then-
|
// Greet with the logon news, and — separately — prompt-then-
|
||||||
// enforce if they arrived on a registered nick they aren't
|
// enforce if they arrived on a registered nick they aren't
|
||||||
// identified to.
|
// identified to.
|
||||||
let mut out = self.news_notices("logon", "News", &uid);
|
let mut out = self.news_notices(NewsKind::Logon, "News", &uid);
|
||||||
out.extend(self.enforce_registered_nick(&uid, &arriving_nick));
|
out.extend(self.enforce_registered_nick(&uid, &arriving_nick));
|
||||||
// A user already logged in the moment they connect (SASL during
|
// A user already logged in the moment they connect (SASL during
|
||||||
// registration) never ran the IDENTIFY path, so give them their
|
// registration) never ran the IDENTIFY path, so give them their
|
||||||
|
|
@ -1446,7 +1446,7 @@ impl Engine {
|
||||||
self.db.mark_account_seen(value, self.now_secs());
|
self.db.mark_account_seen(value, self.now_secs());
|
||||||
// An operator logging in is shown the staff (oper) news.
|
// An operator logging in is shown the staff (oper) news.
|
||||||
if self.oper_privs(value).any() {
|
if self.oper_privs(value).any() {
|
||||||
for note in self.news_notices("oper", "Staff News", target) {
|
for note in self.news_notices(NewsKind::Oper, "Staff News", target) {
|
||||||
self.emit_irc(note);
|
self.emit_irc(note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5160,10 +5160,10 @@ fn infoserv_posts_a_bulletin_admin_only() {
|
||||||
svc_login(&mut e, "alice");
|
svc_login(&mut e, "alice");
|
||||||
// Not an oper yet: refused.
|
// Not an oper yet: refused.
|
||||||
assert!(svc_ask(&mut e, "42SAAAAAJ", "POST scheduled maintenance tonight").iter().any(|l| l.contains("Access denied")));
|
assert!(svc_ask(&mut e, "42SAAAAAJ", "POST scheduled maintenance tonight").iter().any(|l| l.contains("Access denied")));
|
||||||
assert_eq!(e.db.news("logon").len(), 0);
|
assert_eq!(e.db.news(NewsKind::Logon).len(), 0);
|
||||||
svc_oper(&mut e, "alice");
|
svc_oper(&mut e, "alice");
|
||||||
svc_ask(&mut e, "42SAAAAAJ", "POST scheduled maintenance tonight");
|
svc_ask(&mut e, "42SAAAAAJ", "POST scheduled maintenance tonight");
|
||||||
assert_eq!(e.db.news("logon").len(), 1, "bulletin posted");
|
assert_eq!(e.db.news(NewsKind::Logon).len(), 1, "bulletin posted");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue