modules: port randquote, disable, restrictchans, channames

This commit is contained in:
Jean Chevronnet 2026-08-09 20:03:47 +00:00
parent bda7eecfc2
commit 140554719b
6 changed files with 172 additions and 0 deletions

View file

@ -533,6 +533,14 @@ impl Server {
if crate::modules::denychans::intercept(self, uid, name, is_oper) { if crate::modules::denychans::intercept(self, uid, name, is_oper) {
return; return;
} }
// restrictchans — only opers may create new channels (unless whitelisted)
if crate::modules::restrictchans::intercept(self, uid, name, is_oper) {
return;
}
// channames — forbidden characters in new channel names
if crate::modules::channames::intercept(self, uid, name) {
return;
}
// an existing channel can refuse the join (+k / +b / +i / +z / +R / +J) // an existing channel can refuse the join (+k / +b / +i / +z / +R / +J)
if let Some(ch) = self.channels.get(&key) { if let Some(ch) = self.channels.get(&key) {
if let Some(k) = &ch.modes.key { if let Some(k) = &ch.modes.key {

33
src/modules/channames.rs Normal file
View file

@ -0,0 +1,33 @@
//! channames — restrict which characters may appear in *new* channel names, beyond
//! the protocol minimum. `channames_deny = <chars>` lists forbidden characters (e.g.
//! control codes or fancy Unicode an admin doesn't want in channel names). Existing
//! channels are unaffected. Off unless `channames_deny` is set. Dispatched from
//! `Server::join`.
//!
//! Behaviour reference: InspIRCd's `m_channames`. Original native Rust.
use crate::numeric::ERR_BADCHANNEL;
use crate::server::Server;
use crate::Uid;
/// Called from `Server::join`. Returns true when `name` uses a forbidden character
/// (the caller returns without joining). Only creation of new channels is checked.
pub fn intercept(s: &mut Server, uid: Uid, name: &str) -> bool {
let Some(deny) = s.conf("channames_deny").filter(|d| !d.is_empty()) else {
return false;
};
let deny: Vec<char> = deny.chars().collect();
// joining an existing channel is always allowed
if s.channels.contains_key(&name.to_ascii_lowercase()) {
return false;
}
if name.chars().any(|c| deny.contains(&c)) {
s.numeric(
uid,
ERR_BADCHANNEL,
&format!("{name} :Channel name contains characters not permitted here"),
);
return true;
}
false
}

45
src/modules/disable.rs Normal file
View file

@ -0,0 +1,45 @@
//! disable — refuse a configured set of commands to ordinary users (opers bypass).
//! `disabled_commands = LIST WHO KNOCK` (space-separated; repeatable). A disabled
//! command replies with `421` as if it didn't exist. Off unless configured.
//!
//! Behaviour reference: InspIRCd's `m_disable`. Original native Rust.
use crate::module::{ModResult, Module};
use crate::numeric::ERR_UNKNOWNCOMMAND;
use crate::server::Server;
use crate::Uid;
pub struct Disable;
impl Module for Disable {
fn name(&self) -> &'static str {
"disable"
}
fn on_pre_command(
&mut self,
srv: &mut Server,
uid: Uid,
cmd: &str,
_params: &[String],
) -> ModResult {
// opers are never restricted
if srv.is_oper(uid) {
return ModResult::Passthru;
}
let disabled = srv
.conf_all("disabled_commands")
.iter()
.flat_map(|line| line.split_whitespace())
.any(|c| c.eq_ignore_ascii_case(cmd));
if disabled {
srv.numeric(
uid,
ERR_UNKNOWNCOMMAND,
&format!("{cmd} :This command has been disabled by the administrator."),
);
return ModResult::Deny;
}
ModResult::Passthru
}
}

View file

@ -7,6 +7,7 @@ pub mod account_registration;
pub mod antimixedutf8; pub mod antimixedutf8;
pub mod antirandom; pub mod antirandom;
pub mod blockamsg; pub mod blockamsg;
pub mod channames;
pub mod channelban; pub mod channelban;
pub mod chathistory; pub mod chathistory;
pub mod cloak; pub mod cloak;
@ -14,6 +15,7 @@ pub mod cloudflare_challenge;
pub mod connectban; pub mod connectban;
pub mod connflood; pub mod connflood;
pub mod denychans; pub mod denychans;
pub mod disable;
pub mod dnsbl; pub mod dnsbl;
pub mod extended_isupport; pub mod extended_isupport;
pub mod extjwt; pub mod extjwt;
@ -31,9 +33,11 @@ pub mod multiline;
pub mod network_icon; pub mod network_icon;
pub mod password_hash; pub mod password_hash;
pub mod profilelink; pub mod profilelink;
pub mod randquote;
pub mod realnameban; pub mod realnameban;
pub mod recaptcha; pub mod recaptcha;
pub mod reputation; pub mod reputation;
pub mod restrictchans;
pub mod restrictcommands; pub mod restrictcommands;
pub mod restrictmsg; pub mod restrictmsg;
pub mod rpc; pub mod rpc;
@ -70,6 +74,8 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
Box::new(cloudflare_challenge::CloudflareChallenge), Box::new(cloudflare_challenge::CloudflareChallenge),
Box::new(filehost::FileHost), Box::new(filehost::FileHost),
Box::new(irccloudtags::IrcCloudTags), Box::new(irccloudtags::IrcCloudTags),
Box::new(randquote::RandQuote),
Box::new(disable::Disable),
] ]
} }

44
src/modules/randquote.rs Normal file
View file

@ -0,0 +1,44 @@
//! randquote — greet each connecting user with a random line from a configured
//! set of quotes. Off unless one or more `randquote = <line>` are configured.
//!
//! Behaviour reference: InspIRCd's `m_randquote`. Original native Rust.
use openssl::rand::rand_bytes;
use crate::module::Module;
use crate::server::Server;
use crate::Uid;
/// A random index in `0..n` from the CSPRNG (0 if `n == 0` or on error).
fn rand_below(n: usize) -> usize {
if n == 0 {
return 0;
}
let mut b = [0u8; 8];
if rand_bytes(&mut b).is_err() {
return 0;
}
(u64::from_le_bytes(b) % n as u64) as usize
}
pub struct RandQuote;
impl Module for RandQuote {
fn name(&self) -> &'static str {
"randquote"
}
fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) {
let quotes = srv.conf_all("randquote");
if quotes.is_empty() {
return;
}
let quote = quotes[rand_below(quotes.len())].clone();
let nick = srv
.users
.get(&uid)
.map(|u| u.nick.clone())
.unwrap_or_default();
srv.send(uid, format!(":{} NOTICE {nick} :{quote}", srv.name));
}
}

View file

@ -0,0 +1,36 @@
//! restrictchans — only opers may *create* new channels; everyone can still join
//! existing ones. A `restrictchan = <glob>` whitelist lets ordinary users create
//! channels whose name matches (e.g. `restrictchan = #public-*`). Off unless
//! `restrictchans = yes`. Dispatched from `Server::join` (like denychans).
//!
//! Behaviour reference: InspIRCd's `m_restrictchans`. Original native Rust.
use crate::channels::glob_match;
use crate::numeric::ERR_BADCHANNEL;
use crate::server::Server;
use crate::Uid;
/// Called from `Server::join`. Returns true when creating `name` should be blocked
/// (the caller returns without joining). Opers and joins to *existing* channels pass.
pub fn intercept(s: &mut Server, uid: Uid, name: &str, is_oper: bool) -> bool {
if is_oper || !s.conf_bool("restrictchans", false) {
return false;
}
// joining a channel that already exists is always fine
if s.channels.contains_key(&name.to_ascii_lowercase()) {
return false;
}
// creating a new one: allowed only if it matches a whitelist glob
if s.conf_all("restrictchan")
.iter()
.any(|g| glob_match(g, name))
{
return false;
}
s.numeric(
uid,
ERR_BADCHANNEL,
&format!("{name} :Only IRC operators may create new channels here"),
);
true
}