config: >rehash command to reload rustbot.conf live (channels/nick/prefix/verbose/modules) without a restart
All checks were successful
ci / check (push) Successful in 47s
All checks were successful
ci / check (push) Successful in 47s
This commit is contained in:
parent
6af92c1217
commit
3c2154bbc1
7 changed files with 226 additions and 20 deletions
|
|
@ -113,6 +113,22 @@ impl Bot {
|
|||
return self.conn.privmsg(reply_to, &text);
|
||||
}
|
||||
|
||||
if command == "rehash" {
|
||||
// admin-only; unauthorized attempts are ignored (logged, no reply)
|
||||
let ok = !self.config.admin.is_empty()
|
||||
&& self
|
||||
.config
|
||||
.admin
|
||||
.iter()
|
||||
.any(|a| a.eq_ignore_ascii_case(sender));
|
||||
if !ok {
|
||||
crate::log(&format!("rehash denied for {sender}"));
|
||||
return Ok(());
|
||||
}
|
||||
let summary = self.reload_from_file()?;
|
||||
return self.conn.privmsg(reply_to, &summary);
|
||||
}
|
||||
|
||||
let Some(&idx) = self.routes.get(command) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
|
|
|||
140
src/bot/mod.rs
140
src/bot/mod.rs
|
|
@ -31,13 +31,34 @@ impl Bot {
|
|||
pub fn new(config: Config, conn: Connection) -> Bot {
|
||||
let current_nick = config.nick.clone();
|
||||
|
||||
let mods = modules::all(
|
||||
let modules = modules::all(
|
||||
&config.name,
|
||||
crate::i18n::Locale::from_code(&config.locale),
|
||||
(!config.karma_url.is_empty()).then(|| config.karma_url.clone()),
|
||||
);
|
||||
let mut bot = Bot {
|
||||
config,
|
||||
conn,
|
||||
current_nick,
|
||||
registered: false,
|
||||
available_caps: HashSet::new(),
|
||||
enabled_caps: HashSet::new(),
|
||||
batches: HashMap::new(),
|
||||
sasl_started: false,
|
||||
modules,
|
||||
routes: HashMap::new(),
|
||||
topics: HashMap::new(),
|
||||
users: HashMap::new(),
|
||||
names_building: HashMap::new(),
|
||||
};
|
||||
bot.rebuild_routes();
|
||||
bot.write_source_meta();
|
||||
bot
|
||||
}
|
||||
|
||||
fn rebuild_routes(&mut self) {
|
||||
let mut routes: HashMap<&'static str, usize> = HashMap::new();
|
||||
for (i, m) in mods.iter().enumerate() {
|
||||
for (i, m) in self.modules.iter().enumerate() {
|
||||
for spec in m.commands() {
|
||||
if routes.insert(spec.name, i).is_some() {
|
||||
crate::log(&format!(
|
||||
|
|
@ -48,24 +69,105 @@ impl Bot {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.routes = routes;
|
||||
}
|
||||
|
||||
let bot = Bot {
|
||||
config,
|
||||
conn,
|
||||
current_nick,
|
||||
registered: false,
|
||||
available_caps: HashSet::new(),
|
||||
enabled_caps: HashSet::new(),
|
||||
batches: HashMap::new(),
|
||||
sasl_started: false,
|
||||
modules: mods,
|
||||
routes,
|
||||
topics: HashMap::new(),
|
||||
users: HashMap::new(),
|
||||
names_building: HashMap::new(),
|
||||
};
|
||||
bot.write_source_meta();
|
||||
bot
|
||||
/// Re-read the config file and hot-apply this network's `[section]`.
|
||||
pub(super) fn reload_from_file(&mut self) -> io::Result<String> {
|
||||
let name = self.config.name.clone();
|
||||
match crate::config::load().into_iter().find(|c| c.name == name) {
|
||||
Some(new) => self.apply_config(new),
|
||||
None => Ok(format!("rehash: no section '{name}' in the config")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Diff `new` against the running config and hot-apply what can change
|
||||
/// without reconnecting (channels, nick, prefix, verbose, modules), noting
|
||||
/// anything that still needs a restart. Returns a one-line summary.
|
||||
pub fn apply_config(&mut self, new: Config) -> io::Result<String> {
|
||||
let mut changes: Vec<String> = Vec::new();
|
||||
|
||||
let newset: HashSet<&String> = new.channels.iter().collect();
|
||||
let oldset: HashSet<&String> = self.config.channels.iter().collect();
|
||||
let joined: Vec<String> = new
|
||||
.channels
|
||||
.iter()
|
||||
.filter(|c| !oldset.contains(c))
|
||||
.cloned()
|
||||
.collect();
|
||||
let parted: Vec<String> = self
|
||||
.config
|
||||
.channels
|
||||
.iter()
|
||||
.filter(|c| !newset.contains(c))
|
||||
.cloned()
|
||||
.collect();
|
||||
for c in &joined {
|
||||
self.conn.join(c)?;
|
||||
}
|
||||
for c in &parted {
|
||||
self.conn.part(c)?;
|
||||
let key = c.to_lowercase();
|
||||
self.topics.remove(&key);
|
||||
self.users.remove(&key);
|
||||
self.names_building.remove(&key);
|
||||
}
|
||||
if !joined.is_empty() {
|
||||
changes.push(format!("+{}", joined.join(" ")));
|
||||
}
|
||||
if !parted.is_empty() {
|
||||
changes.push(format!("-{}", parted.join(" ")));
|
||||
}
|
||||
|
||||
if !new.nick.eq_ignore_ascii_case(&self.config.nick) {
|
||||
self.conn.nick(&new.nick)?;
|
||||
self.current_nick = new.nick.clone();
|
||||
changes.push(format!("nick {}", new.nick));
|
||||
}
|
||||
if new.verbose != self.config.verbose {
|
||||
self.conn.set_verbose(new.verbose);
|
||||
changes.push(format!("verbose {}", new.verbose));
|
||||
}
|
||||
if new.command_prefix != self.config.command_prefix {
|
||||
changes.push(format!("prefix {}", new.command_prefix));
|
||||
}
|
||||
|
||||
let modules_changed =
|
||||
new.locale != self.config.locale || new.karma_url != self.config.karma_url;
|
||||
let mut restart: Vec<&str> = Vec::new();
|
||||
if new.server != self.config.server {
|
||||
restart.push("server");
|
||||
}
|
||||
if new.port != self.config.port {
|
||||
restart.push("port");
|
||||
}
|
||||
if new.tls != self.config.tls {
|
||||
restart.push("tls");
|
||||
}
|
||||
if new.sasl_user != self.config.sasl_user || new.sasl_pass != self.config.sasl_pass {
|
||||
restart.push("sasl");
|
||||
}
|
||||
|
||||
self.config = new;
|
||||
self.modules = modules::all(
|
||||
&self.config.name,
|
||||
crate::i18n::Locale::from_code(&self.config.locale),
|
||||
(!self.config.karma_url.is_empty()).then(|| self.config.karma_url.clone()),
|
||||
);
|
||||
self.rebuild_routes();
|
||||
if modules_changed {
|
||||
changes.push("modules".to_string());
|
||||
}
|
||||
self.write_source_meta();
|
||||
|
||||
if !restart.is_empty() {
|
||||
changes.push(format!("(restart for: {})", restart.join("/")));
|
||||
}
|
||||
Ok(if changes.is_empty() {
|
||||
"rehash: reloaded (no changes)".to_string()
|
||||
} else {
|
||||
format!("rehash: {}", changes.join(", "))
|
||||
})
|
||||
}
|
||||
|
||||
/// Rewrite the per-network source sidecar the web board reads: server,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub struct Config {
|
|||
pub locale: String,
|
||||
pub karma_url: String,
|
||||
pub webchat: String,
|
||||
pub admin: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
|
@ -42,6 +43,7 @@ impl Default for Config {
|
|||
locale: "en".to_string(),
|
||||
karma_url: String::new(),
|
||||
webchat: String::new(),
|
||||
admin: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -169,6 +171,7 @@ fn apply_kv(c: &mut Config, key: &str, value: &str, where_: &str) {
|
|||
"locale" | "lang" => c.locale = value.to_string(),
|
||||
"karma_url" | "web_url" => c.karma_url = value.to_string(),
|
||||
"webchat" => c.webchat = value.to_string(),
|
||||
"admin" | "owner" => c.admin = split_list(value),
|
||||
other => log(&format!("{where_}: unknown key '{other}'")),
|
||||
}
|
||||
}
|
||||
|
|
@ -235,6 +238,9 @@ fn apply_env(c: &mut Config) {
|
|||
if let Ok(v) = std::env::var("IRC_WEBCHAT") {
|
||||
c.webchat = v;
|
||||
}
|
||||
if let Ok(v) = std::env::var("IRC_ADMIN") {
|
||||
c.admin = split_list(&v);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bool(value: &str) -> bool {
|
||||
|
|
|
|||
|
|
@ -116,6 +116,14 @@ impl Connection {
|
|||
self.send_raw(&format!("JOIN {channel}"))
|
||||
}
|
||||
|
||||
pub fn part(&mut self, channel: &str) -> io::Result<()> {
|
||||
self.send_raw(&format!("PART {channel}"))
|
||||
}
|
||||
|
||||
pub fn set_verbose(&mut self, verbose: bool) {
|
||||
self.verbose = verbose;
|
||||
}
|
||||
|
||||
pub fn pong(&mut self, token: &str) -> io::Result<()> {
|
||||
self.send_raw(&format!("PONG :{token}"))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue