nickserv: INFO shows services role + GroupServ groups (config∪runtime oper privs, all catalogs)
All checks were successful
CI / check (push) Successful in 5m49s

This commit is contained in:
Jean Chevronnet 2026-08-26 22:06:57 +00:00
parent e303350b2b
commit 44d45e9c68
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
10 changed files with 51 additions and 10 deletions

View file

@ -2478,6 +2478,12 @@ pub trait NetView {
fn oper_accounts(&self) -> Vec<String> {
Vec::new()
}
// The services-operator privileges this account holds from the `[[oper]]` config
// (NickServ INFO shows the tier). Empty when it isn't a config operator; runtime
// OPER grants (Store::opers_list) are unioned in by the caller.
fn config_oper_privs(&self, _account: &str) -> Privs {
Privs::default()
}
fn uid_by_nick(&self, nick: &str) -> Option<&str>;
fn nick_of(&self, uid: &str) -> Option<&str>;
fn host_of(&self, uid: &str) -> Option<&str>;

View file

@ -1,4 +1,6 @@
{
" Services role : {role}": " Dienste-Rolle : {role}",
" Groups : {list} (from GroupServ)": " Gruppen : {list} (von GroupServ)",
"{count} year": "{count} Jahr",
"{count} years": "{count} Jahre",
"{count} month": "{count} Monat",

View file

@ -1,4 +1,6 @@
{
" Services role : {role}": " Rol de servicios : {role}",
" Groups : {list} (from GroupServ)": " Grupos : {list} (de GroupServ)",
"{count} year": "{count} año",
"{count} years": "{count} años",
"{count} month": "{count} mes",

View file

@ -1,4 +1,6 @@
{
" Services role : {role}": " Rol de servicios : {role}",
" Groups : {list} (from GroupServ)": " Grupos : {list} (de GroupServ)",
"{count} year": "{count} año",
"{count} years": "{count} años",
"{count} month": "{count} mes",

View file

@ -1,4 +1,6 @@
{
" Services role : {role}": " Rôle services : {role}",
" Groups : {list} (from GroupServ)": " Groupes : {list} (via GroupServ)",
"{count} year": "{count} an",
"{count} years": "{count} ans",
"{count} month": "{count} mois",

View file

@ -1,4 +1,6 @@
{
" Services role : {role}": " Cargo de serviços : {role}",
" Groups : {list} (from GroupServ)": " Grupos : {list} (do GroupServ)",
"{count} year": "{count} ano",
"{count} years": "{count} anos",
"{count} month": "{count} mês",

View file

@ -1,4 +1,6 @@
{
" Services role : {role}": " Cargo de serviços : {role}",
" Groups : {list} (from GroupServ)": " Grupos : {list} (do GroupServ)",
"{count} year": "{count} ano",
"{count} years": "{count} anos",
"{count} month": "{count} mês",

View file

@ -1,4 +1,4 @@
use echo_api::{human_ago, human_time, NetView, Priv, ProfileField, Store};
use echo_api::{human_ago, human_time, NetView, Priv, Privs, ProfileField, Store};
use echo_api::{Sender, ServiceCtx};
use echo_api::t;
@ -16,6 +16,21 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
ctx.notice(me, from.uid, t!(ctx, "Information for \x02{name}\x02:", name = acct.name));
let age = human_ago(ctx.lang(), now_unix().saturating_sub(acct.ts));
ctx.notice(me, from.uid, t!(ctx, " Registered : {when} ({age} ago)", when = human_time(acct.ts), age = age));
// Services-operator role: the config-oper tier unioned with any live runtime OPER
// grant. Public (WHOIS already reveals oper status); shown only when there is one.
let now = now_unix();
let mut privs = net.config_oper_privs(&acct.name);
if let Some((_, names, _)) = db.opers_list().into_iter().find(|(a, _, exp)| a.eq_ignore_ascii_case(&acct.name) && exp.is_none_or(|e| e > now)) {
privs = privs.union(Privs::from_names(&names));
}
if privs.any() {
ctx.notice(me, from.uid, t!(ctx, " Services role : {role}", role = privs.names().join(", ")));
}
// GroupServ groups the account belongs to — public account identity.
let groups = db.groups_of(&acct.name);
if !groups.is_empty() {
ctx.notice(me, from.uid, t!(ctx, " Groups : {list} (from GroupServ)", list = groups.join(" ")));
}
// Last-seen is public by default; SET HIDE STATUS keeps it to owner and opers.
if privileged || !acct.hide_status {
let last_seen = if net.uids_logged_into(&acct.name).is_empty() {

View file

@ -350,7 +350,7 @@ impl Engine {
// Load the services-operator table (casefolded account -> privileges).
pub fn set_opers(&mut self, opers: HashMap<String, Privs>) {
self.network.set_config_opers(opers.keys().cloned().collect());
self.network.set_config_opers(opers.clone());
self.opers = opers;
}

View file

@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
// The read-only network view a module sees; re-exported so the engine keeps
// naming it locally.
pub use echo_api::{ChanSeenView, HelpEntry, IncidentView, NetView, SeenView};
pub use echo_api::{ChanSeenView, HelpEntry, IncidentView, NetView, Privs, SeenView};
// The most recent moderation/action incidents kept for LOGSEARCH.
const INCIDENT_CAP: usize = 10_000;
@ -52,10 +52,11 @@ pub struct Network {
// Network-wide help index (service nick, blurb, per-command topics), built by
// the engine at startup from every service so HelpServ can front it.
pub help_catalog: Vec<(String, &'static str, &'static [HelpEntry])>,
// Account names of the declarative config operators, mirrored from the engine
// so services can enumerate staff (MemoServ STAFF). Runtime OPER grants live
// in the store; a caller unions the two.
config_opers: Vec<String>,
// The declarative config operators and the privilege tier each holds, mirrored
// from the engine so services can enumerate staff (MemoServ STAFF) and show a
// role (NickServ INFO). Runtime OPER grants live in the store; a caller unions
// the two.
config_opers: HashMap<String, Privs>,
}
// ChanFix scoring caps and rates.
@ -296,8 +297,8 @@ impl Network {
// Mirror the engine's declarative config operators, so services can enumerate
// staff. Called whenever the config oper set changes.
pub fn set_config_opers(&mut self, accounts: Vec<String>) {
self.config_opers = accounts;
pub fn set_config_opers(&mut self, opers: HashMap<String, Privs>) {
self.config_opers = opers;
}
// Live sessions from `ip`.
@ -849,7 +850,14 @@ impl Network {
// with the seen record projected into a plain view.
impl NetView for Network {
fn oper_accounts(&self) -> Vec<String> {
self.config_opers.clone()
self.config_opers.keys().cloned().collect()
}
fn config_oper_privs(&self, account: &str) -> Privs {
self.config_opers
.iter()
.find(|(a, _)| a.eq_ignore_ascii_case(account))
.map(|(_, p)| *p)
.unwrap_or_default()
}
fn uid_by_nick(&self, nick: &str) -> Option<&str> {
Network::uid_by_nick(self, nick)