Persist the channel topic setter as a nick!ident@host mask for KEEPTOPIC and INFO
All checks were successful
CI / check (push) Successful in 5m34s
All checks were successful
CI / check (push) Successful in 5m34s
This commit is contained in:
parent
9ccc681a62
commit
948a59707e
17 changed files with 102 additions and 35 deletions
|
|
@ -15,7 +15,7 @@ impl Db {
|
|||
self.log
|
||||
.append(Event::ChannelRegistered { name: name.to_string(), founder: founder.to_string(), ts })
|
||||
.map_err(|_| ChanError::Internal)?;
|
||||
self.channels.insert(k, ChannelInfo { name: name.to_string(), founder: founder.to_string(), ts, lock_on: String::new(), lock_off: String::new(), lock_params: Vec::new(), access: Vec::new(), akick: Vec::new(), levels: Vec::new(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), email: String::new(), settings: ChanSettings::default(), topic: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None });
|
||||
self.channels.insert(k, ChannelInfo { name: name.to_string(), founder: founder.to_string(), ts, lock_on: String::new(), lock_off: String::new(), lock_params: Vec::new(), access: Vec::new(), akick: Vec::new(), levels: Vec::new(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), email: String::new(), settings: ChanSettings::default(), topic: String::new(), topic_setter: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -505,16 +505,18 @@ impl Db {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Remember a channel's topic (KEEPTOPIC / TOPICLOCK).
|
||||
pub fn set_channel_topic(&mut self, channel: &str, topic: &str) -> Result<(), ChanError> {
|
||||
/// Remember a channel's topic and who set it (KEEPTOPIC / TOPICLOCK / INFO).
|
||||
pub fn set_channel_topic(&mut self, channel: &str, topic: &str, setter: &str) -> Result<(), ChanError> {
|
||||
let k = key(channel);
|
||||
if !self.channels.contains_key(&k) {
|
||||
return Err(ChanError::NoChannel);
|
||||
}
|
||||
self.log
|
||||
.append(Event::ChannelTopicSet { channel: channel.to_string(), topic: topic.to_string() })
|
||||
.append(Event::ChannelTopicSet { channel: channel.to_string(), topic: topic.to_string(), setter: setter.to_string() })
|
||||
.map_err(|_| ChanError::Internal)?;
|
||||
self.channels.get_mut(&k).unwrap().topic = topic.to_string();
|
||||
let c = self.channels.get_mut(&k).unwrap();
|
||||
c.topic = topic.to_string();
|
||||
c.topic_setter = setter.to_string();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ pub enum Event {
|
|||
ChannelKickerSet { channel: String, kickers: KickerSettings },
|
||||
ChannelBadwordsSet { channel: String, badwords: Vec<String> },
|
||||
ChannelTriggersSet { channel: String, triggers: Vec<Trigger> },
|
||||
ChannelTopicSet { channel: String, topic: String },
|
||||
ChannelTopicSet { channel: String, topic: String, #[serde(default)] setter: String },
|
||||
ChannelSuspended { channel: String, by: String, reason: String, ts: u64, expires: Option<u64> },
|
||||
ChannelUnsuspended { channel: String },
|
||||
ChannelBotAssigned { channel: String, bot: String },
|
||||
|
|
@ -493,7 +493,7 @@ pub(crate) fn apply(accounts: &mut HashMap<String, Account>, channels: &mut Hash
|
|||
grouped.remove(&key(&nick));
|
||||
}
|
||||
Event::ChannelRegistered { name, founder, ts } => {
|
||||
channels.insert(key(&name), ChannelInfo { name, founder, ts, lock_on: String::new(), lock_off: String::new(), lock_params: Vec::new(), access: Vec::new(), akick: Vec::new(), levels: Vec::new(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), email: String::new(), settings: ChanSettings::default(), topic: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None });
|
||||
channels.insert(key(&name), ChannelInfo { name, founder, ts, lock_on: String::new(), lock_off: String::new(), lock_params: Vec::new(), access: Vec::new(), akick: Vec::new(), levels: Vec::new(), successor: None, desc: String::new(), entrymsg: String::new(), url: String::new(), email: String::new(), settings: ChanSettings::default(), topic: String::new(), topic_setter: String::new(), suspension: None, assigned_bot: None , kickers: KickerSettings::default() , badwords: Vec::new(), badwords_rev: 0 , triggers: Vec::new(), triggers_rev: 0, last_used: ts, noexpire: false, expiry_warned: false, oper_note: None });
|
||||
}
|
||||
Event::ChannelDropped { name } => {
|
||||
channels.remove(&key(&name));
|
||||
|
|
@ -585,9 +585,10 @@ pub(crate) fn apply(accounts: &mut HashMap<String, Account>, channels: &mut Hash
|
|||
c.triggers_rev = c.triggers_rev.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
Event::ChannelTopicSet { channel, topic } => {
|
||||
Event::ChannelTopicSet { channel, topic, setter } => {
|
||||
if let Some(c) = channels.get_mut(&key(&channel)) {
|
||||
c.topic = topic;
|
||||
c.topic_setter = setter;
|
||||
}
|
||||
}
|
||||
Event::ChannelSuspended { channel, by, reason, ts, expires } => {
|
||||
|
|
|
|||
|
|
@ -501,6 +501,10 @@ pub struct ChannelInfo {
|
|||
// Last known topic, kept for KEEPTOPIC / TOPICLOCK.
|
||||
#[serde(default)]
|
||||
pub topic: String,
|
||||
// Nick credited with setting `topic`, restored as the FTOPIC "setby" on
|
||||
// KEEPTOPIC and shown in INFO. Empty if unknown (e.g. pre-upgrade state).
|
||||
#[serde(default)]
|
||||
pub topic_setter: String,
|
||||
// Services suspension, if any (channel frozen while set and unexpired).
|
||||
#[serde(default)]
|
||||
pub suspension: Option<Suspension>,
|
||||
|
|
@ -1384,7 +1388,7 @@ impl Db {
|
|||
snapshot.push(Event::ChannelSettingsSet { channel: c.name.clone(), settings: c.settings });
|
||||
}
|
||||
if !c.topic.is_empty() {
|
||||
snapshot.push(Event::ChannelTopicSet { channel: c.name.clone(), topic: c.topic.clone() });
|
||||
snapshot.push(Event::ChannelTopicSet { channel: c.name.clone(), topic: c.topic.clone(), setter: c.topic_setter.clone() });
|
||||
}
|
||||
if let Some(s) = &c.suspension {
|
||||
snapshot.push(Event::ChannelSuspended { channel: c.name.clone(), by: s.by.clone(), reason: s.reason.clone(), ts: s.ts, expires: s.expires });
|
||||
|
|
|
|||
|
|
@ -515,8 +515,8 @@ impl Store for Db {
|
|||
fn triggers(&self, channel: &str) -> Vec<TriggerView> {
|
||||
Db::triggers(self, channel).iter().map(|t| TriggerView { pattern: t.pattern.clone(), response: t.response.clone(), cooldown: t.cooldown }).collect()
|
||||
}
|
||||
fn set_channel_topic(&mut self, channel: &str, topic: &str) -> Result<(), ChanError> {
|
||||
Db::set_channel_topic(self, channel, topic)
|
||||
fn set_channel_topic(&mut self, channel: &str, topic: &str, setter: &str) -> Result<(), ChanError> {
|
||||
Db::set_channel_topic(self, channel, topic, setter)
|
||||
}
|
||||
fn suspend_channel(&mut self, channel: &str, by: &str, reason: &str, expires: Option<u64>) -> Result<(), ChanError> {
|
||||
Db::suspend_channel(self, channel, by, reason, expires)
|
||||
|
|
@ -661,6 +661,7 @@ fn channel_view(c: &ChannelInfo) -> ChannelView {
|
|||
keeptopic: c.settings.keeptopic,
|
||||
topiclock: c.settings.topiclock,
|
||||
topic: c.topic.clone(),
|
||||
topic_setter: c.topic_setter.clone(),
|
||||
suspended: c.suspension.as_ref().is_some_and(|s| s.expires.is_none_or(|e| e > now())),
|
||||
assigned_bot: c.assigned_bot.clone(),
|
||||
bot_greet: c.settings.bot_greet,
|
||||
|
|
|
|||
|
|
@ -1412,9 +1412,10 @@ impl Engine {
|
|||
match self.db.channel(&channel) {
|
||||
Some(info) => {
|
||||
let mut out = vec![NetAction::ChannelMode { from: from.clone(), channel: channel.clone(), modes: info.lock_modes() }];
|
||||
// KEEPTOPIC: restore the remembered topic on a recreated channel.
|
||||
// KEEPTOPIC: restore the remembered topic on a recreated channel,
|
||||
// credited to whoever originally set it.
|
||||
if info.settings.keeptopic && !info.topic.is_empty() {
|
||||
out.push(NetAction::Topic { from, channel, topic: info.topic.clone() });
|
||||
out.push(NetAction::Topic { from, channel, topic: info.topic.clone(), setter: info.topic_setter.clone() });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -1563,20 +1564,34 @@ impl Engine {
|
|||
self.network.set_channel_key(&channel, key);
|
||||
Vec::new()
|
||||
}
|
||||
NetEvent::TopicChange { channel, setter, topic } => {
|
||||
NetEvent::TopicChange { channel, setter, topic, setby } => {
|
||||
let watch = self.notify_line('t', &setter, Some(&channel), &format!("changed the topic of {channel}"));
|
||||
let info = self.db.channel(&channel).map(|c| (c.settings.keeptopic, c.settings.topiclock, c.topic.clone()));
|
||||
// Who set it, as a stable nick!ident@host — NEVER the source uid, which
|
||||
// is recycled to another user the instant this connection drops. Prefer
|
||||
// the ircd's own setby mask; else snapshot the still-connected source.
|
||||
let author = if !setby.is_empty() {
|
||||
setby
|
||||
} else {
|
||||
match self.network.nick_of(&setter) {
|
||||
Some(nick) => match (self.network.ident_of(&setter), self.network.host_of(&setter)) {
|
||||
(Some(ident), Some(host)) => format!("{nick}!{ident}@{host}"),
|
||||
_ => nick.to_string(),
|
||||
},
|
||||
None => String::new(),
|
||||
}
|
||||
};
|
||||
let info = self.db.channel(&channel).map(|c| (c.settings.keeptopic, c.settings.topiclock, c.topic.clone(), c.topic_setter.clone()));
|
||||
// The setter may change a locked topic only with op-level access.
|
||||
let authorized = self.network.account_of(&setter).and_then(|a| self.db.channel(&channel).map(|c| c.is_op(a))).unwrap_or(false);
|
||||
let mut out: Vec<NetAction> = match info {
|
||||
// TOPICLOCK + unauthorised: put the kept topic back.
|
||||
Some((_, true, stored)) if !authorized => {
|
||||
// TOPICLOCK + unauthorised: put the kept topic back, keeping its author.
|
||||
Some((_, true, stored, stored_by)) if !authorized => {
|
||||
let from = self.chan_service.clone().unwrap_or_default();
|
||||
vec![NetAction::Topic { from, channel, topic: stored }]
|
||||
vec![NetAction::Topic { from, channel, topic: stored, setter: stored_by }]
|
||||
}
|
||||
// Otherwise remember the new topic if the channel keeps it.
|
||||
Some((keep, lock, _)) if keep || lock => {
|
||||
let _ = self.db.set_channel_topic(&channel, &topic);
|
||||
Some((keep, lock, _, _)) if keep || lock => {
|
||||
let _ = self.db.set_channel_topic(&channel, &topic, &author);
|
||||
Vec::new()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
|
|
|
|||
|
|
@ -3810,11 +3810,11 @@
|
|||
db.register_channel("#c", "boss").unwrap();
|
||||
db.set_channel_setting("#c", db::ChanSetting::KeepTopic, true).unwrap();
|
||||
db.set_channel_setting("#c", db::ChanSetting::TopicLock, true).unwrap();
|
||||
db.set_channel_topic("#c", "Official topic").unwrap();
|
||||
db.set_channel_topic("#c", "Official topic", "alice").unwrap();
|
||||
let mut e = Engine::new(vec![Box::new(ChanServ { uid: "42SAAAAAB".into() })], db);
|
||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "rando".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
||||
// A user without access changes the topic -> reverted to the stored one.
|
||||
let out = e.handle(NetEvent::TopicChange { channel: "#c".into(), setter: "000AAAAAB".into(), topic: "hacked".into() });
|
||||
let out = e.handle(NetEvent::TopicChange { channel: "#c".into(), setter: "000AAAAAB".into(), topic: "hacked".into(), setby: String::new() });
|
||||
assert!(out.iter().any(|a| matches!(a, NetAction::Topic { topic, .. } if topic == "Official topic")), "topiclock reverts: {out:?}");
|
||||
// KEEPTOPIC restores the remembered topic on channel (re)creation.
|
||||
let out = e.handle(NetEvent::ChannelCreate { channel: "#c".into() });
|
||||
|
|
|
|||
|
|
@ -246,7 +246,8 @@ pub fn import_anope(anope_path: &str, out_path: &str, node: &str) -> std::io::Re
|
|||
db.migrate_append(Event::ChannelDescSet { channel: name.to_string(), desc: desc.to_string() })?;
|
||||
}
|
||||
if let Some(topic) = field(ci, "last_topic") {
|
||||
db.migrate_append(Event::ChannelTopicSet { channel: name.to_string(), topic: topic.to_string() })?;
|
||||
let setter = field(ci, "last_topic_setter").unwrap_or("").to_string();
|
||||
db.migrate_append(Event::ChannelTopicSet { channel: name.to_string(), topic: topic.to_string(), setter })?;
|
||||
}
|
||||
if let Some(bot) = field(ci, "bi") {
|
||||
if !SERVICE_NICKS.contains(&bot.to_ascii_lowercase().as_str()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue