v0.4.0 - Scripting, Plugins, DCC & IRCv3
v0.4.0 — Scripting, Plugins, DCC & IRCv3
Major feature release implementing Phases 4–6 of the RustIRC development plan. This release transforms the project from a GUI-focused IRC client into a fully extensible platform with production-ready Lua scripting, a plugin system, DCC protocol support, IRCv3 extensions, flood protection, and proxy support.
Release Metrics
| Metric | v0.3.9 | v0.4.0 | Change |
|---|---|---|---|
| Tests | 144 | 266 | +84% |
| Unit Tests | 144 | 233 | +89 |
| Integration Tests | 0 | 33 | +33 |
| New Files | — | 21 | — |
| Modified Files | — | 28 | — |
| Lines Added | — | 7,446 | — |
| Lines Removed | — | 521 | — |
| Clippy Warnings | 0 | 0 | ✅ |
| Compilation Errors | 0 | 0 | ✅ |
Phase 4: Scripting, Plugins & Configuration
Lua Scripting Engine (crates/rustirc-scripting/)
Complete rewrite of the scripting subsystem from stubs to a production-ready engine.
ScriptEngine (engine.rs — rewritten from 82 → ~300 lines)
load_script(name, code, priority)— creates Lua VM, applies sandbox, registers IRC API, executes codetrigger_event(event, msg)— dispatches IRC events to registered Lua handlers in priority orderexecute_command(cmd, args) -> Result<bool>— routes/commands through script-registered handlersauto_load_scripts()— scans configuredscripts_pathfor.luafiles and loads themlist_scripts()/unload_script(name)— script lifecycle management- Uses
std::sync::RwLock(nottokio::sync::RwLock) to avoid "Cannot start runtime from within runtime" panics when called from async contexts - Scripts sorted by priority (highest first) for deterministic event handler execution
ScriptMessage UserData (script_message.rs — new file)
- Lua-accessible IRC message type implementing
mlua::UserData - Methods:
get_nick(),get_channel(),get_text(),get_command(),get_params(),get_prefix() from_protocol_message()converter for bridgingrustirc_protocol::Message
Sandbox Security (sandbox.rs — rewritten from 20 → ~80 lines)
Sandbox::new(memory_limit, timeout_ms)andfrom_config(&ScriptingConfig)apply(&self, lua: &Lua): sets memory limit, removesio/debug/loadfile/dofile/requiremodules, restrictsosto safe subset (clock/date/difftime/time), sets instruction count hook returningmlua::VmState::Continuefor CPU timeout
IRC API Table — registered as irc global in each Lua VM:
irc.print(msg)— output to clientirc.send_message(target, text)— send PRIVMSG via EventBusirc.join(channel)/irc.part(channel)— channel managementirc.register_handler(event, callback)— event handler registrationirc.command(name, callback)— custom command registrationirc.get_var(key)/irc.set_var(key, value)— shared variable storage across scripts
Plugin System (crates/rustirc-plugins/)
PluginManager (manager.rs — rewritten from 63 → ~150 lines)
HashMap<String, LoadedPlugin>storage withLoadedPlugin { instance, info, enabled }register_plugin(Box<dyn PluginApi>)— auto-initializes viaPluginContext, stores infounload_plugin(name)— callsshutdown(), removes from mapenable_plugin()/disable_plugin()— toggle withset_enabled()callbackshutdown_all()— iterates all plugins, callsshutdown(), clears mapDropimpl callsshutdown_all()for cleanup safety
Built-in Plugins (builtin/ — new directory)
- LoggerPlugin (
logger.rs): Creates log directories on init, manages file-based IRC message logging - HighlightPlugin (
highlight.rs): Case-insensitive keyword matching viacheck_message(), dynamic word management withadd_word()/remove_word(), deduplication on add
Plugin Loader (loader.rs — rewritten)
PluginLoaderwith configurable search pathsdefault_plugin_dir()usingdirs::data_dir()/rustirc/pluginsdiscover_plugins()for directory scanning
Config File I/O (crates/rustirc-core/src/config.rs)
Config::from_file(path) -> Result<Self>— TOML deserialization withtoml::from_str()Config::save(path) -> Result<()>— pretty TOML serialization, automatic parent directory creation viastd::fs::create_dir_all()Config::default_path() -> PathBuf—dirs::config_dir()/rustirc/config.toml(XDG-compliant)Config::load_or_default() -> Self— tries default path, falls back toDefault::default()with warningConfig::generate_default_config(path)— creates commented config with example Libera Chat server- All config structs annotated with
#[serde(default)]for forward compatibility when new fields are added
New Config Sections:
DccConfig—enabled,download_dir,auto_accept,max_file_size,port_range_start/endFloodConfig—enabled,messages_per_second,burst_limit,queue_sizeProxyConfig—proxy_type(None/Socks5/HttpConnect),address,port,username,passwordNotificationConfig—enabled,highlight_words,notify_on_mention,notify_on_privateQuietHours—enabled,start_hour,end_hour,weekends
Integration Wiring (src/main.rs)
- Replaced no-op
load_config()with realConfig::from_file()/Config::load_or_default() init_scripting(config)— createsScriptEnginefrom config, callsauto_load_scripts()init_plugins(config)— createsPluginManager, registersLoggerPluginandHighlightPlugin
Phase 5: Advanced Features
DCC Protocol (crates/rustirc-core/src/dcc/)
DccManager (mod.rs — new, ~1130 lines)
- Central session tracker with
Arc<RwLock<HashMap<SessionId, DccSession>>>and async lifecycle parse_dcc_request(peer_nick, ctcp_data) -> DccResult<DccRequest>— parses CHAT, SEND, RESUME, ACCEPT from CTCP data with IP long-format conversionhandle_dcc_request(request)— creates session, emitsDccEvent::Offered, checks file size limitsinitiate_chat()/initiate_send()— outgoing DCC with CTCP message generation (filename space → underscore sanitization)accept_transfer()/cancel()/complete_session()/fail_session()— full session lifecycleip_to_long(&IpAddr) -> u64/parse_ip_long(&str) -> DccResult<IpAddr>— DCC IP encoding (32-bit integer ↔ dotted-quad)- Event channel:
mpsc::UnboundedSender<DccEvent>withtake_event_receiver()for consumer
Session Types:
DccSession::Chat { id, peer_nick, direction, remote_addr, remote_port, connected }DccSession::Send { id, peer_nick, filename, file_size, progress, active }DccSession::Receive { id, peer_nick, filename, file_size, progress, active }
DCC Chat (chat.rs — new)
DccChatwith TCP stream wrapper for direct messaginginitiate()— bind listener, return CTCP messagewait_for_connection()/connect_to_peer()— connection establishmentsend_line()/recv_line()— newline-delimited message protocol- Event emission for connect/message/disconnect
DCC Transfer (transfer.rs — new)
DccTransferwithTransferProgress { bytes_transferred, total_bytes, speed_bps, percentage }receive_file()— async file download with progress tracking and speed calculationsend_file()— async file upload with chunked writing- Cancel support via
CancellationToken
DccError — comprehensive error enum: Disabled, Io, SessionNotFound, FileTooLarge, InvalidPortRange, NoAvailablePort, InvalidRequest, ConnectionFailed, Cancelled, ResumeNotSupported, InvalidAddress, Timeout
IRCv3 Batch Handler (crates/rustirc-core/src/batch.rs — new, ~420 lines)
BatchManagerwithopen_batchesandcompleted_batchesHashMapshandle_batch_start(&Message)— parses+<ref_tag>and batch type, detects nesting viabatchtag on BATCH message itselfhandle_batch_end(&Message)— moves batch from open to completed, returnsBatchadd_message(&Message) -> bool— routes messages to open batch bybatchtag valuemessage_is_batched()/is_in_batch()/open_count()/completed_count()— query methodsget_batch()/take_batch()— completed batch retrieval
BatchType enum: Netjoin, Netsplit, ChatHistory, LabeledResponse, Custom(String) with parse() and as_str() methods
IRCv3 CHATHISTORY (crates/rustirc-core/src/chathistory.rs — new, ~370 lines)
ChatHistoryManagerwithVecDeque<PendingRequest>for FIFO request correlationrequest_history(HistoryRequest) -> (u64, Message)— queues request, returns correlation ID + protocol messagebuild_request_message(&HistoryRequest) -> Message— constructs CHATHISTORY commandhandle_response() -> Option<(u64, HistoryRequest)>— pops FIFO queue for correlationclear_pending()— disconnect cleanup
HistoryRequest enum: Before, After, Between, Around, Latest with target, reference(s), and limit
MessageReference: MsgId(String) and Timestamp(String) with parse() and to_param() for wire format
Flood Protection (crates/rustirc-core/src/flood.rs — new, ~340 lines)
Token bucket algorithm implementation:
FloodProtector::new(max_tokens, refill_rate, max_queue_size)FloodProtector::from_config(&FloodConfig)try_send() -> bool— refill tokens, consume one if availableenqueue(message) -> bool— queue message if queue not fulldrain_ready() -> Vec<String>— send all queued messages that have tokensnext_send_time() -> Option<Instant>— calculate when next token availableset_enabled(bool)— runtime toggle (disabled = unlimited sending)
Proxy Support (crates/rustirc-core/src/proxy/ — new directory)
ProxyConnectortrait (mod.rs):async fn connect(target_host, target_port) -> Result<TcpStream>withfrom_config()factory- SOCKS5 (
socks5.rs): Viatokio-sockscrate, optional username/password authentication - HTTP CONNECT (
http.rs): Manual implementation withCONNECT host:port+ basic auth header, 200 status verification
Message Tag Helpers (crates/rustirc-protocol/src/message.rs)
Added to Message:
get_tag(key) -> Option<String>— retrieve tag value by keyhas_tag(key) -> bool— check tag existenceget_time()— shortcut fortimetag (server-time)get_msgid()— shortcut formsgidtagget_batch()— shortcut forbatchtagget_label()— shortcut forlabeltag
GUI Enhancements
Notification Rules Engine (crates/rustirc-gui/src/notifications.rs — new)
NotificationRuleswith configurable highlight words, nick mention detection, channel/user filtersQuietHourswith time-based suppression and weekend overrideNotificationEntrylog with timestamps andNotificationTypeclassification
Search Engine (crates/rustirc-gui/src/search.rs — new)
SearchEnginewith full-text message searchSearchQuery: text, channel filter, user filter, date range, case sensitivitySearchStatefor UI panel integration
URL Preview (crates/rustirc-gui/src/widgets/url_preview.rs — new)
OnceLock<Regex>URL detection (MSRV 1.75 compatible — avoidsLazyLockwhich requires 1.80)UrlInfowith display text and original URL extraction
Settings Persistence (crates/rustirc-gui/src/state.rs)
- Added
#[derive(serde::Serialize, serde::Deserialize)]and#[serde(default)]toAppSettings settings_path()/save()/load()for XDG-compliant settings storage
Phase 6: Testing & Integration
Integration Test Suite (tests/)
| Test File | Tests | Coverage |
|---|---|---|
config_test.rs |
6 | Roundtrip save/load, parent dir creation, forward compatibility, default path, config generation, all-sections persistence |
scripting_test.rs |
7 | Engine creation from config, event handler firing, command execution, sandbox blocking (io/require/dofile), variable persistence, priority ordering, ScriptMessage methods |
plugin_test.rs |
7 | Registration/listing, enable/disable, unload lifecycle, highlight word matching, logger init, shutdown_all, plugin info retrieval |
ircv3_test.rs |
6 | Batch lifecycle (start/add/end), message tag helpers, CHATHISTORY request building, MessageReference parsing, flood burst limiting, flood queue management |
dcc_test.rs |
7 | Manager creation, SEND parsing, CHAT parsing, RESUME parsing, IP long-format conversion, disabled config behavior, invalid request handling |
Test Isolation: Each config test uses a unique temp subdirectory to prevent race conditions during parallel execution.
Raw String Handling: Uses r##"..."## (double-hash raw strings) for Lua code containing "#" characters (e.g., "#rust" channel names).
Dependencies Added
| Crate | Version | Purpose | Used In |
|---|---|---|---|
dirs |
6.0 | XDG-compliant config/data directory resolution | core, gui, plugins |
notify-rust |
4 | Linux D-Bus desktop notifications | gui |
tokio-socks |
0.5 | SOCKS5 proxy TCP stream wrapping | core |
chrono |
0.4 | Notification quiet hours time calculations | gui |
Breaking Changes
None — this is a purely additive release. All existing functionality, APIs, and CLI behavior remain unchanged.
Technical Notes
- MSRV: 1.75 maintained (uses
OnceLockinstead ofLazyLockwhich requires 1.80) - Async Safety: ScriptEngine uses
std::sync::RwLockto avoid tokio runtime-within-runtime panics - Clippy Compliance:
BatchType::from_str()renamed toparse()to avoidFromStrtrait confusion - Config
#[derive(Default)]: Required by clippy when manualDefaultimpl matches sub-field defaults
Full Changelog
Full Changelog: v0.3.9...v0.4.0