irc_bot/core/
mod.rs

1pub use self::bot_cmd::BotCmdAttr;
2pub use self::bot_cmd::BotCmdAuthLvl;
3pub use self::bot_cmd::BotCmdResult;
4pub use self::bot_cmd::BotCommand;
5pub use self::config::Config;
6pub use self::config::IntoConfig;
7pub use self::err::Error;
8pub use self::err::ErrorKind;
9pub use self::err::Result;
10pub use self::handler::BotCmdHandler;
11pub use self::handler::ErrorHandler;
12pub use self::handler::TriggerHandler;
13use self::irc_msgs::parse_msg_to_nick;
14pub use self::irc_msgs::MsgDest;
15pub use self::irc_msgs::MsgMetadata;
16pub use self::irc_msgs::MsgPrefix;
17use self::irc_msgs::OwningMsgPrefix;
18use self::irc_send::push_to_outbox;
19use self::misc_traits::GetDebugInfo;
20pub use self::modl_sys::mk_module;
21pub use self::modl_sys::Module;
22use self::modl_sys::ModuleFeatureInfo;
23use self::modl_sys::ModuleInfo;
24use self::modl_sys::ModuleLoadMode;
25pub use self::reaction::ErrorReaction;
26use self::reaction::LibReaction;
27pub use self::reaction::Reaction;
28pub use self::trigger::Trigger;
29pub use self::trigger::TriggerAttr;
30pub use self::trigger::TriggerPriority;
31use crossbeam_channel;
32use irc::client::prelude as aatxe;
33use irc::client::prelude::ClientExt as AatxeClientExt;
34use irc::proto::Message;
35use rand::EntropyRng;
36use rand::SeedableRng;
37use rand::StdRng;
38use std::borrow::Borrow;
39use std::borrow::Cow;
40use std::collections::BTreeMap;
41use std::path::PathBuf;
42use std::sync::Arc;
43use std::sync::Mutex;
44use std::sync::RwLock;
45use std::thread;
46use uuid::Uuid;
47
48pub(crate) mod bot_cmd;
49
50mod config;
51mod err;
52mod handler;
53mod irc_comm;
54mod irc_msgs;
55mod irc_send;
56mod misc_traits;
57mod modl_sys;
58mod pkg_info;
59mod reaction;
60mod state;
61mod trigger;
62
63const THREAD_NAME_FAIL: &str = "This thread is unnamed?! We specifically gave it a name; what \
64                                happened?!";
65
66const LOCK_EARLY_POISON_FAIL: &str =
67    "A lock was poisoned?! Already?! We really oughtn't have panicked yet, so let's panic some \
68     more....";
69
70pub struct State {
71    aatxe_clients: RwLock<BTreeMap<ServerId, aatxe::IrcClient>>,
72    addressee_suffix: Cow<'static, str>,
73    commands: BTreeMap<Cow<'static, str>, BotCommand>,
74    config: config::Config,
75    error_handler: Arc<ErrorHandler>,
76    module_data_path: PathBuf,
77    modules: BTreeMap<Cow<'static, str>, Arc<Module>>,
78    // TODO: This is server-specific.
79    msg_prefix: RwLock<OwningMsgPrefix>,
80    rng: Mutex<StdRng>,
81    servers: BTreeMap<ServerId, RwLock<Server>>,
82    triggers: BTreeMap<TriggerPriority, Vec<Trigger>>,
83}
84
85#[derive(Debug)]
86struct Server {
87    id: ServerId,
88    aatxe_config: Arc<aatxe::Config>,
89    socket_addr_string: String,
90}
91
92#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
93pub struct ServerId {
94    uuid: Uuid,
95}
96
97impl ServerId {
98    fn new() -> Self {
99        ServerId {
100            uuid: Uuid::new_v4(),
101        }
102    }
103}
104
105impl State {
106    fn new<ErrF>(
107        config: config::Config,
108        module_data_path: PathBuf,
109        error_handler: ErrF,
110    ) -> Result<State>
111    where
112        ErrF: ErrorHandler,
113    {
114        let msg_prefix = RwLock::new(OwningMsgPrefix::from_string(format!(
115            "{}!{}@",
116            config.nickname, config.username
117        )));
118
119        Ok(State {
120            aatxe_clients: Default::default(),
121            addressee_suffix: ": ".into(),
122            commands: Default::default(),
123            config: config,
124            error_handler: Arc::new(error_handler),
125            module_data_path,
126            modules: Default::default(),
127            msg_prefix,
128            rng: Mutex::new(StdRng::from_rng(EntropyRng::new())?),
129            servers: Default::default(),
130            triggers: Default::default(),
131        })
132    }
133
134    fn handle_err<S>(&self, err: Error, desc: S) -> Option<LibReaction<Message>>
135    where
136        S: Borrow<str>,
137    {
138        let desc = desc.borrow();
139
140        let reaction = self.error_handler.run(err);
141
142        match reaction {
143            ErrorReaction::Proceed => {
144                trace!(
145                    "Proceeding despite error{}{}{}.",
146                    if desc.is_empty() { "" } else { " (" },
147                    desc,
148                    if desc.is_empty() { "" } else { ")" }
149                );
150                None
151            }
152            ErrorReaction::Quit(msg) => {
153                trace!(
154                    "Quitting because of error{}{}{}.",
155                    if desc.is_empty() { "" } else { " (" },
156                    desc,
157                    if desc.is_empty() { "" } else { ")" }
158                );
159                Some(irc_comm::mk_quit(msg))
160            }
161        }
162    }
163
164    fn handle_err_generic(&self, err: Error) -> Option<LibReaction<Message>> {
165        self.handle_err(err, "")
166    }
167}
168
169pub fn run<Cfg, ModlData, ErrF, ModlCtor, Modls>(
170    config: Cfg,
171    module_data_path: ModlData,
172    error_handler: ErrF,
173    modules: Modls,
174) where
175    Cfg: IntoConfig,
176    ModlData: Into<PathBuf>,
177    ErrF: ErrorHandler,
178    Modls: IntoIterator<Item = ModlCtor>,
179    ModlCtor: Fn() -> Module,
180{
181    let config = match config.into_config() {
182        Ok(cfg) => {
183            trace!("Loaded configuration: {:#?}", cfg);
184            cfg
185        }
186        Err(e) => {
187            error_handler.run(e);
188            error!("Terminal error: Failed to load configuration.");
189            return;
190        }
191    };
192
193    let mut state = match State::new(config, module_data_path.into(), error_handler) {
194        Ok(s) => {
195            trace!("Assembled bot state.");
196            s
197        }
198        Err(e) => {
199            error!("Terminal error while assembling bot state: {}", e);
200            return;
201        }
202    };
203
204    match state.load_modules(modules.into_iter().map(|f| f()), ModuleLoadMode::Add) {
205        Ok(()) => trace!("Loaded all requested modules without error."),
206        Err(errs) => for err in errs {
207            match state.error_handler.run(err) {
208                ErrorReaction::Proceed => {}
209                ErrorReaction::Quit(msg) => {
210                    error!(
211                        "Terminal error while loading modules: {:?}",
212                        msg.unwrap_or_default().as_ref()
213                    );
214                    return;
215                }
216            }
217        },
218    }
219
220    info!(
221        "Loaded modules: {:?}",
222        state.modules.keys().collect::<Vec<_>>()
223    );
224    info!(
225        "Loaded commands: {:?}",
226        state.commands.keys().collect::<Vec<_>>()
227    );
228
229    let mut servers = BTreeMap::new();
230
231    for aatxe_config in &state.config.servers {
232        let server_id = ServerId::new();
233
234        let socket_addr_string = match (&aatxe_config.server, aatxe_config.port) {
235            (Some(h), Some(p)) => format!("{}:{}", h, p),
236            (Some(h), None) => format!("{}:<unknown port>", h),
237            (None, Some(p)) => format!("<unknown hostname>:{}", p),
238            (None, None) => format!("<unknown hostname>:<unknown port>"),
239        };
240
241        let server = Server {
242            id: server_id,
243            aatxe_config: aatxe_config.clone(),
244            socket_addr_string,
245        };
246
247        match servers.insert(server_id, RwLock::new(server)) {
248            None => {}
249            Some(other_server) => {
250                error!(
251                    "This shouldn't happen, but there was already a server registered with UUID \
252                     {uuid}: {other_server:?}",
253                    uuid = server_id.uuid.hyphenated(),
254                    other_server = other_server.read().expect(LOCK_EARLY_POISON_FAIL),
255                );
256                return;
257            }
258        }
259    }
260
261    state.servers = servers;
262
263    let state = Arc::new(state);
264    trace!("Stored bot state onto heap.");
265
266    let mut aatxe_reactor = match aatxe::IrcReactor::new() {
267        Ok(r) => {
268            trace!("Successfully initialized IRC reactor.");
269            r
270        }
271        Err(e) => {
272            error!("Terminal error: Failed to initialize IRC reactor: {}", e);
273            return;
274        }
275    };
276
277    let (outbox_sender, outbox_receiver) = crossbeam_channel::bounded(irc_send::OUTBOX_SIZE);
278
279    spawn_thread(
280        &state,
281        "*".into(),
282        "send",
283        |_| "sending thread".into(),
284        |state| irc_send::send_main(state, outbox_receiver),
285    );
286
287    for (&server_id, server) in &state.servers {
288        let server = server.read().expect(LOCK_EARLY_POISON_FAIL);
289
290        let state_alias = state.clone();
291
292        let outbox_sender_clone = outbox_sender.clone();
293
294        let aatxe_client = match aatxe_reactor.prepare_client_and_connect(&server.aatxe_config) {
295            Ok(client) => {
296                trace!("Connected to server {:?}.", server.socket_addr_string);
297                client
298            }
299            Err(err) => {
300                error!(
301                    "Failed to connect to server {:?}: {}",
302                    server.socket_addr_string, err,
303                );
304                continue;
305            }
306        };
307
308        match state
309            .aatxe_clients
310            .write()
311            .expect(LOCK_EARLY_POISON_FAIL)
312            .insert(server_id, aatxe_client.clone())
313        {
314            None => {}
315            Some(_other_aatxe_client) => {
316                // TODO: If <https://github.com/aatxe/irc/issues/104> is resolved in favor of
317                // `IrcServer` implementing `Debug`, add the other server to this message.
318                error!(
319                    "This shouldn't happen, but there was already a server registered \
320                     with UUID {uuid}!",
321                    uuid = server_id.uuid.hyphenated(),
322                );
323                return;
324            }
325        }
326
327        match aatxe_client.identify() {
328            Ok(()) => debug!(
329                "recv[{}]: Sent identification sequence to server.",
330                server.socket_addr_string
331            ),
332            Err(e) => error!(
333                "recv[{}]: Failed to send identification sequence to server: {}",
334                server.socket_addr_string, e
335            ),
336        }
337
338        aatxe_reactor.register_client_with_handler(aatxe_client, move |_aatxe_client, msg| {
339            handle_msg(&state_alias, server_id, &outbox_sender_clone, Ok(msg));
340
341            Ok(())
342        });
343    }
344
345    match aatxe_reactor.run() {
346        Ok(()) => trace!("IRC reactor shut down normally."),
347        Err(e) => error!("IRC reactor shut down abnormally: {}", e),
348    }
349}
350
351fn handle_msg(
352    state: &Arc<State>,
353    server_id: ServerId,
354    outbox: &irc_send::OutboxPort,
355    input: Result<Message>,
356) {
357    match input.and_then(|msg| irc_comm::handle_msg(&state, server_id, outbox, msg)) {
358        Ok(()) => {}
359        Err(e) => push_to_outbox(outbox, server_id, state.handle_err_generic(e)),
360    }
361}
362
363fn spawn_thread<F, PurposeF>(
364    state: &Arc<State>,
365    addr: String,
366    purpose_desc_abbr: &str,
367    purpose_desc_full: PurposeF,
368    business: F,
369) where
370    F: FnOnce(Arc<State>) -> Result<()> + Send + 'static,
371    PurposeF: FnOnce(&str) -> String,
372{
373    let label = format!("{}[{}]", purpose_desc_abbr, addr);
374
375    let state_alias = state.clone();
376
377    let thread_build_result = thread::Builder::new().name(label).spawn(move || {
378        let current_thread = thread::current();
379        let thread_label = current_thread.name().expect(THREAD_NAME_FAIL);
380
381        trace!("{}: Starting....", thread_label);
382
383        match business(state_alias) {
384            Ok(()) => debug!("{}: Thread exited successfully.", thread_label),
385
386            // TODO: Call `state.error_handler`.
387            Err(err) => error!("{}: Thread exited with error: {:?}", thread_label, err),
388        }
389    });
390
391    match thread_build_result {
392        Ok(thread::JoinHandle { .. }) => {
393            trace!("Spawned {purpose}.", purpose = purpose_desc_full(&addr));
394        }
395        Err(err) => match state.error_handler.run(err.into()) {
396            ErrorReaction::Proceed => error!(
397                "Failed to create {purpose}; ignoring.",
398                purpose = purpose_desc_full(&addr),
399            ),
400            ErrorReaction::Quit(msg) => error!(
401                "Terminal error: Failed to create {purpose}: {msg:?}",
402                purpose = purpose_desc_full(&addr),
403                msg = msg
404            ),
405        },
406    }
407}