rpc: add user + channel providers (list/get + kill/kick/set_topic/set_nick/set_mode/set_vhost/set_oper)
This commit is contained in:
parent
4b1f096ed8
commit
ee445396eb
3 changed files with 314 additions and 4 deletions
159
src/modules/rpc/channel.rs
Normal file
159
src/modules/rpc/channel.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
//! rpc channel provider — `channel.list`, `channel.get`, and the mutators
|
||||||
|
//! `channel.kick`, `channel.set_topic`. InspIRCd's `m_rpc_channel`. (`channel.set_mode`
|
||||||
|
//! lands with the shared server-side mode applier in a later pass.)
|
||||||
|
|
||||||
|
use super::json::{obj, qstr};
|
||||||
|
use super::RpcError;
|
||||||
|
use crate::channels::Topic;
|
||||||
|
use crate::server::{now, Server};
|
||||||
|
|
||||||
|
/// All prefix chars a member holds, highest first (e.g. `"@+"`).
|
||||||
|
fn prefixes(m: &crate::channels::Member) -> String {
|
||||||
|
let mut p = String::new();
|
||||||
|
for (has, ch) in [
|
||||||
|
(m.owner, '~'),
|
||||||
|
(m.admin, '&'),
|
||||||
|
(m.op, '@'),
|
||||||
|
(m.halfop, '%'),
|
||||||
|
(m.voice, '+'),
|
||||||
|
] {
|
||||||
|
if has {
|
||||||
|
p.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compact channel object for `channel.list`.
|
||||||
|
fn brief(s: &Server, key: &str) -> String {
|
||||||
|
let ch = &s.channels[key];
|
||||||
|
obj(&[
|
||||||
|
("name", qstr(&ch.name)),
|
||||||
|
("usercount", ch.members.len().to_string()),
|
||||||
|
("modes", qstr(&ch.modes.render(true))),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full channel object for `channel.get`.
|
||||||
|
fn full(s: &Server, key: &str) -> String {
|
||||||
|
let ch = &s.channels[key];
|
||||||
|
let members: Vec<String> = ch
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.map(|(&uid, m)| {
|
||||||
|
let (nick, uuid) = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| (u.nick.clone(), u.uuid.clone()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
obj(&[
|
||||||
|
("nick", qstr(&nick)),
|
||||||
|
("uuid", qstr(&uuid)),
|
||||||
|
("prefixes", qstr(&prefixes(m))),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let bans: Vec<String> = ch
|
||||||
|
.bans
|
||||||
|
.iter()
|
||||||
|
.map(|b| {
|
||||||
|
obj(&[
|
||||||
|
("mask", qstr(&b.mask)),
|
||||||
|
("setter", qstr(&b.setter)),
|
||||||
|
("time", b.ts.to_string()),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut fields = vec![
|
||||||
|
("name", qstr(&ch.name)),
|
||||||
|
("created", ch.created.to_string()),
|
||||||
|
("usercount", ch.members.len().to_string()),
|
||||||
|
("modes", qstr(&ch.modes.render(true))),
|
||||||
|
("members", format!("[{}]", members.join(","))),
|
||||||
|
("bans", format!("[{}]", bans.join(","))),
|
||||||
|
];
|
||||||
|
if let Some(t) = &ch.topic {
|
||||||
|
let topic = obj(&[
|
||||||
|
("text", qstr(&t.text)),
|
||||||
|
("setter", qstr(&t.setter)),
|
||||||
|
("time", t.ts.to_string()),
|
||||||
|
]);
|
||||||
|
fields.push(("topic", topic));
|
||||||
|
}
|
||||||
|
obj(&fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||||
|
match action {
|
||||||
|
"list" => {
|
||||||
|
let keys: Vec<String> = s.channels.keys().cloned().collect();
|
||||||
|
let arr: Vec<String> = keys.iter().map(|k| brief(s, k)).collect();
|
||||||
|
Ok(obj(&[("channels", format!("[{}]", arr.join(",")))]))
|
||||||
|
}
|
||||||
|
"get" => {
|
||||||
|
let name = json_channel(params)?;
|
||||||
|
let key = name.to_ascii_lowercase();
|
||||||
|
if !s.channels.contains_key(&key) {
|
||||||
|
return Err(RpcError::not_found("no such channel"));
|
||||||
|
}
|
||||||
|
Ok(full(s, &key))
|
||||||
|
}
|
||||||
|
"kick" => {
|
||||||
|
let name = json_channel(params)?;
|
||||||
|
let key = name.to_ascii_lowercase();
|
||||||
|
let victim = super::json::get_str(params, "nick")
|
||||||
|
.ok_or_else(|| RpcError::invalid_params("missing 'nick'"))?;
|
||||||
|
let reason =
|
||||||
|
super::json::get_str(params, "reason").unwrap_or_else(|| "Kicked via RPC".into());
|
||||||
|
if !s.channels.contains_key(&key) {
|
||||||
|
return Err(RpcError::not_found("no such channel"));
|
||||||
|
}
|
||||||
|
let tuid = s
|
||||||
|
.find_nick(&victim)
|
||||||
|
.filter(|t| s.channels[&key].members.contains_key(t))
|
||||||
|
.ok_or_else(|| RpcError::not_found("user not on channel"))?;
|
||||||
|
s.to_channel(
|
||||||
|
&key,
|
||||||
|
&format!(
|
||||||
|
":{} KICK {} {victim} :{reason}",
|
||||||
|
s.name, s.channels[&key].name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if let Some(ch) = s.channels.get_mut(&key) {
|
||||||
|
ch.members.remove(&tuid);
|
||||||
|
}
|
||||||
|
if let Some(u) = s.users.get_mut(&tuid) {
|
||||||
|
u.channels.remove(&key);
|
||||||
|
}
|
||||||
|
s.channels.retain(|_, c| c.keep_alive());
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
|
"set_topic" => {
|
||||||
|
let name = json_channel(params)?;
|
||||||
|
let key = name.to_ascii_lowercase();
|
||||||
|
let text = super::json::get_str(params, "topic")
|
||||||
|
.ok_or_else(|| RpcError::invalid_params("missing 'topic'"))?;
|
||||||
|
if !s.channels.contains_key(&key) {
|
||||||
|
return Err(RpcError::not_found("no such channel"));
|
||||||
|
}
|
||||||
|
let display = s.channels[&key].name.clone();
|
||||||
|
if let Some(ch) = s.channels.get_mut(&key) {
|
||||||
|
ch.topic = Some(Topic {
|
||||||
|
text: text.clone(),
|
||||||
|
setter: s.name.clone(),
|
||||||
|
ts: now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
s.to_channel(&key, &format!(":{} TOPIC {display} :{text}", s.name), None);
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
|
other => Err(RpcError::method_not_found(&format!("channel.{other}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The required `channel` param.
|
||||||
|
fn json_channel(params: &str) -> Result<String, RpcError> {
|
||||||
|
super::json::get_str(params, "channel")
|
||||||
|
.ok_or_else(|| RpcError::invalid_params("missing 'channel'"))
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,11 @@
|
||||||
//! (default `127.0.0.1:8080`); every request must carry the token (HTTP Basic or
|
//! (default `127.0.0.1:8080`); every request must carry the token (HTTP Basic or
|
||||||
//! Bearer), checked constant-time on the listener thread before anything dispatches.
|
//! Bearer), checked constant-time on the listener thread before anything dispatches.
|
||||||
|
|
||||||
|
pub mod channel;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod httpd;
|
pub mod httpd;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
|
|
@ -59,17 +61,36 @@ impl RpcError {
|
||||||
|
|
||||||
/// Every method name the interface exposes (drives `rpc.methods`). Keep in sync
|
/// Every method name the interface exposes (drives `rpc.methods`). Keep in sync
|
||||||
/// with the `dispatch` routes as providers are added.
|
/// with the `dispatch` routes as providers are added.
|
||||||
pub const ALL_METHODS: &[&str] = &["rpc.methods", "rpc.info", "server.info", "stats.get"];
|
pub const ALL_METHODS: &[&str] = &[
|
||||||
|
"rpc.methods",
|
||||||
|
"rpc.info",
|
||||||
|
"server.info",
|
||||||
|
"stats.get",
|
||||||
|
"user.list",
|
||||||
|
"user.get",
|
||||||
|
"user.kill",
|
||||||
|
"user.set_mode",
|
||||||
|
"user.set_vhost",
|
||||||
|
"user.set_nick",
|
||||||
|
"user.set_oper",
|
||||||
|
"channel.list",
|
||||||
|
"channel.get",
|
||||||
|
"channel.kick",
|
||||||
|
"channel.set_topic",
|
||||||
|
];
|
||||||
|
|
||||||
/// 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
|
||||||
/// the `params` member (`{}` if none); `id` is the raw JSON of the request id
|
/// the `params` member (`{}` if none); `id` is the raw JSON of the request id
|
||||||
/// (echoed verbatim). Returns the full JSON-RPC response envelope.
|
/// (echoed verbatim). Returns the full JSON-RPC response envelope.
|
||||||
pub fn dispatch(s: &mut Server, method: &str, _params: &str, id: &str) -> String {
|
pub fn dispatch(s: &mut Server, method: &str, params: &str, id: &str) -> String {
|
||||||
// `_params` is consumed once the param-taking providers (user/channel/…) land.
|
|
||||||
let result: Result<String, RpcError> = match method {
|
let result: Result<String, RpcError> = match method {
|
||||||
"rpc.methods" | "rpc.info" => core::rpc_info(s, method),
|
"rpc.methods" | "rpc.info" => core::rpc_info(s, method),
|
||||||
"server.info" | "stats.get" => core::server_info(s),
|
"server.info" | "stats.get" => core::server_info(s),
|
||||||
other => Err(RpcError::method_not_found(other)),
|
_ => match method.split_once('.') {
|
||||||
|
Some(("user", action)) => user::handle(s, action, params),
|
||||||
|
Some(("channel", action)) => channel::handle(s, action, params),
|
||||||
|
_ => Err(RpcError::method_not_found(method)),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
envelope(method, id, result)
|
envelope(method, id, result)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
src/modules/rpc/user.rs
Normal file
130
src/modules/rpc/user.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
//! rpc user provider — `user.list`, `user.get`, and the mutators `user.kill`,
|
||||||
|
//! `user.set_mode`, `user.set_vhost`, `user.set_nick`, `user.set_oper`. InspIRCd's
|
||||||
|
//! `m_rpc_user`. Mutators route through the same `Server` primitives the commands
|
||||||
|
//! use, so behaviour and side-effects (QUIT/CHGHOST/MODE broadcasts) stay identical.
|
||||||
|
|
||||||
|
use super::json::{self, obj, qstr};
|
||||||
|
use super::RpcError;
|
||||||
|
use crate::coremods::core_mode::svs_set_user_modes;
|
||||||
|
use crate::server::Server;
|
||||||
|
use crate::Uid;
|
||||||
|
|
||||||
|
/// Resolve the target uid from a `nick` or `uuid` param.
|
||||||
|
fn resolve(s: &Server, params: &str) -> Option<Uid> {
|
||||||
|
if let Some(nick) = json::get_str(params, "nick") {
|
||||||
|
if let Some(uid) = s.find_nick(&nick) {
|
||||||
|
return Some(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(uuid) = json::get_str(params, "uuid") {
|
||||||
|
return s
|
||||||
|
.users
|
||||||
|
.iter()
|
||||||
|
.find(|(_, u)| u.uuid == uuid)
|
||||||
|
.map(|(&id, _)| id);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User modes as a bare letter string (no leading `+`).
|
||||||
|
fn modes_str(s: &Server, uid: Uid) -> String {
|
||||||
|
s.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.flags.umodes().trim_start_matches('+').to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A compact user object (used in `user.list`).
|
||||||
|
fn brief(s: &Server, uid: Uid) -> String {
|
||||||
|
let u = &s.users[&uid];
|
||||||
|
obj(&[
|
||||||
|
("nick", qstr(&u.nick)),
|
||||||
|
("uuid", qstr(&u.uuid)),
|
||||||
|
("ident", qstr(&u.ident)),
|
||||||
|
("host", qstr(u.host_display())),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full user object (used in `user.get`).
|
||||||
|
fn full(s: &Server, uid: Uid) -> String {
|
||||||
|
let u = &s.users[&uid];
|
||||||
|
let channels: Vec<String> = u.channels.iter().map(|c| qstr(c)).collect();
|
||||||
|
let mut fields = vec![
|
||||||
|
("nick", qstr(&u.nick)),
|
||||||
|
("uuid", qstr(&u.uuid)),
|
||||||
|
("ident", qstr(&u.ident)),
|
||||||
|
("realname", qstr(&u.realname)),
|
||||||
|
("host", qstr(&u.host)),
|
||||||
|
("displayhost", qstr(u.host_display())),
|
||||||
|
("ip", qstr(&u.addr.ip().to_string())),
|
||||||
|
("mask", qstr(&u.prefix())),
|
||||||
|
("server", qstr(&s.name)),
|
||||||
|
("signon", u.signon.to_string()),
|
||||||
|
("modes", qstr(&modes_str(s, uid))),
|
||||||
|
("channels", format!("[{}]", channels.join(","))),
|
||||||
|
("oper", u.flags.oper.to_string()),
|
||||||
|
("secure", u.secure.to_string()),
|
||||||
|
];
|
||||||
|
if let Some(acct) = &u.account {
|
||||||
|
fields.push(("account", qstr(acct)));
|
||||||
|
}
|
||||||
|
if let Some(away) = &u.flags.away {
|
||||||
|
fields.push(("away", qstr(away)));
|
||||||
|
}
|
||||||
|
if let Some(fp) = &u.certfp {
|
||||||
|
fields.push(("fingerprint", qstr(fp)));
|
||||||
|
}
|
||||||
|
obj(&fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||||
|
match action {
|
||||||
|
"list" => {
|
||||||
|
let users: Vec<Uid> = s.users.keys().copied().collect();
|
||||||
|
let arr: Vec<String> = users.iter().map(|&id| brief(s, id)).collect();
|
||||||
|
Ok(obj(&[("users", format!("[{}]", arr.join(",")))]))
|
||||||
|
}
|
||||||
|
"get" => {
|
||||||
|
let uid = resolve(s, params).ok_or_else(|| RpcError::not_found("no such user"))?;
|
||||||
|
Ok(full(s, uid))
|
||||||
|
}
|
||||||
|
"kill" => {
|
||||||
|
let uid = resolve(s, params).ok_or_else(|| RpcError::not_found("no such user"))?;
|
||||||
|
let reason = json::get_str(params, "reason").unwrap_or_else(|| "Killed via RPC".into());
|
||||||
|
s.remove_user(uid, &reason);
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
|
"set_mode" => {
|
||||||
|
let uid = resolve(s, params).ok_or_else(|| RpcError::not_found("no such user"))?;
|
||||||
|
let modes = json::get_str(params, "modes")
|
||||||
|
.ok_or_else(|| RpcError::invalid_params("missing 'modes'"))?;
|
||||||
|
svs_set_user_modes(s, uid, &modes);
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
|
"set_vhost" => {
|
||||||
|
let uid = resolve(s, params).ok_or_else(|| RpcError::not_found("no such user"))?;
|
||||||
|
let host = json::get_str(params, "vhost")
|
||||||
|
.or_else(|| json::get_str(params, "host"))
|
||||||
|
.ok_or_else(|| RpcError::invalid_params("missing 'vhost'"))?;
|
||||||
|
s.change_host_ident(uid, None, Some(&host));
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
|
"set_nick" => {
|
||||||
|
let uid = resolve(s, params).ok_or_else(|| RpcError::not_found("no such user"))?;
|
||||||
|
let newnick = json::get_str(params, "newnick")
|
||||||
|
.ok_or_else(|| RpcError::invalid_params("missing 'newnick'"))?;
|
||||||
|
s.set_nick(uid, &newnick);
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
|
"set_oper" => {
|
||||||
|
let uid = resolve(s, params).ok_or_else(|| RpcError::not_found("no such user"))?;
|
||||||
|
let oper = json::get_str(params, "oper").or_else(|| json::get_str(params, "type"));
|
||||||
|
match oper {
|
||||||
|
Some(name) if !name.is_empty() => s.oper_up(uid),
|
||||||
|
_ => svs_set_user_modes(s, uid, "-o"), // de-oper
|
||||||
|
}
|
||||||
|
Ok(obj(&[("result", "true".into())]))
|
||||||
|
}
|
||||||
|
other => Err(RpcError::method_not_found(&format!("user.{other}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue