modules: port jsonlog (draft/json-log cap) — structured JSON tag on snotices for capable opers
This commit is contained in:
parent
06afd1e59d
commit
100e33c76b
4 changed files with 129 additions and 3 deletions
107
src/modules/jsonlog.rs
Normal file
107
src/modules/jsonlog.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
//! jsonlog — the `draft/json-log` capability. reverse's own module. When an oper
|
||||||
|
//! negotiates `CAP REQ draft/json-log`, every server notice they receive carries a
|
||||||
|
//! structured JSON object (timestamp, level, subsystem, msg, …) as an IRCv3 message
|
||||||
|
//! **tag** — the human-readable text stays in the NOTICE, the machine-readable copy
|
||||||
|
//! rides alongside. Companion to the RPC `log.*` methods, which expose the same data
|
||||||
|
//! over HTTP. Dispatched straight from `Server::snotice`; the tag build lives here.
|
||||||
|
//!
|
||||||
|
//! Behaviour reference: reverse's InspIRCd `m_jsonrpclog`. Original native Rust.
|
||||||
|
|
||||||
|
use crate::modules::rpc::json::{obj, qstr};
|
||||||
|
use crate::server::{iso_time, now, Server};
|
||||||
|
|
||||||
|
/// Strip mIRC/IRC formatting control codes so the `msg` field is clean plaintext:
|
||||||
|
/// bold/reset/mono/reverse/italic/strike/underline, colour (`\x03 fg[,bg]`) and hex
|
||||||
|
/// colour (`\x04 rrggbb[,rrggbb]`). The raw NOTICE keeps its formatting.
|
||||||
|
fn strip_formatting(input: &str) -> String {
|
||||||
|
let b: Vec<char> = input.chars().collect();
|
||||||
|
let mut out = String::with_capacity(input.len());
|
||||||
|
let mut i = 0;
|
||||||
|
while i < b.len() {
|
||||||
|
let c = b[i];
|
||||||
|
match c {
|
||||||
|
'\u{02}' | '\u{0F}' | '\u{11}' | '\u{16}' | '\u{1D}' | '\u{1E}' | '\u{1F}' => {}
|
||||||
|
'\u{03}' => i = skip_run(&b, i, 2, |c| c.is_ascii_digit()),
|
||||||
|
'\u{04}' => i = skip_run(&b, i, 6, |c| c.is_ascii_hexdigit()),
|
||||||
|
_ => out.push(c),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a colour/hex-colour introducer at `i`, skip up to `max` matching digits and
|
||||||
|
/// an optional `,` + up to `max` more. Returns the index of the last consumed char.
|
||||||
|
fn skip_run(b: &[char], mut i: usize, max: usize, ok: impl Fn(char) -> bool) -> usize {
|
||||||
|
let mut n = 0;
|
||||||
|
while i + 1 < b.len() && ok(b[i + 1]) && n < max {
|
||||||
|
i += 1;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
if n > 0 && i + 2 < b.len() && b[i + 1] == ',' && ok(b[i + 2]) {
|
||||||
|
i += 1; // the comma
|
||||||
|
n = 0;
|
||||||
|
while i + 1 < b.len() && ok(b[i + 1]) && n < max {
|
||||||
|
i += 1;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IRCv3 message-tag value escape (space→`\s`, `;`→`\:`, `\`→`\\`, CR/LF). Required
|
||||||
|
/// because the JSON is full of spaces and would otherwise split the wire line apart.
|
||||||
|
fn escape_tag(v: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(v.len());
|
||||||
|
for c in v.chars() {
|
||||||
|
match c {
|
||||||
|
';' => out.push_str("\\:"),
|
||||||
|
' ' => out.push_str("\\s"),
|
||||||
|
'\\' => out.push_str("\\\\"),
|
||||||
|
'\r' => out.push_str("\\r"),
|
||||||
|
'\n' => out.push_str("\\n"),
|
||||||
|
c => out.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The escaped `draft/json-log` tag *value* for a server notice `msg`. Place after
|
||||||
|
/// `draft/json-log=` in the tag block. echoIRCd snotices are untyped, so `subsystem`
|
||||||
|
/// / `event_id` are derived from the leading word and `snomask` is the generic `s`.
|
||||||
|
pub fn tag_value(s: &Server, msg: &str) -> String {
|
||||||
|
let head = msg
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("general")
|
||||||
|
.trim_end_matches(':');
|
||||||
|
let subsystem = head.to_ascii_lowercase();
|
||||||
|
let event_id = head.to_ascii_uppercase();
|
||||||
|
let json = obj(&[
|
||||||
|
("timestamp", qstr(&iso_time(now()))),
|
||||||
|
("level", qstr("info")),
|
||||||
|
("subsystem", qstr(&subsystem)),
|
||||||
|
("event_id", qstr(&event_id)),
|
||||||
|
("log_source", qstr(&s.name)),
|
||||||
|
("msg", qstr(&strip_formatting(msg))),
|
||||||
|
("snomask", qstr("s")),
|
||||||
|
]);
|
||||||
|
escape_tag(&json)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_control_codes() {
|
||||||
|
assert_eq!(strip_formatting("\u{02}bold\u{0F} x"), "bold x");
|
||||||
|
assert_eq!(strip_formatting("\u{03}04red\u{03} y"), "red y");
|
||||||
|
assert_eq!(strip_formatting("\u{03}04,08two z"), "two z");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escapes_tag_value() {
|
||||||
|
assert_eq!(escape_tag("a b;c\\d"), "a\\sb\\:c\\\\d");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ pub mod filter;
|
||||||
pub mod flood;
|
pub mod flood;
|
||||||
pub mod hashident;
|
pub mod hashident;
|
||||||
pub mod hidewhois;
|
pub mod hidewhois;
|
||||||
|
pub mod jsonlog;
|
||||||
pub mod jwt;
|
pub mod jwt;
|
||||||
pub mod markread;
|
pub mod markread;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
|
|
||||||
|
|
@ -619,13 +619,27 @@ impl Server {
|
||||||
.filter(|(_, u)| u.flags.oper && u.flags.snomask)
|
.filter(|(_, u)| u.flags.oper && u.flags.snomask)
|
||||||
.map(|(&u, _)| u)
|
.map(|(&u, _)| u)
|
||||||
.collect();
|
.collect();
|
||||||
|
// draft/json-log: the structured tag value is the same for every recipient
|
||||||
|
let jval = crate::modules::jsonlog::tag_value(self, msg);
|
||||||
for o in opers {
|
for o in opers {
|
||||||
let nick = self
|
let (nick, json_cap, time_cap) = self
|
||||||
.users
|
.users
|
||||||
.get(&o)
|
.get(&o)
|
||||||
.map(|u| u.nick.clone())
|
.map(|u| (u.nick.clone(), u.caps.json_log, u.caps.server_time))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
self.send(o, format!(":{} NOTICE {nick} :*** {msg}", self.name));
|
let base = format!(":{} NOTICE {nick} :*** {msg}", self.name);
|
||||||
|
if json_cap {
|
||||||
|
// build one tag block (server-time too, if negotiated) and emit raw,
|
||||||
|
// so we don't collide with the auto server-time tagging in `send`
|
||||||
|
let mut tags = String::new();
|
||||||
|
if time_cap {
|
||||||
|
tags.push_str(&format!("time={};", iso_time(now())));
|
||||||
|
}
|
||||||
|
tags.push_str(&format!("draft/json-log={jval}"));
|
||||||
|
self.emit_to(o, format!("@{tags} {base}"));
|
||||||
|
} else {
|
||||||
|
self.send(o, base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ pub const SUPPORTED_CAPS: &[&str] = &[
|
||||||
"draft/metadata-2",
|
"draft/metadata-2",
|
||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
"draft/account-registration",
|
"draft/account-registration",
|
||||||
|
"draft/json-log",
|
||||||
"cap-notify",
|
"cap-notify",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -145,6 +146,7 @@ pub struct Caps {
|
||||||
pub metadata: bool, // draft/metadata-2 — wants metadata + change notices
|
pub metadata: bool, // draft/metadata-2 — wants metadata + change notices
|
||||||
pub multiline: bool, // draft/multiline — may send multiline message batches
|
pub multiline: bool, // draft/multiline — may send multiline message batches
|
||||||
pub acct_registration: bool, // draft/account-registration — REGISTER/VERIFY understood
|
pub acct_registration: bool, // draft/account-registration — REGISTER/VERIFY understood
|
||||||
|
pub json_log: bool, // draft/json-log — structured JSON tag on server notices
|
||||||
pub cap_notify: bool,
|
pub cap_notify: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +206,7 @@ impl Caps {
|
||||||
"draft/metadata-2" => self.metadata,
|
"draft/metadata-2" => self.metadata,
|
||||||
"draft/multiline" => self.multiline,
|
"draft/multiline" => self.multiline,
|
||||||
"draft/account-registration" => self.acct_registration,
|
"draft/account-registration" => self.acct_registration,
|
||||||
|
"draft/json-log" => self.json_log,
|
||||||
"cap-notify" => self.cap_notify,
|
"cap-notify" => self.cap_notify,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
|
@ -235,6 +238,7 @@ impl Caps {
|
||||||
"draft/metadata-2" => &mut self.metadata,
|
"draft/metadata-2" => &mut self.metadata,
|
||||||
"draft/multiline" => &mut self.multiline,
|
"draft/multiline" => &mut self.multiline,
|
||||||
"draft/account-registration" => &mut self.acct_registration,
|
"draft/account-registration" => &mut self.acct_registration,
|
||||||
|
"draft/json-log" => &mut self.json_log,
|
||||||
"cap-notify" => &mut self.cap_notify,
|
"cap-notify" => &mut self.cap_notify,
|
||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue