tag service replies with +draft/reply and +draft/channel-context
All checks were successful
CI / check (push) Successful in 5m36s
All checks were successful
CI / check (push) Successful in 5m36s
This commit is contained in:
parent
29eefaafe3
commit
9ccc681a62
5 changed files with 496 additions and 337 deletions
|
|
@ -19,7 +19,10 @@ pub enum NetEvent {
|
||||||
// idle half of a routed WHOIS (`WHOIS x x` / mIRC's double-click). Must be
|
// idle half of a routed WHOIS (`WHOIS x x` / mIRC's double-click). Must be
|
||||||
// answered or that WHOIS produces no output at all.
|
// answered or that WHOIS produces no output at all.
|
||||||
Idle { requester: String, target: String },
|
Idle { requester: String, target: String },
|
||||||
Privmsg { from: String, to: String, text: String },
|
// `msgid` is the uplink's IRCv3 message-id tag on the incoming line, if any —
|
||||||
|
// threaded back onto our replies as `+draft/reply` so a client can group them
|
||||||
|
// under the command that triggered them.
|
||||||
|
Privmsg { from: String, to: String, text: String, msgid: Option<String> },
|
||||||
UserConnect { uid: String, nick: String, host: String, ip: String },
|
UserConnect { uid: String, nick: String, host: String, ip: String },
|
||||||
// The rest of a user's UID that UserConnect omits — ident, real host, and
|
// The rest of a user's UID that UserConnect omits — ident, real host, and
|
||||||
// real name (gecos). The ircd sends them in the same UID line; extban
|
// real name (gecos). The ircd sends them in the same UID line; extban
|
||||||
|
|
@ -132,6 +135,10 @@ pub enum NetAction {
|
||||||
IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String },
|
IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String },
|
||||||
Privmsg { from: String, to: String, text: String },
|
Privmsg { from: String, to: String, text: String },
|
||||||
Notice { from: String, to: String, text: String },
|
Notice { from: String, to: String, text: String },
|
||||||
|
// Wrap another action with IRCv3 client-only message tags (keys begin with
|
||||||
|
// `+`, e.g. `+draft/reply`, `+draft/channel-context`). The serializer prepends
|
||||||
|
// them to each emitted line, or drops them if the uplink can't relay client tags.
|
||||||
|
Tagged { tags: Vec<(String, String)>, inner: Box<NetAction> },
|
||||||
// An IRCv3 standard reply (FAIL/WARN/NOTE) from a service to a client. Not
|
// An IRCv3 standard reply (FAIL/WARN/NOTE) from a service to a client. Not
|
||||||
// routable over s2s directly, so it's encapsulated for the target's server
|
// routable over s2s directly, so it's encapsulated for the target's server
|
||||||
// (m_services_stdrpl) to re-emit locally, cap-gated. `command`/`from` may be
|
// (m_services_stdrpl) to re-emit locally, cap-gated. `command`/`from` may be
|
||||||
|
|
@ -591,6 +598,18 @@ macro_rules! plural {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl NetAction {
|
||||||
|
// The action inside a [`NetAction::Tagged`] wrapper, or this action itself.
|
||||||
|
// Lets an engine transform that rewrites a Notice/Privmsg/StandardReply reach
|
||||||
|
// the real action through any `+draft/…` reply tag it carries.
|
||||||
|
pub fn inner_mut(&mut self) -> &mut NetAction {
|
||||||
|
match self {
|
||||||
|
NetAction::Tagged { inner, .. } => inner,
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The intent sink a service writes to. A service never mutates the network or
|
/// The intent sink a service writes to. A service never mutates the network or
|
||||||
/// the store itself; it pushes normalized actions (via [`ServiceCtx::notice`]
|
/// the store itself; it pushes normalized actions (via [`ServiceCtx::notice`]
|
||||||
/// and friends) that the engine drains and applies.
|
/// and friends) that the engine drains and applies.
|
||||||
|
|
@ -614,6 +633,11 @@ pub struct ServiceCtx {
|
||||||
// preference, else the network default). Set by the engine before dispatch;
|
// preference, else the network default). Set by the engine before dispatch;
|
||||||
// empty means English. Read via [`ServiceCtx::lang`] / the `t!` macro.
|
// empty means English. Read via [`ServiceCtx::lang`] / the `t!` macro.
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
// The uid that sent the command being handled, and the IRCv3 msgid of that
|
||||||
|
// line (if the uplink tagged it). A reply going back to `sender_uid` is
|
||||||
|
// threaded with `+draft/reply=<reply_to>`; replies to anyone else are not.
|
||||||
|
pub sender_uid: String,
|
||||||
|
pub reply_to: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A login-attempt outcome a module hands back for the auth audit feed.
|
// A login-attempt outcome a module hands back for the auth audit feed.
|
||||||
|
|
@ -642,7 +666,32 @@ impl ServiceCtx {
|
||||||
// unchanged). Interpolated messages must use the `t!` macro so their template
|
// unchanged). Interpolated messages must use the `t!` macro so their template
|
||||||
// — not the already-filled-in text — is the lookup key.
|
// — not the already-filled-in text — is the lookup key.
|
||||||
let text = render(self.lang(), &text.into(), &[]);
|
let text = render(self.lang(), &text.into(), &[]);
|
||||||
self.actions.push(NetAction::Notice { from: from.to_string(), to: to.to_string(), text });
|
let inner = NetAction::Notice { from: from.to_string(), to: to.to_string(), text };
|
||||||
|
self.push_tagged(inner, to, Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like [`notice`], but with extra IRCv3 client tags attached (e.g.
|
||||||
|
// `+draft/channel-context=#chan`). The reply-thread tag, if any, is merged in.
|
||||||
|
pub fn notice_tagged(&mut self, from: &str, to: &str, text: impl Into<String>, tags: Vec<(String, String)>) {
|
||||||
|
let text = render(self.lang(), &text.into(), &[]);
|
||||||
|
let inner = NetAction::Notice { from: from.to_string(), to: to.to_string(), text };
|
||||||
|
self.push_tagged(inner, to, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue an action, wrapping it in [`NetAction::Tagged`] when there are tags to
|
||||||
|
// carry. A reply to the command's own sender also gets `+draft/reply` so the
|
||||||
|
// client can thread it under the triggering line.
|
||||||
|
fn push_tagged(&mut self, inner: NetAction, to: &str, mut tags: Vec<(String, String)>) {
|
||||||
|
if to == self.sender_uid {
|
||||||
|
if let Some(id) = &self.reply_to {
|
||||||
|
tags.push(("+draft/reply".to_string(), id.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.actions.push(if tags.is_empty() {
|
||||||
|
inner
|
||||||
|
} else {
|
||||||
|
NetAction::Tagged { tags, inner: Box::new(inner) }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// IRCv3 standard replies from a service (`from` uid) to a client (`to` uid).
|
// IRCv3 standard replies from a service (`from` uid) to a client (`to` uid).
|
||||||
|
|
@ -664,14 +713,15 @@ impl ServiceCtx {
|
||||||
|
|
||||||
fn standard_reply(&mut self, kind: ReplyKind, from: &str, to: &str, command: &str, code: &str, text: impl Into<String>) {
|
fn standard_reply(&mut self, kind: ReplyKind, from: &str, to: &str, command: &str, code: &str, text: impl Into<String>) {
|
||||||
let text = render(self.lang(), &text.into(), &[]); // auto-localize static replies (see `notice`)
|
let text = render(self.lang(), &text.into(), &[]); // auto-localize static replies (see `notice`)
|
||||||
self.actions.push(NetAction::StandardReply {
|
let inner = NetAction::StandardReply {
|
||||||
kind,
|
kind,
|
||||||
from: from.to_string(),
|
from: from.to_string(),
|
||||||
to: to.to_string(),
|
to: to.to_string(),
|
||||||
command: command.to_string(),
|
command: command.to_string(),
|
||||||
code: code.to_string(),
|
code: code.to_string(),
|
||||||
text,
|
text,
|
||||||
});
|
};
|
||||||
|
self.push_tagged(inner, to, Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record a stat counter bump (namespaced, e.g. "chanserv.register"). Folded
|
// Record a stat counter bump (namespaced, e.g. "chanserv.register"). Folded
|
||||||
|
|
@ -2499,8 +2549,14 @@ pub fn notify_or_memo(ctx: &mut ServiceCtx, net: &dyn NetView, store: &mut dyn S
|
||||||
if online.is_empty() {
|
if online.is_empty() {
|
||||||
let _ = store.memo_send(target, by, &text, false);
|
let _ = store.memo_send(target, by, &text, false);
|
||||||
} else {
|
} else {
|
||||||
|
// Tag the notice with the channel it's about (`+draft/channel-context`) so a
|
||||||
|
// client can file this "your access changed" line under that channel.
|
||||||
|
let tags: Vec<(String, String)> = args.iter()
|
||||||
|
.find(|(k, _)| *k == "chan")
|
||||||
|
.map(|(_, chan)| vec![("+draft/channel-context".to_string(), chan.clone())])
|
||||||
|
.unwrap_or_default();
|
||||||
for uid in online {
|
for uid in online {
|
||||||
ctx.notice(service, &uid, text.clone());
|
ctx.notice_tagged(service, &uid, text.clone(), tags.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,15 @@ pub struct InspIrcd {
|
||||||
// consults this so a param-taking mode outside the static fallback table (e.g.
|
// consults this so a param-taking mode outside the static fallback table (e.g.
|
||||||
// m_chanfilter's +g) doesn't desync the param cursor and mispair status/key args.
|
// m_chanfilter's +g) doesn't desync the param cursor and mispair status/key args.
|
||||||
chanmodes: std::collections::HashMap<char, echo_api::ChanModeCap>,
|
chanmodes: std::collections::HashMap<char, echo_api::ChanModeCap>,
|
||||||
|
// Whether the uplink loaded m_ircv3_ctctags (learned from CAPAB MODULES). Only
|
||||||
|
// then can it relay the client-only `+` tags we attach to replies; otherwise we
|
||||||
|
// emit the reply untagged.
|
||||||
|
supports_ctctags: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InspIrcd {
|
impl InspIrcd {
|
||||||
pub fn new(name: String, description: String, sid: String, password: String, protocol: u32, ts: u64, service_modes: String) -> Self {
|
pub fn new(name: String, description: String, sid: String, password: String, protocol: u32, ts: u64, service_modes: String) -> Self {
|
||||||
Self { sid, name, description, password, protocol, ts, service_modes, membid: 0, chanmodes: std::collections::HashMap::new() }
|
Self { sid, name, description, password, protocol, ts, service_modes, membid: 0, chanmodes: std::collections::HashMap::new(), supports_ctctags: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sourced(&self, cmd: String) -> String {
|
fn sourced(&self, cmd: String) -> String {
|
||||||
|
|
@ -44,12 +48,14 @@ impl Protocol for InspIrcd {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(&mut self, line: &str) -> Vec<NetEvent> {
|
fn parse(&mut self, line: &str) -> Vec<NetEvent> {
|
||||||
// Strip an optional IRCv3 message-tag prefix (@k=v;… ) — insp tags PRIVMSGs
|
// Split off an optional IRCv3 message-tag prefix (@k=v;… ) — insp tags
|
||||||
// with time/msgid, and it sits before the :source.
|
// PRIVMSGs with time/msgid, and it sits before the :source. We keep the
|
||||||
let line = match line.strip_prefix('@') {
|
// msgid to thread onto our replies (+draft/reply); the rest is parsed as-is.
|
||||||
Some(rest) => rest.split_once(' ').map(|x| x.1).unwrap_or(""),
|
let (tag_prefix, line) = match line.strip_prefix('@') {
|
||||||
None => line,
|
Some(rest) => rest.split_once(' ').unwrap_or((rest, "")),
|
||||||
|
None => ("", line),
|
||||||
};
|
};
|
||||||
|
let msgid = tag_prefix.split(';').find_map(|kv| kv.strip_prefix("msgid=")).map(str::to_string);
|
||||||
let (source, rest) = match line.strip_prefix(':') {
|
let (source, rest) = match line.strip_prefix(':') {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
let mut it = s.splitn(2, ' ');
|
let mut it = s.splitn(2, ' ');
|
||||||
|
|
@ -119,6 +125,7 @@ impl Protocol for InspIrcd {
|
||||||
from: source.unwrap_or_default(),
|
from: source.unwrap_or_default(),
|
||||||
to,
|
to,
|
||||||
text: trailing(rest),
|
text: trailing(rest),
|
||||||
|
msgid,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
// UID <uuid> <nickts> <nick> … — track who's online so services can
|
// UID <uuid> <nickts> <nick> … — track who's online so services can
|
||||||
|
|
@ -374,6 +381,9 @@ impl Protocol for InspIrcd {
|
||||||
// which echo verifies its dependencies against.
|
// which echo verifies its dependencies against.
|
||||||
Some(s) if s.eq_ignore_ascii_case("MODULES") || s.eq_ignore_ascii_case("MODSUPPORT") => {
|
Some(s) if s.eq_ignore_ascii_case("MODULES") || s.eq_ignore_ascii_case("MODSUPPORT") => {
|
||||||
let modules: Vec<_> = trailing(rest).split_whitespace().map(module_name).collect();
|
let modules: Vec<_> = trailing(rest).split_whitespace().map(module_name).collect();
|
||||||
|
if modules.iter().any(|m| m == "ircv3_ctctags") {
|
||||||
|
self.supports_ctctags = true;
|
||||||
|
}
|
||||||
if modules.is_empty() {
|
if modules.is_empty() {
|
||||||
vec![]
|
vec![]
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -425,6 +435,20 @@ impl Protocol for InspIrcd {
|
||||||
NetAction::Notice { from, to, text } => {
|
NetAction::Notice { from, to, text } => {
|
||||||
split_body(text).into_iter().map(|chunk| format!(":{} NOTICE {} :{}", from, to, chunk)).collect()
|
split_body(text).into_iter().map(|chunk| format!(":{} NOTICE {} :{}", from, to, chunk)).collect()
|
||||||
}
|
}
|
||||||
|
// Prepend the client tags to each line of the wrapped action — but only
|
||||||
|
// if the uplink can relay them; otherwise emit the inner action untagged.
|
||||||
|
NetAction::Tagged { tags, inner } => {
|
||||||
|
let inner_lines = self.serialize(inner);
|
||||||
|
if !self.supports_ctctags || tags.is_empty() {
|
||||||
|
inner_lines
|
||||||
|
} else {
|
||||||
|
let prefix = tags.iter()
|
||||||
|
.map(|(k, v)| format!("{}={}", k, escape_tag_value(v)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";");
|
||||||
|
inner_lines.into_iter().map(|l| format!("@{} {}", prefix, l)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
// Standard reply: encapsulate for the target's server to re-emit locally
|
// Standard reply: encapsulate for the target's server to re-emit locally
|
||||||
// (FAIL/WARN/NOTE aren't s2s-routable). ENCAP * so only the server the
|
// (FAIL/WARN/NOTE aren't s2s-routable). ENCAP * so only the server the
|
||||||
// target is local to acts. "*" = no source service / no command.
|
// target is local to acts. "*" = no source service / no command.
|
||||||
|
|
@ -683,6 +707,23 @@ fn trailing(rest: &str) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape a value for an IRCv3 message tag (the mandatory five substitutions), so
|
||||||
|
// a channel name or msgid carrying a space/semicolon can't break the tag list.
|
||||||
|
fn escape_tag_value(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"),
|
||||||
|
_ => out.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
// A free-text reason param (PART/KICK/QUIT), which the ircd always sends as a
|
// A free-text reason param (PART/KICK/QUIT), which the ircd always sends as a
|
||||||
// `:`-prefixed trailing. Unlike `trailing()`, a MISSING reason yields "" rather
|
// `:`-prefixed trailing. Unlike `trailing()`, a MISSING reason yields "" rather
|
||||||
// than the last preceding word — so `KICK #c target` (no reason) doesn't record
|
// than the last preceding word — so `KICK #c target` (no reason) doesn't record
|
||||||
|
|
@ -844,7 +885,7 @@ mod tests {
|
||||||
fn squery_is_handled_like_privmsg() {
|
fn squery_is_handled_like_privmsg() {
|
||||||
let mut p = proto();
|
let mut p = proto();
|
||||||
assert!(matches!(p.parse(":0IRAAAAAB SQUERY 42SAAAAAB :HELP").as_slice(),
|
assert!(matches!(p.parse(":0IRAAAAAB SQUERY 42SAAAAAB :HELP").as_slice(),
|
||||||
[NetEvent::Privmsg { from, to, text }] if from == "0IRAAAAAB" && to == "42SAAAAAB" && text == "HELP"));
|
[NetEvent::Privmsg { from, to, text, .. }] if from == "0IRAAAAAB" && to == "42SAAAAAB" && text == "HELP"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAPAB CAPABILITIES surfaces CASEMAPPING (echo verifies it's ascii).
|
// CAPAB CAPABILITIES surfaces CASEMAPPING (echo verifies it's ascii).
|
||||||
|
|
@ -993,6 +1034,48 @@ mod tests {
|
||||||
assert_eq!(without, vec![":42SAAAAAA REDACT alice xyz".to_string()], "no trailing when reason is empty");
|
assert_eq!(without, vec![":42SAAAAAA REDACT alice xyz".to_string()], "no trailing when reason is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A tagged reply carries its `+draft/…` tags only after the uplink advertises
|
||||||
|
// m_ircv3_ctctags; before that (or with no tags) it's emitted plain.
|
||||||
|
#[test]
|
||||||
|
fn serializes_tagged_notice_only_when_ctctags_loaded() {
|
||||||
|
let tagged = || NetAction::Tagged {
|
||||||
|
tags: vec![("+draft/reply".to_string(), "abc123".to_string())],
|
||||||
|
inner: Box::new(NetAction::Notice { from: "42SAAAAAA".into(), to: "0IRAAAAAB".into(), text: "hi".into() }),
|
||||||
|
};
|
||||||
|
// No ctctags module -> tags dropped, plain notice.
|
||||||
|
assert_eq!(proto().serialize(&tagged()), vec![":42SAAAAAA NOTICE 0IRAAAAAB :hi".to_string()]);
|
||||||
|
// After CAPAB MODULES lists it -> the tag is prepended.
|
||||||
|
let mut p = proto();
|
||||||
|
p.parse("CAPAB MODULES :account ircv3_ctctags");
|
||||||
|
assert_eq!(p.serialize(&tagged()), vec!["@+draft/reply=abc123 :42SAAAAAA NOTICE 0IRAAAAAB :hi".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag values with reserved characters are escaped, and every split line of a
|
||||||
|
// multi-line inner action carries the tags.
|
||||||
|
#[test]
|
||||||
|
fn tagged_values_escaped_and_applied_per_line() {
|
||||||
|
let mut p = proto();
|
||||||
|
p.parse("CAPAB MODULES :ircv3_ctctags");
|
||||||
|
let out = p.serialize(&NetAction::Tagged {
|
||||||
|
tags: vec![("+draft/channel-context".to_string(), "#a b".to_string())],
|
||||||
|
inner: Box::new(NetAction::Notice { from: "42SAAAAAA".into(), to: "0IRAAAAAB".into(), text: "word ".repeat(200).trim().to_string() }),
|
||||||
|
});
|
||||||
|
assert!(out.len() >= 2, "long notice split into multiple lines");
|
||||||
|
assert!(out.iter().all(|l| l.starts_with("@+draft/channel-context=#a\\sb :")), "every line tagged + escaped: {out:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// insp tags an incoming PRIVMSG with a msgid; we capture it so the reply can
|
||||||
|
// thread against it.
|
||||||
|
#[test]
|
||||||
|
fn parses_msgid_from_privmsg_tags() {
|
||||||
|
let ev = proto().parse("@msgid=xyz789;time=2026-01-01T00:00:00Z :0IRAAAAAB PRIVMSG 42SAAAAAB :HELP");
|
||||||
|
assert!(matches!(ev.as_slice(),
|
||||||
|
[NetEvent::Privmsg { msgid: Some(m), text, .. }] if m == "xyz789" && text == "HELP"));
|
||||||
|
// No tag prefix -> no msgid.
|
||||||
|
let ev = proto().parse(":0IRAAAAAB PRIVMSG 42SAAAAAB :HELP");
|
||||||
|
assert!(matches!(ev.as_slice(), [NetEvent::Privmsg { msgid: None, .. }]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serializes_svspart() {
|
fn serializes_svspart() {
|
||||||
let lines = proto().serialize(&NetAction::ForcePart { uid: "0IRAAAAAB".into(), channel: "#chan".into(), reason: "bye".into() });
|
let lines = proto().serialize(&NetAction::ForcePart { uid: "0IRAAAAAB".into(), channel: "#chan".into(), reason: "bye".into() });
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,14 @@ impl Engine {
|
||||||
|
|
||||||
// Route a PRIVMSG addressed to a service (by uid or nick) into that service,
|
// Route a PRIVMSG addressed to a service (by uid or nick) into that service,
|
||||||
// handing it the sender's resolved nick, login state, and the account store.
|
// handing it the sender's resolved nick, login state, and the account store.
|
||||||
pub(crate) fn dispatch(&mut self, from: &str, to: &str, text: &str) -> Vec<NetAction> {
|
pub(crate) fn dispatch(&mut self, from: &str, to: &str, text: &str, msgid: Option<String>) -> Vec<NetAction> {
|
||||||
let nick = self.network.nick_of(from).unwrap_or(from).to_string();
|
let nick = self.network.nick_of(from).unwrap_or(from).to_string();
|
||||||
let account = self.network.account_of(from).map(str::to_string);
|
let account = self.network.account_of(from).map(str::to_string);
|
||||||
let privs = account.as_deref().map(|a| self.oper_privs(a)).unwrap_or_default();
|
let privs = account.as_deref().map(|a| self.oper_privs(a)).unwrap_or_default();
|
||||||
// Render this command's replies in the sender's language: their account
|
// Render this command's replies in the sender's language: their account
|
||||||
// preference if set, otherwise the network default.
|
// preference if set, otherwise the network default.
|
||||||
let lang = account.as_deref().and_then(|a| self.db.language_of(a)).unwrap_or_else(|| self.db.default_language());
|
let lang = account.as_deref().and_then(|a| self.db.language_of(a)).unwrap_or_else(|| self.db.default_language());
|
||||||
let mut ctx = ServiceCtx { lang, ..Default::default() };
|
let mut ctx = ServiceCtx { lang, sender_uid: from.to_string(), reply_to: msgid, ..Default::default() };
|
||||||
// Mark the log so we can tell exactly which events this command appends.
|
// Mark the log so we can tell exactly which events this command appends.
|
||||||
let audit_mark = self.db.log_len();
|
let audit_mark = self.db.log_len();
|
||||||
let sender = Sender { uid: from, nick: &nick, account: account.as_deref(), privs };
|
let sender = Sender { uid: from, nick: &nick, account: account.as_deref(), privs };
|
||||||
|
|
@ -194,7 +194,7 @@ impl Engine {
|
||||||
fn apply_msg_style(&self, out: &mut [NetAction]) {
|
fn apply_msg_style(&self, out: &mut [NetAction]) {
|
||||||
let mut named: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
|
let mut named: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
|
||||||
for a in out.iter_mut() {
|
for a in out.iter_mut() {
|
||||||
let NetAction::Notice { from, to, text } = a else { continue };
|
let NetAction::Notice { from, to, text } = a.inner_mut() else { continue };
|
||||||
let Some(account) = self.network.account_of(to) else { continue };
|
let Some(account) = self.network.account_of(to) else { continue };
|
||||||
if !self.db.account_wants_snotice(account) {
|
if !self.db.account_wants_snotice(account) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -230,8 +230,9 @@ impl Engine {
|
||||||
self.route_to(&dsuid, sender, &args, ctx);
|
self.route_to(&dsuid, sender, &args, ctx);
|
||||||
// Turn DiceServ's private notices into the bot speaking to the channel.
|
// Turn DiceServ's private notices into the bot speaking to the channel.
|
||||||
for a in ctx.actions[mark..].iter_mut() {
|
for a in ctx.actions[mark..].iter_mut() {
|
||||||
if let NetAction::Notice { text, .. } = a {
|
let slot = a.inner_mut();
|
||||||
*a = NetAction::Privmsg { from: botuid.clone(), to: chan.to_string(), text: std::mem::take(text) };
|
if let NetAction::Notice { text, .. } = slot {
|
||||||
|
*slot = NetAction::Privmsg { from: botuid.clone(), to: chan.to_string(), text: std::mem::take(text) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -265,11 +266,12 @@ impl Engine {
|
||||||
// Fantasy is public: the bot speaks ChanServ's text replies into the channel,
|
// Fantasy is public: the bot speaks ChanServ's text replies into the channel,
|
||||||
// and its actions (modes, kicks) are re-sourced from the bot.
|
// and its actions (modes, kicks) are re-sourced from the bot.
|
||||||
for a in ctx.actions[mark..].iter_mut() {
|
for a in ctx.actions[mark..].iter_mut() {
|
||||||
match a {
|
let slot = a.inner_mut();
|
||||||
|
match slot {
|
||||||
NetAction::Notice { text, .. } => {
|
NetAction::Notice { text, .. } => {
|
||||||
*a = NetAction::Privmsg { from: botuid.clone(), to: chan.to_string(), text: std::mem::take(text) };
|
*slot = NetAction::Privmsg { from: botuid.clone(), to: chan.to_string(), text: std::mem::take(text) };
|
||||||
}
|
}
|
||||||
_ => resource_action(a, &csuid, &botuid),
|
_ => resource_action(slot, &csuid, &botuid),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1638,7 +1638,7 @@ impl Engine {
|
||||||
}
|
}
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
NetEvent::Privmsg { from, to, text } => self.dispatch(&from, &to, &text),
|
NetEvent::Privmsg { from, to, text, msgid } => self.dispatch(&from, &to, &text, msgid),
|
||||||
NetEvent::AccountRequest { reqid, origin, kind, account, p2, p3 } => {
|
NetEvent::AccountRequest { reqid, origin, kind, account, p2, p3 } => {
|
||||||
self.account_request(reqid, origin, kind, account, p2, p3)
|
self.account_request(reqid, origin, kind, account, p2, p3)
|
||||||
}
|
}
|
||||||
|
|
@ -1688,13 +1688,14 @@ impl Engine {
|
||||||
// (or `*** text` when there's no command), sourced from the same service.
|
// (or `*** text` when there's no command), sourced from the same service.
|
||||||
fn degrade_standard_replies(&self, actions: &mut [NetAction]) {
|
fn degrade_standard_replies(&self, actions: &mut [NetAction]) {
|
||||||
for a in actions.iter_mut() {
|
for a in actions.iter_mut() {
|
||||||
if let NetAction::StandardReply { from, to, command, text, .. } = a {
|
let slot = a.inner_mut();
|
||||||
|
if let NetAction::StandardReply { from, to, command, text, .. } = slot {
|
||||||
let body = if command.is_empty() || command == "*" {
|
let body = if command.is_empty() || command == "*" {
|
||||||
format!("*** {text}")
|
format!("*** {text}")
|
||||||
} else {
|
} else {
|
||||||
format!("*** {command}: {text}")
|
format!("*** {command}: {text}")
|
||||||
};
|
};
|
||||||
*a = NetAction::Notice { from: std::mem::take(from), to: std::mem::take(to), text: body };
|
*slot = NetAction::Notice { from: std::mem::take(from), to: std::mem::take(to), text: body };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2073,7 +2074,7 @@ fn audit_summary(event: &db::Event) -> Option<String> {
|
||||||
// Rewrite the source uid of an action from `old` to `new`, where the variant
|
// Rewrite the source uid of an action from `old` to `new`, where the variant
|
||||||
// carries a `from`. Used to make fantasy output appear to come from the bot.
|
// carries a `from`. Used to make fantasy output appear to come from the bot.
|
||||||
fn resource_action(a: &mut NetAction, old: &str, new: &str) {
|
fn resource_action(a: &mut NetAction, old: &str, new: &str) {
|
||||||
let from = match a {
|
let from = match a.inner_mut() {
|
||||||
NetAction::Notice { from, .. }
|
NetAction::Notice { from, .. }
|
||||||
| NetAction::Privmsg { from, .. }
|
| NetAction::Privmsg { from, .. }
|
||||||
| NetAction::ChannelMode { from, .. }
|
| NetAction::ChannelMode { from, .. }
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue