rpc: implement the deferred methods — channel.set_mode, server.connect, log.tail/log.events (in-memory log ring)
This commit is contained in:
parent
185ff471b1
commit
06afd1e59d
7 changed files with 214 additions and 0 deletions
|
|
@ -155,6 +155,66 @@ pub fn apply_mode(s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply channel modes with **server** authority (no acting user) — for the RPC
|
||||||
|
/// `channel.set_mode`. Same per-letter dispatch as [`apply_mode`] under `mode_sudo`
|
||||||
|
/// (so every rank gate passes), but the resulting `MODE` line is sourced from the
|
||||||
|
/// server, not a user. The handlers only ever touch the actor uid through
|
||||||
|
/// `s.rank()` (maxed by sudo) or `s.users.get()` (safe on the `0` sentinel), so no
|
||||||
|
/// live actor is needed. Returns whether anything actually changed.
|
||||||
|
pub fn svs_set_chan_modes(s: &mut Server, target: &str, modestring: &str, args: &[String]) -> bool {
|
||||||
|
let key = target.to_ascii_lowercase();
|
||||||
|
if !s.channels.contains_key(&key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
s.mode_sudo = true;
|
||||||
|
let mut argi = 0usize;
|
||||||
|
let mut sign = '+';
|
||||||
|
let mut applied = String::new();
|
||||||
|
let mut last = ' ';
|
||||||
|
let mut echoed: Vec<String> = Vec::new();
|
||||||
|
for c in modestring.chars() {
|
||||||
|
if c == '+' || c == '-' {
|
||||||
|
sign = c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let adding = sign == '+';
|
||||||
|
let Some(handler) = chan_mode(c) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let param = if handler.wants_param(adding) {
|
||||||
|
let p = args.get(argi).cloned();
|
||||||
|
if p.is_some() {
|
||||||
|
argi += 1;
|
||||||
|
}
|
||||||
|
p
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Applied::Yes(echo) = handler.apply(s, target, &key, 0, adding, param.as_deref()) {
|
||||||
|
emit(&mut applied, &mut last, sign, c);
|
||||||
|
if let Some(p) = echo {
|
||||||
|
echoed.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mode_sudo = false;
|
||||||
|
if applied.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let pstr = if echoed.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" {}", echoed.join(" "))
|
||||||
|
};
|
||||||
|
s.to_channel(
|
||||||
|
&key,
|
||||||
|
&format!(":{} MODE {target} {applied}{pstr}", s.name),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
s.propagate(&format!(":{} MODE {target} {applied}{pstr}", s.sid), None); // links
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// User modes: dispatched to the [`crate::mode`] `UserMode` handler objects.
|
/// User modes: dispatched to the [`crate::mode`] `UserMode` handler objects.
|
||||||
fn apply_user_modes(s: &mut Server, uid: Uid, target: &str, params: &[String]) -> CmdResult {
|
fn apply_user_modes(s: &mut Server, uid: Uid, target: &str, params: &[String]) -> CmdResult {
|
||||||
let me = s
|
let me = s
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,24 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcE
|
||||||
s.to_channel(&key, &format!(":{} TOPIC {display} :{text}", s.name), None);
|
s.to_channel(&key, &format!(":{} TOPIC {display} :{text}", s.name), None);
|
||||||
Ok(obj(&[("result", "true".into())]))
|
Ok(obj(&[("result", "true".into())]))
|
||||||
}
|
}
|
||||||
|
"set_mode" => {
|
||||||
|
let name = json_channel(params)?;
|
||||||
|
let key = name.to_ascii_lowercase();
|
||||||
|
let modes = super::json::get_str(params, "modes")
|
||||||
|
.ok_or_else(|| RpcError::invalid_params("missing 'modes'"))?;
|
||||||
|
if !s.channels.contains_key(&key) {
|
||||||
|
return Err(RpcError::not_found("no such channel"));
|
||||||
|
}
|
||||||
|
// `parameters` (array) or `param` (single) — the mode arguments
|
||||||
|
let mut args = super::json::get_str_array(params, "parameters");
|
||||||
|
if args.is_empty() {
|
||||||
|
if let Some(p) = super::json::get_str(params, "param") {
|
||||||
|
args.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let changed = crate::coremods::core_mode::svs_set_chan_modes(s, &name, &modes, &args);
|
||||||
|
Ok(obj(&[("result", changed.to_string())]))
|
||||||
|
}
|
||||||
other => Err(RpcError::method_not_found(&format!("channel.{other}"))),
|
other => Err(RpcError::method_not_found(&format!("channel.{other}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,36 @@ pub fn get_num<T: std::str::FromStr>(obj: &str, key: &str) -> Option<T> {
|
||||||
raw.trim_matches('"').parse().ok()
|
raw.trim_matches('"').parse().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The string elements of a top-level array field, e.g. `"parameters":["a","b"]`
|
||||||
|
/// → `["a","b"]`. Empty vec if the key is absent or not an array of strings.
|
||||||
|
pub fn get_str_array(obj: &str, key: &str) -> Vec<String> {
|
||||||
|
let Some(raw) = get_raw(obj, key) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let inner = raw.trim();
|
||||||
|
if !inner.starts_with('[') {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let b = inner.as_bytes();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut i = 1;
|
||||||
|
while i < b.len() {
|
||||||
|
match b[i] {
|
||||||
|
b'"' => {
|
||||||
|
if let Some(end) = scan_string(b, i) {
|
||||||
|
out.push(unescape(&inner[i + 1..end - 1]));
|
||||||
|
i = end;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b']' => break,
|
||||||
|
_ => i += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// A boolean field (`true`/`false`, or the strings `"true"`/`"false"`).
|
/// A boolean field (`true`/`false`, or the strings `"true"`/`"false"`).
|
||||||
pub fn get_bool(obj: &str, key: &str) -> Option<bool> {
|
pub fn get_bool(obj: &str, key: &str) -> Option<bool> {
|
||||||
match get_raw(obj, key)?.trim_matches('"') {
|
match get_raw(obj, key)?.trim_matches('"') {
|
||||||
|
|
|
||||||
46
src/modules/rpc/log.rs
Normal file
46
src/modules/rpc/log.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
//! rpc log provider — `log.tail` and `log.events`. InspIRCd's `m_rpc_log` /
|
||||||
|
//! `m_jsonrpclog`. echoIRCd has no log *file* (it logs to journald), so both read
|
||||||
|
//! the in-memory server-log ring that `Server::snotice` feeds (`Server.log`).
|
||||||
|
|
||||||
|
use super::json::{self, obj, qstr};
|
||||||
|
use super::RpcError;
|
||||||
|
use crate::server::Server;
|
||||||
|
|
||||||
|
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||||
|
let log = s.log.borrow();
|
||||||
|
match action {
|
||||||
|
"tail" => {
|
||||||
|
let n = json::get_num::<usize>(params, "lines")
|
||||||
|
.unwrap_or(200)
|
||||||
|
.clamp(1, 1000);
|
||||||
|
let start = log.ring.len().saturating_sub(n);
|
||||||
|
let lines: Vec<String> = log.ring.iter().skip(start).map(|e| qstr(&e.msg)).collect();
|
||||||
|
Ok(obj(&[("lines", format!("[{}]", lines.join(",")))]))
|
||||||
|
}
|
||||||
|
"events" => {
|
||||||
|
let since = json::get_num::<u64>(params, "since").unwrap_or(0);
|
||||||
|
let limit = json::get_num::<usize>(params, "limit")
|
||||||
|
.unwrap_or(200)
|
||||||
|
.clamp(1, 1000);
|
||||||
|
let events: Vec<String> = log
|
||||||
|
.ring
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.id > since)
|
||||||
|
.take(limit)
|
||||||
|
.map(|e| {
|
||||||
|
obj(&[
|
||||||
|
("id", e.id.to_string()),
|
||||||
|
("timestamp", e.ts.to_string()),
|
||||||
|
("msg", qstr(&e.msg)),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let last_id = log.ring.back().map(|e| e.id).unwrap_or(since);
|
||||||
|
Ok(obj(&[
|
||||||
|
("events", format!("[{}]", events.join(","))),
|
||||||
|
("last_id", last_id.to_string()),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
other => Err(RpcError::method_not_found(&format!("log.{other}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ pub mod channel;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod httpd;
|
pub mod httpd;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
pub mod log;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod spamfilter;
|
pub mod spamfilter;
|
||||||
|
|
@ -83,8 +84,10 @@ pub const ALL_METHODS: &[&str] = &[
|
||||||
"channel.get",
|
"channel.get",
|
||||||
"channel.kick",
|
"channel.kick",
|
||||||
"channel.set_topic",
|
"channel.set_topic",
|
||||||
|
"channel.set_mode",
|
||||||
"server.list",
|
"server.list",
|
||||||
"server.rehash",
|
"server.rehash",
|
||||||
|
"server.connect",
|
||||||
"server.disconnect",
|
"server.disconnect",
|
||||||
"module.list",
|
"module.list",
|
||||||
"oper.list",
|
"oper.list",
|
||||||
|
|
@ -97,6 +100,8 @@ pub const ALL_METHODS: &[&str] = &[
|
||||||
"spamfilter.list",
|
"spamfilter.list",
|
||||||
"spamfilter.add",
|
"spamfilter.add",
|
||||||
"spamfilter.del",
|
"spamfilter.del",
|
||||||
|
"log.tail",
|
||||||
|
"log.events",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Run a parsed JSON-RPC request on the core thread. `params` is the raw JSON of
|
/// Run a parsed JSON-RPC request on the core thread. `params` is the raw JSON of
|
||||||
|
|
@ -115,6 +120,7 @@ pub fn dispatch(s: &mut Server, method: &str, params: &str, id: &str) -> String
|
||||||
Some(("message", action)) => message::handle(s, action, params),
|
Some(("message", action)) => message::handle(s, action, params),
|
||||||
Some(("whowas", action)) => whowas::handle(s, action, params),
|
Some(("whowas", action)) => whowas::handle(s, action, params),
|
||||||
Some(("spamfilter", action)) => spamfilter::handle(s, action, params),
|
Some(("spamfilter", action)) => spamfilter::handle(s, action, params),
|
||||||
|
Some(("log", action)) => log::handle(s, action, params),
|
||||||
_ => Err(RpcError::method_not_found(method)),
|
_ => Err(RpcError::method_not_found(method)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,28 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcE
|
||||||
}
|
}
|
||||||
None => Err(RpcError::internal("config file could not be read")),
|
None => Err(RpcError::internal("config file could not be read")),
|
||||||
},
|
},
|
||||||
|
"connect" => {
|
||||||
|
let name = json_name(params)?;
|
||||||
|
let Some(b) = s
|
||||||
|
.link_blocks
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.name.eq_ignore_ascii_case(&name))
|
||||||
|
.cloned()
|
||||||
|
else {
|
||||||
|
return Err(RpcError::not_found(&format!("no link block named {name}")));
|
||||||
|
};
|
||||||
|
if s.servers
|
||||||
|
.values()
|
||||||
|
.any(|sv| sv.name.eq_ignore_ascii_case(&b.name))
|
||||||
|
{
|
||||||
|
return Err(RpcError::invalid_params("server is already linked"));
|
||||||
|
}
|
||||||
|
let addr = format!("{}:{}", b.ip, b.port);
|
||||||
|
let (tx, counter) = (s.event_tx.clone(), s.conn_counter.clone());
|
||||||
|
std::thread::spawn(move || crate::socketengine::connect_link(&addr, tx, counter));
|
||||||
|
s.snotice(&format!("RPC initiated a link to {}", b.name));
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
"disconnect" => {
|
"disconnect" => {
|
||||||
let name = json_name(params)?;
|
let name = json_name(params)?;
|
||||||
let via = s
|
let via = s
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,21 @@ pub struct WhowasEntry {
|
||||||
pub ts: u64,
|
pub ts: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One captured server log line (fed by `snotice`), for the RPC `log.tail` /
|
||||||
|
/// `log.events` methods — echoIRCd's in-memory answer to InspIRCd's log file.
|
||||||
|
pub struct LogLine {
|
||||||
|
pub id: u64,
|
||||||
|
pub ts: u64,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rolling server-log ring plus its monotonic sequence counter.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LogState {
|
||||||
|
pub seq: u64,
|
||||||
|
pub ring: VecDeque<LogLine>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub network: String,
|
pub network: String,
|
||||||
|
|
@ -141,6 +156,8 @@ pub struct Server {
|
||||||
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
|
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
|
||||||
// output primitives are `&self`.
|
// output primitives are `&self`.
|
||||||
pub label_capture: RefCell<Option<(Uid, Vec<String>)>>,
|
pub label_capture: RefCell<Option<(Uid, Vec<String>)>>,
|
||||||
|
/// Rolling in-memory server log (fed by `snotice`), read by the RPC log methods.
|
||||||
|
pub log: RefCell<LogState>,
|
||||||
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
||||||
pub conn_counter: Arc<AtomicU64>, // mints connection uids (for CONNECT dials)
|
pub conn_counter: Arc<AtomicU64>, // mints connection uids (for CONNECT dials)
|
||||||
/// Module-owned server state, keyed by type — the InspIRCd `ExtensionItem`
|
/// Module-owned server state, keyed by type — the InspIRCd `ExtensionItem`
|
||||||
|
|
@ -189,6 +206,7 @@ impl Server {
|
||||||
webirc: cfg.webirc,
|
webirc: cfg.webirc,
|
||||||
raw_config: cfg.raw,
|
raw_config: cfg.raw,
|
||||||
label_capture: RefCell::new(None),
|
label_capture: RefCell::new(None),
|
||||||
|
log: RefCell::new(LogState::default()),
|
||||||
event_tx,
|
event_tx,
|
||||||
conn_counter,
|
conn_counter,
|
||||||
ext: Extensible::default(),
|
ext: Extensible::default(),
|
||||||
|
|
@ -581,6 +599,20 @@ impl Server {
|
||||||
|
|
||||||
/// Send a server notice to every operator who has snomask (+s) on.
|
/// Send a server notice to every operator who has snomask (+s) on.
|
||||||
pub fn snotice(&self, msg: &str) {
|
pub fn snotice(&self, msg: &str) {
|
||||||
|
// record it in the rolling server log (for RPC log.tail / log.events)
|
||||||
|
{
|
||||||
|
let mut lg = self.log.borrow_mut();
|
||||||
|
lg.seq += 1;
|
||||||
|
let id = lg.seq;
|
||||||
|
lg.ring.push_back(LogLine {
|
||||||
|
id,
|
||||||
|
ts: now(),
|
||||||
|
msg: msg.to_string(),
|
||||||
|
});
|
||||||
|
while lg.ring.len() > 1000 {
|
||||||
|
lg.ring.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
let opers: Vec<Uid> = self
|
let opers: Vec<Uid> = self
|
||||||
.users
|
.users
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue