Auto-op the founder on join
Surface channel joins (FJOIN members, stripping the membid, and IJOIN) and set +o for a joining user who is identified as the channel's founder.
This commit is contained in:
parent
353aee1b54
commit
e785c9d8ac
3 changed files with 75 additions and 7 deletions
|
|
@ -186,6 +186,15 @@ impl Engine {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Auto-op the founder when they join their registered channel.
|
||||||
|
NetEvent::Join { uid, channel } => {
|
||||||
|
let founder = self.db.channel(&channel).map(|c| c.founder.clone());
|
||||||
|
let account = self.network.account_of(&uid).map(str::to_string);
|
||||||
|
match (founder, account) {
|
||||||
|
(Some(f), Some(a)) if f == a => vec![NetAction::ChannelMode { channel, modes: format!("+o {uid}") }],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
NetEvent::Quit { uid } => {
|
NetEvent::Quit { uid } => {
|
||||||
self.network.user_quit(&uid);
|
self.network.user_quit(&uid);
|
||||||
self.sasl_sessions.remove(&uid); // drop any half-finished exchange
|
self.sasl_sessions.remove(&uid); // drop any half-finished exchange
|
||||||
|
|
@ -1001,4 +1010,25 @@ mod tests {
|
||||||
// An unrelated mode change is left alone.
|
// An unrelated mode change is left alone.
|
||||||
assert!(e.handle(NetEvent::ChannelModeChange { channel: "#lk".into(), modes: "+m".into() }).is_empty());
|
assert!(e.handle(NetEvent::ChannelModeChange { channel: "#lk".into(), modes: "+m".into() }).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The founder is opped when they join their channel; others are not.
|
||||||
|
#[test]
|
||||||
|
fn founder_is_opped_on_join() {
|
||||||
|
let path = std::env::temp_dir().join("fedserv-autoop.jsonl");
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
let mut db = Db::open(&path, "42S");
|
||||||
|
db.scram_iterations = 4096;
|
||||||
|
db.register("alice", "sesame", None).unwrap();
|
||||||
|
db.register_channel("#x", "alice").unwrap();
|
||||||
|
let ns = NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 };
|
||||||
|
let mut e = Engine::new(vec![Box::new(ns)], db);
|
||||||
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into() });
|
||||||
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
||||||
|
|
||||||
|
let out = e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#x".into() });
|
||||||
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#x" && modes == "+o 000AAAAAB")), "founder opped: {out:?}");
|
||||||
|
|
||||||
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into() });
|
||||||
|
assert!(e.handle(NetEvent::Join { uid: "000AAAAAC".into(), channel: "#x".into() }).is_empty(), "non-founder not opped");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,30 @@ impl Protocol for InspIrcd {
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
},
|
},
|
||||||
// FJOIN <chan> <ts> <modes> [params] :<members> — channel create/burst.
|
// FJOIN <chan> <ts> <modes> [params] :<members> — channel create/burst.
|
||||||
"FJOIN" => match tokens.next() {
|
// Each member is "<prefixes>,<uid>"; surface a Join for each.
|
||||||
Some(chan) if !chan.is_empty() => vec![NetEvent::ChannelCreate { channel: chan.to_string() }],
|
"FJOIN" => {
|
||||||
|
let chan = tokens.next().unwrap_or("").to_string();
|
||||||
|
if chan.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
let mut out = vec![NetEvent::ChannelCreate { channel: chan.clone() }];
|
||||||
|
// Members are "<prefixes>,<uid>[:<membid>]"; take the uid.
|
||||||
|
for m in trailing(rest).split_whitespace() {
|
||||||
|
if let Some((_, member)) = m.split_once(',') {
|
||||||
|
let uid = member.split(':').next().unwrap_or("");
|
||||||
|
if !uid.is_empty() {
|
||||||
|
out.push(NetEvent::Join { uid: uid.to_string(), channel: chan.clone() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// :<uid> IJOIN <chan> — a single user joining an existing channel.
|
||||||
|
"IJOIN" => match (source.as_deref(), tokens.next()) {
|
||||||
|
(Some(uid), Some(chan)) if chan.starts_with('#') => {
|
||||||
|
vec![NetEvent::Join { uid: uid.to_string(), channel: chan.to_string() }]
|
||||||
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
},
|
},
|
||||||
// :<src> FMODE <chan> <ts> <modes> [params] — a channel mode change.
|
// :<src> FMODE <chan> <ts> <modes> [params] — a channel mode change.
|
||||||
|
|
@ -249,14 +271,13 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FJOIN (channel create/burst) surfaces as ChannelCreate for the channel.
|
// FJOIN (channel create/burst) surfaces as ChannelCreate for the channel, and
|
||||||
|
// the member's uid is taken without its :membid suffix.
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_fjoin_as_channel_create() {
|
fn parses_fjoin_as_channel_create() {
|
||||||
let ev = proto().parse(":0IR FJOIN #chan 1783845132 +tn :o,0IRAAAAAB:0");
|
let ev = proto().parse(":0IR FJOIN #chan 1783845132 +tn :o,0IRAAAAAB:0");
|
||||||
assert!(
|
assert!(matches!(ev.first(), Some(NetEvent::ChannelCreate { channel }) if channel == "#chan"), "{ev:?}");
|
||||||
matches!(ev.as_slice(), [NetEvent::ChannelCreate { channel }] if channel == "#chan"),
|
assert!(ev.iter().any(|e| matches!(e, NetEvent::Join { uid, .. } if uid == "0IRAAAAAB")), "{ev:?}");
|
||||||
"{ev:?}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A peer FMODE surfaces as a mode change; our own (sid 42S) is filtered out.
|
// A peer FMODE surfaces as a mode change; our own (sid 42S) is filtered out.
|
||||||
|
|
@ -270,4 +291,19 @@ mod tests {
|
||||||
let ev = proto().parse(":42S FMODE #chan 1783845132 -m");
|
let ev = proto().parse(":42S FMODE #chan 1783845132 -m");
|
||||||
assert!(!ev.iter().any(|e| matches!(e, NetEvent::ChannelModeChange { .. })), "own change filtered: {ev:?}");
|
assert!(!ev.iter().any(|e| matches!(e, NetEvent::ChannelModeChange { .. })), "own change filtered: {ev:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FJOIN members and IJOIN both surface as Join events.
|
||||||
|
#[test]
|
||||||
|
fn parses_joins() {
|
||||||
|
let ev = proto().parse(":0IR FJOIN #chan 1783845132 +nt :o,0IRAAAAAB ,0IRAAAAAC");
|
||||||
|
assert!(matches!(ev.first(), Some(NetEvent::ChannelCreate { channel }) if channel == "#chan"));
|
||||||
|
let joins: Vec<&str> = ev.iter().filter_map(|e| match e {
|
||||||
|
NetEvent::Join { uid, .. } => Some(uid.as_str()),
|
||||||
|
_ => None,
|
||||||
|
}).collect();
|
||||||
|
assert_eq!(joins, ["0IRAAAAAB", "0IRAAAAAC"]);
|
||||||
|
|
||||||
|
let ev = proto().parse(":0IRAAAAAD IJOIN #chan");
|
||||||
|
assert!(matches!(ev.as_slice(), [NetEvent::Join { uid, channel }] if uid == "0IRAAAAAD" && channel == "#chan"), "{ev:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ pub enum NetEvent {
|
||||||
// A channel was created or bursted (InspIRCd FJOIN). Subsequent single joins
|
// A channel was created or bursted (InspIRCd FJOIN). Subsequent single joins
|
||||||
// arrive as IJOIN and are not surfaced.
|
// arrive as IJOIN and are not surfaced.
|
||||||
ChannelCreate { channel: String },
|
ChannelCreate { channel: String },
|
||||||
|
// A user joined a channel (an FJOIN member or an IJOIN), for auto-op.
|
||||||
|
Join { uid: String, channel: String },
|
||||||
// A channel's modes changed (FMODE), for enforcing mode locks. Our own
|
// A channel's modes changed (FMODE), for enforcing mode locks. Our own
|
||||||
// changes are filtered out by the protocol layer.
|
// changes are filtered out by the protocol layer.
|
||||||
ChannelModeChange { channel: String, modes: String },
|
ChannelModeChange { channel: String, modes: String },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue