rpc: implement the deferred methods — channel.set_mode, server.connect, log.tail/log.events (in-memory log ring)

This commit is contained in:
Jean Chevronnet 2026-08-09 16:36:34 +00:00
parent 185ff471b1
commit 06afd1e59d
7 changed files with 214 additions and 0 deletions

View file

@ -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);
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}"))),
}
}

View file

@ -112,6 +112,36 @@ pub fn get_num<T: std::str::FromStr>(obj: &str, key: &str) -> Option<T> {
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"`).
pub fn get_bool(obj: &str, key: &str) -> Option<bool> {
match get_raw(obj, key)?.trim_matches('"') {

46
src/modules/rpc/log.rs Normal file
View 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}"))),
}
}

View file

@ -19,6 +19,7 @@ pub mod channel;
pub mod core;
pub mod httpd;
pub mod json;
pub mod log;
pub mod message;
pub mod server;
pub mod spamfilter;
@ -83,8 +84,10 @@ pub const ALL_METHODS: &[&str] = &[
"channel.get",
"channel.kick",
"channel.set_topic",
"channel.set_mode",
"server.list",
"server.rehash",
"server.connect",
"server.disconnect",
"module.list",
"oper.list",
@ -97,6 +100,8 @@ pub const ALL_METHODS: &[&str] = &[
"spamfilter.list",
"spamfilter.add",
"spamfilter.del",
"log.tail",
"log.events",
];
/// 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(("whowas", action)) => whowas::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)),
},
};

View file

@ -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")),
},
"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" => {
let name = json_name(params)?;
let via = s