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 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 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 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}