Compare commits

...

95 Commits

Author SHA1 Message Date
Adrian Malacoda
45031b505f fix get_weekday() to return a better value 2018-04-30 10:30:48 -05:00
Adrian Malacoda
c2a8d5b938 use ipairs 2018-04-29 20:35:28 -05:00
Adrian Malacoda
a65c36ad9b dailies 1.1 2018-04-29 17:47:25 -05:00
Adrian Malacoda
af59c8d360 fix typo 2018-04-29 17:47:17 -05:00
Adrian Malacoda
2414eefc5f add yugipedia searcher 2018-04-29 16:23:33 -05:00
Adrian Malacoda
fb4e741386 fix issue where current_time was being reused 2018-02-27 08:10:23 -06:00
Adrian Malacoda
4ad7c33727 add lua module for sending a message on a certain day of the week 2018-02-26 23:34:53 -06:00
Adrian Malacoda
4e8e290730 allow discord channel id to have a # in front. This is mainly to work around a problem in the lua module where a string channel id becomes a float and loses precision, but to also allow for a use case where we allow channel names and not just ids. 2018-02-26 23:34:30 -06:00
Adrian Malacoda
80bcfe0580 initial stab at LuaPush implementation, doesn't currently work right now though. Also don't set type as that is a lua builtin. 2018-02-26 23:31:17 -06:00
Adrian Malacoda
44b6eecd70 make it possible to execute an lua file and code block in the same module 2018-02-25 22:49:18 -06:00
Adrian Malacoda
fec6f7f274 beef up stdin module too. This can serve two purposes, to test/interact with other modules (e.g. like a one-person chatroom) or to bridge to irc/discord. 2018-02-25 07:28:40 -06:00
Adrian Malacoda
b0c2928e78 beefed up lua scripting support, the message is now presented as a table/struct/object and lua now has access to a sender object which it can use to send messages downstream. 2018-02-25 07:04:47 -06:00
Adrian Malacoda
c6dc3f15b8 add a rudimentary irc module 2018-02-25 04:47:04 -06:00
Adrian Malacoda
906b5709d0 expand on readme 2018-02-25 03:09:53 -06:00
Adrian Malacoda
c0ee8b4d6d actually implement config reloading using notify 2018-02-25 02:52:40 -06:00
Adrian Malacoda
6fe20b8b86 reimplement reconfigure as an event that is transmitted to all modules whenever the config file changes. THis allows reconfiguration to be done in a threadsafe and relatively simple way. 2018-02-25 02:33:04 -06:00
Adrian Malacoda
9295b603aa expose a sender on the Module so we can send events directly to the module 2018-02-25 02:20:26 -06:00
Adrian Malacoda
6acffb59cc stub out config reloader thread, not currently functional 2018-02-24 19:13:09 -06:00
Adrian Malacoda
a01ad46efa make it (theoretically) possible to reconfigure a module. Might have some sort of file watcher thread which periodically checks to see if config file has been modified and reconfigures if necessary. 2018-02-24 18:54:44 -06:00
Adrian Malacoda
d9cfab7081 split off event filtering into own module 2018-02-23 00:08:13 -06:00
Adrian Malacoda
72eadd4549 add more events, dry up event filter code 2018-02-23 00:03:05 -06:00
Adrian Malacoda
93ee23c831 give the discord module the ability to recieve events and transmit messages 2018-02-22 23:43:46 -06:00
Adrian Malacoda
a37b6ab627 increment version 2018-02-22 21:44:40 -06:00
Adrian Malacoda
69c16ccd5f example of trout slap command 2018-02-22 20:55:38 -06:00
Adrian Malacoda
c774988fc9 overhaul random module to use regex instead of prefix match 2018-02-22 20:52:47 -06:00
Adrian Malacoda
240fb7f70e rules is now Optional 2018-02-22 03:19:05 -06:00
Adrian Malacoda
42fc884a58 add bulbapedia autolinker 2018-02-22 03:14:21 -06:00
Adrian Malacoda
6446bb87db there was a better way to do it 2018-02-22 03:13:03 -06:00
Adrian Malacoda
bcbc14ed17 implement a simple cache for autolink module 2018-02-22 03:07:27 -06:00
Adrian Malacoda
ca6f3391d4 simplify even more; just return an EventLoop (will probably be renamed something like Handler) 2018-02-22 02:50:49 -06:00
Adrian Malacoda
5c87b9001a simplify Envelope type, just have event loop threads generate Events which are then wrapped by Envelopes transparently 2018-02-22 02:40:22 -06:00
Adrian Malacoda
055a323d64 simplify loops, less work done in dispatcher thread 2018-02-22 02:30:54 -06:00
Adrian Malacoda
56ca5ae767 move to a model of one thread per dispatcher, instead of one main thread 2018-02-22 02:04:09 -06:00
Adrian Malacoda
fcc86a671e rearchitect event transmission so that the parents/children of each module are explicitly specified and we establish the linkages between them 2018-02-22 01:09:39 -06:00
Adrian Malacoda
3614c7eb5d update all dependencies to latest version. Need to wait for discord-rs to update though 2018-02-18 16:29:30 -06:00
Adrian Malacoda
0b289b6956 prefer if let where possible, removes empty blocks and reduces nesting 2017-05-17 21:23:00 -05:00
Adrian Malacoda
c69cc61114 prefer if let when possible 2017-05-16 21:35:05 -05:00
Adrian Malacoda
25d247f299 New filters implementation. "tags" are removed and replaced with an implementation that filters directly on the event, using an object/map instead of strings. 2017-05-11 01:29:33 -05:00
Adrian Malacoda
8cd8756722 add todo 2017-05-10 02:28:04 -05:00
Adrian Malacoda
97cc215f05 more tags 2017-05-10 02:16:05 -05:00
Adrian Malacoda
2c0e5170f4 properly implement discord channels, dry up code 2017-05-10 02:13:56 -05:00
Adrian Malacoda
bb25846566 Add GCL to interwiki list 2017-05-10 01:16:04 -05:00
Adrian Malacoda
495e3129d4 split logic for filtering out events into Subscription struct 2017-05-10 01:13:52 -05:00
Adrian Malacoda
7c26e0294a Add tag filters. This is a primitive way to screen out events that do not match a certain criteria (e.g. from user Dave) 2017-05-10 00:17:22 -05:00
Adrian Malacoda
6dda3e227f add "Logger" module which logs all received events 2017-05-09 23:39:21 -05:00
Adrian Malacoda
2baffdacd3 implement Debug for all structs, provide implementation for message senders 2017-05-09 23:36:56 -05:00
Adrian Malacoda
e8b944b836 implement run() which just forwards to event loop 2017-05-09 22:48:04 -05:00
Adrian Malacoda
9e9da11f79 begin tenquestionmarks 0.0.2. Separate Module trait into Module struct (which holds metadata and config about the module) and EventLoop trait, which implements the event loop. The constructors still return Modules, but they are structs and not boxes. 2017-05-09 22:44:33 -05:00
Adrian Malacoda
23e32f28fe update to discord 0.8.0 2017-05-09 19:50:33 -05:00
Adrian Malacoda
21b312543b yugioh rules text shouldn't be bolded 2017-03-12 18:58:20 -05:00
Adrian Malacoda
33e83d63d9 expand on mtg/ygo autolink 2017-03-12 18:48:34 -05:00
Adrian Malacoda
4888029aff Data YES (thanks Ikewise) 2017-03-05 17:18:00 +00:00
Adrian Malacoda
9112b4ada6 add some yeses to balance out the nos 2017-03-05 05:29:26 +00:00
Adrian Malacoda
8c57bd6eb7 initial working implementation of autolink module 2017-02-26 18:05:45 -06:00
Adrian Malacoda
5551ebd552 WIP for autolinker module. Currently can't use stc and tqm together because of conflicting dependencies, so this is a placeholder. 2017-02-26 17:17:22 -06:00
Adrian Malacoda
a7b9d801a1 Require echobox to have a parameter 2017-02-26 03:05:40 -06:00
Adrian Malacoda
fd1aecf4d3 add support for "general" config in the module loader. The "general" config is found under the "general" heading and is passed to each module constructor. 2017-02-26 02:44:53 -06:00
Adrian Malacoda
0a45cbb9f2 "general" as a header is reserved 2017-02-26 02:19:46 -06:00
Adrian Malacoda
d9ab75a607 Initial implementation of lua module. 2017-02-26 02:10:28 -06:00
Adrian Malacoda
41fa36cccf Transmit selfjoin event on connect 2017-02-25 23:52:43 -06:00
Adrian Malacoda
109a9131f0 Parse loglevel from environment variable 2017-02-25 23:52:31 -06:00
Adrian Malacoda
709eff63e0 add timestamp to log message 2017-02-25 23:34:01 -06:00
Adrian Malacoda
57b52772f5 set default loglevel to info 2017-02-25 23:25:09 -06:00
Adrian Malacoda
8dad1fc4aa Do not send events to their originators, or to any event not specified in the "to" list. 2017-02-25 21:31:32 -06:00
Adrian Malacoda
1fbba2554d replace Sender with ExtSender from transformable_channels. Now we can tag each outgoing envelope with the name of its sender 2017-02-25 21:11:25 -06:00
Adrian Malacoda
544974117f rename various sender/receiver variables to be clearer 2017-02-25 20:38:09 -06:00
Adrian Malacoda
4d5a412395 we're sending Envelope and transmitting Arc<Envelope>, since we're wrapping the Envelope in an Arc to transmit it, we (probably) don't need to wrap the Event in an Arc too 2017-02-25 20:33:47 -06:00
Adrian Malacoda
f22e4755d3 remove unused variable 2017-02-25 20:20:01 -06:00
Adrian Malacoda
37a9645f5b Simplify module trait by combining produce/consume event methods into a single run method that runs in the module's own thread and can produce and/or consume events. Introduce an Envelope struct that encapsulates event + to/from so we can (eventually) tag every event and also limit where events are sent (e.g. you can have a specific module configured to talk or listen only to a certain other module). 2017-02-25 20:17:46 -06:00
Adrian Malacoda
442b617f31 now with amazing echobox powers 2017-02-22 23:40:30 -06:00
Adrian Malacoda
2c893926c3 can finally commit more helpful example config 2017-02-20 15:11:13 -06:00
Adrian Malacoda
96bc25234e more event types 2017-02-19 18:21:11 -06:00
Adrian Malacoda
ce35368676 tag senders/receivers with module names so we can eventually associate individual events to module names 2017-02-19 17:31:37 -06:00
Adrian Malacoda
0f945ec604 create a dedicated Message struct and implement reply(&str) on there, since it seems to be a commonly used thing. Begin implementing helpers for command parsing. 2017-02-19 05:37:56 -06:00
Adrian Malacoda
b9d5b7916c cleanup 2017-02-19 05:06:55 -06:00
Adrian Malacoda
9b00500a77 pvn dependency 2017-02-19 04:49:26 -06:00
Adrian Malacoda
c5a88b8405 initial pvn module implementation 2017-02-19 04:49:06 -06:00
Adrian Malacoda
84d2921f8f send/receive Arc<Event> instead of Event so we don't have to clone objects all over the place. This might also enable us to be a bit more flexible with what we send with Events, and might simplify things elsewhere. 2017-02-19 02:55:30 -06:00
Adrian Malacoda
1a69349557 Add ability to specify the "playing" status string (with tenquestionmarks default) 2017-02-17 02:44:00 -06:00
Adrian Malacoda
5fc231eec6 Add logging 2017-02-17 02:38:15 -06:00
Adrian Malacoda
166805d1c2 actually add random module 2017-02-16 13:00:17 -06:00
Adrian Malacoda
26e56ebee9 Add random response module 2017-02-16 02:00:38 -06:00
Adrian Malacoda
2c87c586e2 Remove hello module 2017-02-16 01:07:19 -06:00
Adrian Malacoda
0a51c7294f rename plugin -> module 2017-02-16 01:05:33 -06:00
Adrian Malacoda
bdee07143b finally a working implementation of Discord sender 2017-02-16 00:27:53 -06:00
Adrian Malacoda
7581521b61 try arc instead of box 2017-02-16 00:16:48 -06:00
Adrian Malacoda
b238b98b82 start actually implementing discord plugin 2017-02-15 23:45:14 -06:00
Adrian Malacoda
66180578d6 alternate implementation using Box<MessageSender> and clone 2017-02-15 23:41:29 -06:00
Adrian Malacoda
5b9f1610dd attempt to flesh out send support for channel/message. Currently does not build 2017-02-15 22:41:52 -06:00
Adrian Malacoda
d414e65fd9 begin fleshing out discord module. Implement sender/channel as struct for now, there might be a performance hit from copying so much data around but we can look at optimization later 2017-02-13 22:13:33 -06:00
Adrian Malacoda
6424a7a37f Remove unused imports, stub out send() for user and channel. 2017-02-13 21:44:37 -06:00
Adrian Malacoda
9bb6887bed make plugin produce/consume events api symmetrical 2017-02-13 00:55:30 -06:00
Adrian Malacoda
edef05d123 actually write somewhat of a readme 2017-02-13 00:27:02 -06:00
Adrian Malacoda
26a6b77632 Flesh out plugins event handling, add example stdin plugin (event producer) and echo plugin (event consumer). Next step: fleshing out user/channel structs 2017-02-13 00:22:06 -06:00
Adrian Malacoda
a31b060dd3 Beginning of rust implementation for tenquestionmarks. 2017-02-08 03:25:03 -06:00
23 changed files with 2075 additions and 0 deletions

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name="tenquestionmarks"
version="0.0.3"
authors=["Adrian Malacoda <adrian.malacoda@monarch-pass.net>"]
[dependencies]
hlua = "0.4.1"
discord = "0.8.0"
toml = "0.4.5"
crossbeam = "0.3.2"
rand = "0.4.2"
log = "0.4.1"
env_logger = "0.5.3"
transformable_channels = "0.1.1"
time = "0.1"
regex = "0.2"
multimap = "0.4.0"
notify = "4.0.0"
irc = "0.11.8" # latest version which supports old openssl version (required by discord)
thread_local = "0.3"
pvn = { git = "http://gitlab.monarch-pass.net/malacoda/pvn.git" }
echobox = { git = "http://gitlab.monarch-pass.net/malacoda/echobox.git" }
stc = { git = "http://gitlab.monarch-pass.net/malacoda/stc.git" }

View File

@ -0,0 +1,17 @@
# tenquestionmarks chat bot
tenquestionmarks is an extensible, scriptable chat bot. This iteration is written in rust.
## Configuration
Configuration is done in TOML. By default, tenquestionmarks looks for `tenquestionmarks.toml`.
As of tenquestionmarks 0.0.3, tenquestionmarks supports a limited form of live configuration reloading. tenquestionmarks monitors the configuration file and, upon detecting changes, will emit a reconfiguration event to all modules to ask them to reconfigure. Reconfiguration is implemented on a per-module basis.
## Modules
tenquestionmarks is a series of modules. Modules produce events and consume events.
In this particular iteration of tenquestionmarks, there are at most two threads spawned for a module: an event loop thread (which has access to an event emitter and event receiver), and the event dispatcher thread associated with said event emitter. The event dispatcher thread takes events emitted from the event loop thread and pushes them to downstream modules' event receivers. No event dispatcher thread will be spawned for a module with no downstreams.
As of tenquestionmarks 0.0.3, each module is required to explicitly identify which modules it wishes to send and/or receive events from.
## Events
Events are things such as message, join, quit.

11
TODO.md Normal file
View File

@ -0,0 +1,11 @@
# TODO
## 0.0.2
### Filters
* Basic mechanism for filtering out messages between modules.
* Filter tags are of the type `key:value` (e.g. `type:message`, `username:Kuschelyagi`)
* Each individual filter is a stack of one or more filter values.
* Event must match AT LEAST one of these filters IN FULL in order to be accepted.
* For example, module `foo` declares `filters = [["username:Kuschelyagi", "channel:shitpost"]]`. This is a single filter with two tag conditions. This means events must match on BOTH tags to be accepted by `foo`
* If, on the other hand, module `foo` instead declares `filters = ["username:Kuschelyagi", "channel:shitpost"]` then these are separate filters and the event must only match on ONE of them.
* Other proposed filter notations:
* `filters = [{ username = "Kuschelyagi", channel = "shitpost" }]`

52
lua/dailies.lua Normal file
View File

@ -0,0 +1,52 @@
target_time = {hour=hour, min=minute, sec=second}
function get_total_day_seconds (time_table)
return (((time_table.hour * 60) + (time_table.min)) * 60) + time_table.sec
end
SECONDS_PER_DAY = get_total_day_seconds({hour=24, min=0, sec=0})
function get_sleep_duration_sec ()
current_time = os.time()
current_time_table = os.date("*t", current_time)
current_day_seconds = get_total_day_seconds(current_time_table)
target_day_seconds = get_total_day_seconds(target_time)
difference = target_day_seconds - current_day_seconds
if difference > 0 then
return difference
else
return SECONDS_PER_DAY + difference
end
end
-- Returns full weekday all lowercased (monday, tuesday, etc)
function get_weekday ()
return string.lower(os.date("%A", os.time()))
end
function sleep (sec)
return os.execute("sleep " .. tonumber(sec))
end
function run_dailies (dailies)
while true do
sleep_duration = get_sleep_duration_sec()
print("sleep for " .. sleep_duration)
if not sleep(sleep_duration) then
print("sleep exited abnormally - break")
break
end
time_table = os.date("*t", os.time())
time_table.weekday = get_weekday()
for i, fn in ipairs(dailies) do
message = fn(time_table)
if message then
print("send message " .. message)
sender:send({type = "message", channel = channel, message = message})
break
end
end
end
end

96
src/event/filter.rs Normal file
View File

@ -0,0 +1,96 @@
use toml::value::Table;
use event::{Envelope, Event};
pub trait EventFilter: Sync + Send {
fn accept (&self, envelope: &Envelope) -> bool;
}
pub struct AttributeEventFilter {
// Attributes that can be filtered out
event_type: Option<String>,
username: Option<String>,
channel: Option<String>,
message: Option<String>
}
impl AttributeEventFilter {
pub fn new (attributes: &Table) -> AttributeEventFilter {
AttributeEventFilter {
event_type: attributes.get("type").and_then(|value| value.as_str()).map(|value| String::from(value)),
message: attributes.get("message").and_then(|value| value.as_str()).map(|value| String::from(value)),
username: attributes.get("username").and_then(|value| value.as_str()).map(|value| String::from(value)),
channel: attributes.get("channel").and_then(|value| value.as_str()).map(|value| String::from(value)),
}
}
}
impl EventFilter for AttributeEventFilter {
fn accept (&self, envelope: &Envelope) -> bool {
let mut result = true;
match &envelope.event {
&Event::Message { ref message } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "message";
}
if let Some(ref channel_name) = self.channel {
match message.channel {
Some(ref channel) => result = result && channel_name == &channel.name,
None => result = false
}
}
if let Some(ref username) = self.username {
result = result && &message.author.name == username;
}
},
&Event::SelfJoin { ref channel } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "selfjoin";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
},
&Event::SelfQuit { ref channel } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "selfquit";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
},
&Event::UserJoin { ref channel, ref user } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "userjoin";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
if let Some(ref username) = self.username {
result = result && &user.name == username;
}
},
&Event::UserQuit { ref channel, ref user } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "userquit";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
if let Some(ref username) = self.username {
result = result && &user.name == username;
}
},
_ => {}
}
result
}
}

29
src/event/mod.rs Normal file
View File

@ -0,0 +1,29 @@
pub mod filter;
use toml::value::Table;
use {Message, Channel, User};
#[derive(Debug)]
pub enum Event {
Message { message: Message }, // A user sends a message
SelfJoin { channel: Channel }, // We join a channel
SelfQuit { channel: Channel }, // We quit a channel
UserJoin { channel: Channel, user: User }, // A user joins a channel
UserQuit { channel: Channel, user: User }, // A user quits a channel
UserKick { channel: Channel, user: User }, // A usre is kicked from a channel
UserBan { channel: Channel, user: User }, // A user is banned from a channel
TopicChange { channel: Channel }, // Channel topic is changed,
Configure { configuration: Table } // Request to reconfigure a module
}
#[derive(Debug)]
pub struct Envelope {
pub from: String,
pub event: Event,
}
impl Envelope {}

6
src/helpers/command.rs Normal file
View File

@ -0,0 +1,6 @@
pub fn split_command (input: &str) -> Option<(&str, &str)> {
match input.split_whitespace().into_iter().next() {
Some(command) => { Some((command, input[command.len()..].trim())) },
None => None
}
}

1
src/helpers/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod command;

255
src/lib.rs Normal file
View File

@ -0,0 +1,255 @@
extern crate toml;
extern crate crossbeam;
extern crate discord;
extern crate rand;
extern crate pvn;
extern crate echobox;
extern crate transformable_channels;
extern crate stc;
extern crate regex;
extern crate multimap;
extern crate irc;
extern crate thread_local;
#[macro_use]
extern crate hlua;
use std::collections::BTreeMap;
use toml::value::Table;
mod modules;
use modules::Module;
use modules::loader::{ModuleLoader, ModuleLoaderError};
mod event;
use event::Event;
use event::Envelope;
use event::filter::{EventFilter, AttributeEventFilter};
use std::sync::Arc;
use std::sync::mpsc;
use std::sync::mpsc::Sender;
use transformable_channels::mpsc::TransformableSender;
use transformable_channels::mpsc::Receiver;
use multimap::MultiMap;
mod helpers;
#[macro_use]
extern crate log;
pub struct Tenquestionmarks {
modules: BTreeMap<String, Module>,
subscriptions: MultiMap<String, Subscription>
}
impl Tenquestionmarks {
pub fn with_modules (modules: BTreeMap<String, Module>) -> Tenquestionmarks {
let mut subscriptions = MultiMap::new();
for (name, module) in modules.iter() {
for parent in module.parents() {
info!("{:?} registered as parent of {:?}", parent, name);
subscriptions.insert(parent, Subscription::new(name.to_owned(), &module));
}
for child_name in module.children() {
if let Some(ref child) = modules.get(&child_name) {
info!("{:?} registered as child of {:?}", child_name, name);
subscriptions.insert(name.clone(), Subscription::new(child_name.to_owned(), &child));
}
}
}
Tenquestionmarks {
modules: modules,
subscriptions: subscriptions
}
}
pub fn from_configuration (configuration: Table) -> Result<Tenquestionmarks, ModuleLoaderError> {
let loader = ModuleLoader::new();
let modules = loader.load_from_configuration(configuration)?;
Result::Ok(Tenquestionmarks::with_modules(modules))
}
pub fn get_module (&self, name: &str) -> Option<&Module> {
self.modules.get(name)
}
pub fn reconfigure (&self, configuration: &Table) {
for (key, module_configuration) in configuration {
if let (Some(module_configuration_table), Some(ref module)) = (module_configuration.as_table(), self.modules.get(key)) {
module.reconfigure(module_configuration_table.clone());
}
}
}
pub fn run (&self) {
crossbeam::scope(|scope| {
let mut dispatchers: BTreeMap<&str, Receiver<Envelope>> = BTreeMap::new();
// Event loop threads.
// Event loop threads consume events passed in by other modules' dispatcher threads,
// and produce events through their own dispatcher threads.
let senders: BTreeMap<&str, Sender<Arc<Envelope>>> = self.modules.iter().map(|(key, module)| {
let from = key.clone();
let (dispatcher_sender, dispatcher_receiver) = transformable_channels::mpsc::channel();
dispatchers.insert(key, dispatcher_receiver);
let mapped_sender = dispatcher_sender.map(move |event: Event| {
Envelope {
from: from.clone(),
event: event
}
});
let (module_sender, module_receiver) = mpsc::channel();
info!("Spawning event loop thread for \"{}\"", key);
scope.spawn(move || {
module.run(Box::new(mapped_sender), module_receiver);
info!("Event loop thread for \"{}\" is exiting", key);
});
module.set_sender(&module_sender);
(&key[..], module_sender)
}).collect();
// Dispatcher threads.
// Dispatcher threads transmit events produced by parent modules to child modules.
for (from, receiver) in dispatchers.into_iter() {
if let Some(subscriptions) = self.subscriptions.get_vec(from) {
let dispatcher_senders: BTreeMap<&str, (&Subscription, Sender<Arc<Envelope>>)> = senders.iter().filter_map(|(key, value)| {
subscriptions.iter().find(|subscription| subscription.name == *key)
.map(|subscription| (*key, (subscription, value.clone())))
}).collect();
info!("Spawning dispatcher thread for \"{}\"", from);
scope.spawn(move || {
loop {
match receiver.recv() {
Ok(envelope) => {
let arc_envelope = Arc::new(envelope);
for (child_name, &(subscription, ref sender)) in dispatcher_senders.iter() {
if subscription.can_handle_event(&arc_envelope) {
if let Err(err) = sender.send(arc_envelope.clone()) {
debug!("Failed to dispatch event to module \"{}\": {:?}", child_name, err);
}
}
}
},
Err(err) => {
error!("Failed to receive event from module: \"{}\": {:?}", from, err);
break;
}
}
}
info!("Dispatcher thread for \"{}\" is exiting", from);
});
}
}
});
}
}
#[derive(Debug)]
pub struct Message {
content: String,
author: User,
channel: Option<Channel>
}
impl Message {
fn reply (&self, message: &str) {
match self.channel {
Some(ref channel) => channel.send(message),
None => self.author.send(message)
}
}
}
#[derive(Debug)]
pub struct Channel {
name: String,
description: String,
topic: String,
sender: Box<MessageSender>
}
impl Channel {
pub fn send (&self, message: &str) {
self.sender.send_message(message);
}
}
#[derive(Debug)]
pub struct User {
name: String,
sender: Box<MessageSender>
}
impl User {
pub fn send (&self, message: &str) {
self.sender.send_message(message);
}
}
pub trait MessageSender : Sync + Send + std::fmt::Debug {
fn send_message (&self, _: &str) {}
}
pub struct NullMessageSender {}
impl MessageSender for NullMessageSender {}
impl std::fmt::Debug for NullMessageSender {
fn fmt (&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "NullMessageSender")
}
}
struct Subscription {
pub name: String,
pub filters: Vec<Box<EventFilter>>
}
impl Subscription {
pub fn new (name: String, module: &Module) -> Subscription {
let filters: Vec<Box<EventFilter>> = module.config.get("filters")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.into_iter()
.map(|value| {
match value.as_table() {
Some(table) => Some(Box::new(AttributeEventFilter::new(table)) as Box<EventFilter>),
None => None
}
})
.filter(|possible_filter| possible_filter.is_some())
.map(|possible_filter| possible_filter.unwrap())
.collect();
Subscription {
name: name,
filters: filters
}
}
pub fn can_handle_event (&self, envelope: &Envelope) -> bool {
if !self.filters.is_empty() {
for filter in &self.filters {
if filter.accept(envelope) {
return true;
}
}
debug!(
"Refusing to transmit envelope from {:?} to {:?} since envelope was filtered out",
envelope.from,
self.name
);
return false;
}
true
}
}

84
src/main.rs Normal file
View File

@ -0,0 +1,84 @@
use std::env;
use std::fs::File;
use std::io::{Read, Write};
use std::sync::mpsc;
use std::time::Duration;
extern crate tenquestionmarks;
use tenquestionmarks::Tenquestionmarks;
extern crate toml;
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate time;
extern crate crossbeam;
extern crate notify;
use notify::{RecommendedWatcher, Watcher, RecursiveMode, DebouncedEvent};
fn init_logger () {
let mut builder = env_logger::Builder::new();
builder.format(|buf, record: &log::Record| {
let t = time::now();
writeln!(buf, "{} {}:{}: {}", time::strftime("%Y-%m-%d %H:%M:%S", &t).unwrap(), record.level(), record.module_path().unwrap_or("?"), record.args())
}).filter(None, log::LevelFilter::Info);;
if env::var("RUST_LOG").is_ok() {
builder.parse(&env::var("RUST_LOG").unwrap());
}
builder.init();
}
fn main () {
init_logger();
let config_file_name = env::args().nth(1).unwrap_or("tenquestionmarks.toml".into());
match Tenquestionmarks::from_configuration(read_config_from_file(&config_file_name)) {
Ok(tqm) => {
info!("tenquestionmarks initialized successfully");
crossbeam::scope(|scope| {
scope.spawn(|| {
let (notify_sender, notify_reciever) = mpsc::channel();
let mut watcher: RecommendedWatcher = Watcher::new(notify_sender, Duration::from_secs(2)).expect("Failed to create watcher");
watcher.watch(&config_file_name, RecursiveMode::NonRecursive).expect("Failed to watch config file");
loop {
if let Ok(event) = notify_reciever.recv() {
if let DebouncedEvent::Write(_) = event {
info!("Detected modified config file, reconfiguring");
tqm.reconfigure(&read_config_from_file(&config_file_name));
}
}
}
});
tqm.run();
})
},
Err(e) => error!("Failed to initialize tenquestionmarks: {:?}", e)
}
}
fn read_config_from_file (config_file_name: &str) -> toml::value::Table {
if let Ok(mut file) = File::open(&config_file_name) {
let mut contents = String::new();
if let Err(e) = file.read_to_string(&mut contents) {
panic!("Failed to open config file {}: {:?}", config_file_name, e);
} else {
match toml::from_str(&contents) {
Ok(configuration) => {
info!("Loaded configuration from: {}", config_file_name);
configuration
},
Err(error) => panic!("Failed to parse config file {}: {:?}. Config file must be a valid TOML file.", config_file_name, error)
}
}
} else {
panic!("Failed to open config file! Please specify path to a config file.");
}
}

117
src/modules/autolink.rs Normal file
View File

@ -0,0 +1,117 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use Message;
use event::{Event, Envelope};
use stc::Link;
use stc::searchers::{Searcher, AggregateSearcher};
use stc::searchers::yugioh::{YugiohCard, YugiohSearcher};
use stc::searchers::mtg::{MtgCard, MtgSearcher};
use stc::searchers::mediawiki::MediawikiSearcher;
use regex::Regex;
use std::collections::BTreeMap;
pub struct AutolinkModule {}
impl AutolinkModule {
pub fn new (_: &Table, _: &Table) -> Box<EventLoop> {
Box::new(AutolinkModule {})
}
}
fn print_mtg_card (card: &MtgCard, message: &Message) {
message.reply(&format!("{}", card.image_url));
if let Some(ref cost) = card.cost {
message.reply(&format!("**{}** (**{}**)", card.name, cost));
} else {
message.reply(&format!("**{}**", card.name));
}
message.reply(&format!("**{}**", card.typeline));
if let Some(ref rules) = card.rules {
message.reply(&format!("{}", rules));
}
if let Some(ref flavor) = card.flavor {
message.reply(&format!("*{}*", flavor));
}
if let (&Some(ref power), &Some(ref toughness)) = (&card.power, &card.toughness) {
message.reply(&format!("{}/{}", power, toughness));
}
}
fn print_ygo_card (card: &YugiohCard, message: &Message) {
if let Some(ref family) = card.family {
message.reply(&format!("**{}** ({})", card.name, family));
} else {
message.reply(&format!("**{}**", card.name));
}
if let Some(ref level) = card.level {
message.reply(&format!("**Level**: {}", level));
}
if let Some(ref subtype) = card.subtype {
message.reply(&format!("**{} - {}**", card.card_type, subtype));
} else {
message.reply(&format!("**{}**", card.card_type));
}
message.reply(&format!("{}", card.text));
if let (&Some(ref atk), &Some(ref def)) = (&card.atk, &card.def) {
message.reply(&format!("{}/{}", atk, def));
}
}
fn print_any_link (link: &Link, message: &Message) {
message.reply(&format!("**Autolink:** {} -> {}", link.label(), link.url()));
}
impl EventLoop for AutolinkModule {
fn run (&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let link_regex = Regex::new(r"\[\[([^\[\]]*)\]\]").expect("Invalid regex...");
let mut searchers = AggregateSearcher::new();
searchers.add_searcher("mtg", Box::new(MtgSearcher::new()));
searchers.add_searcher("ygo", Box::new(YugiohSearcher::new()));
searchers.add_searcher("ygp", Box::new(MediawikiSearcher::new(String::from("https://yugipedia.com/wiki/"))));
searchers.add_searcher("pk", Box::new(MediawikiSearcher::new(String::from("https://bulbapedia.bulbagarden.net/wiki/"))));
searchers.add_searcher("gp", Box::new(MediawikiSearcher::new(String::from("https://gammapedia.monarch-pass.net/wiki/"))));
searchers.add_searcher("ip", Box::new(MediawikiSearcher::new(String::from("http://infinitypedia.org/wiki/"))));
searchers.add_searcher("gcl", Box::new(MediawikiSearcher::new(String::from("https://glitchcity.info/wiki/"))));
searchers.add_searcher("wp", Box::new(MediawikiSearcher::new(String::from("https://en.wikipedia.org/wiki/"))));
let mut searcher_cache = BTreeMap::new();
loop {
match receiver.recv() {
Ok(envelope) => {
if let Event::Message { ref message } = envelope.event {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
for cap in link_regex.captures_iter(&message.content) {
let term = cap[1].to_owned();
if let &mut Some(ref mut item) = searcher_cache.entry(term.to_owned()).or_insert_with(|| searchers.exact_search(&term)) {
print_any_link(item, message);
if let Some(card) = item.downcast_ref::<MtgCard>() {
print_mtg_card(card, message);
} else if let Some(card) = item.downcast_ref::<YugiohCard>() {
print_ygo_card(card, message);
}
}
}
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

179
src/modules/discord.rs Normal file
View File

@ -0,0 +1,179 @@
use discord;
use discord::Discord;
use discord::model::{Event, PossibleServer};
use modules::EventLoop;
use toml::value::Table;
use event;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use std::fmt;
use std::fmt::{Debug, Formatter};
use crossbeam;
use {MessageSender, Message, User, Channel};
pub struct DiscordModule {
token: String,
playing: String
}
const DEFAULT_PLAYING: &'static str = "tenquestionmarks 0.0.3";
impl DiscordModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
let token = configuration.get("token")
.and_then(|value| value.as_str())
.unwrap_or("");
let playing = configuration.get("playing")
.and_then(|value| value.as_str())
.unwrap_or(DEFAULT_PLAYING);
Box::new(DiscordModule {
token: String::from(token),
playing: String::from(playing)
})
}
}
pub struct DiscordMessageSender {
discord: Arc<Discord>,
channel_id: discord::model::ChannelId
}
impl MessageSender for DiscordMessageSender {
fn send_message (&self, message: &str) {
debug!("Send message to channel id {:?}: {:?}", self.channel_id, message);
match self.discord.send_message(self.channel_id, message, "", false) {
Ok(message) => { debug!("Send message succeeded: {:?}", message.id); },
Err(err) => { error!("Send message failed: {:?}", err) }
}
}
}
impl Debug for DiscordMessageSender {
fn fmt (&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "DiscordMessageSender {{ channel_id: {:?} }}", self.channel_id)
}
}
fn make_channel_object (discord: Arc<Discord>, channel: discord::model::Channel) -> Option<Channel> {
match channel {
discord::model::Channel::Group(group) => {
Some(Channel {
name: group.name.unwrap_or_else(|| String::from("Group channel")),
description: String::from(""),
topic: String::from(""),
sender: Box::new(DiscordMessageSender {
discord: discord,
channel_id: group.channel_id
})
})
},
discord::model::Channel::Private(private_channel) => {
None
},
discord::model::Channel::Public(public_channel) => {
Some(channel_from_public_channel(discord, public_channel))
}
}
}
fn channel_from_public_channel (discord: Arc<Discord>, channel: discord::model::PublicChannel) -> Channel {
Channel {
name: channel.name,
description: String::from(""),
topic: channel.topic.unwrap_or_else(|| String::new()),
sender: Box::new(DiscordMessageSender {
discord: discord,
channel_id: channel.id
})
}
}
impl EventLoop for DiscordModule {
fn run (&self, sender: Box<ExtSender<event::Event>>, receiver: Receiver<Arc<event::Envelope>>) {
let discord = Arc::new(Discord::from_bot_token(&self.token[..]).expect("Discord module: Login failed"));
let (mut connection, _) = discord.connect().expect("Discord module: Connection failed");
info!("Playing {}", self.playing);
connection.set_game_name(self.playing.clone());
crossbeam::scope(|scope| {
let discord_sender = discord.clone();
scope.spawn(move || {
loop {
if let Ok(envelope) = receiver.recv() {
if let event::Event::Message { ref message } = envelope.event {
if let Some(ref channel) = message.channel {
let channel_string = channel.name.trim_left_matches('#');
if let Ok(channel_id) = channel_string.parse::<u64>() {
discord_sender.send_message(discord::model::ChannelId(channel_id), &message.content, "", false);
}
}
}
} else {
break;
}
}
});
loop {
let event = connection.recv_event();
debug!("Received event: {:?}", event);
match event {
Ok(Event::ServerCreate(server)) => {
match server {
PossibleServer::Online(server) => {
info!("Joined server: {}", server.name);
for channel in server.channels {
info!(" - Joined channel: {}", channel.name);
match sender.send(event::Event::SelfJoin {
channel: channel_from_public_channel(discord.clone(), channel)
}) {
Err(err) => error!("Error sending selfjoin event: {:?}", err),
Ok(_) => {}
}
}
},
_ => {}
}
},
Ok(Event::MessageCreate(message)) => {
let author = User {
name: message.author.name.clone(),
sender: Box::new(DiscordMessageSender {
discord: discord.clone(),
channel_id: message.channel_id
})
};
let message = Message {
author: author,
content: message.content,
channel: discord.get_channel(message.channel_id).ok().and_then(|channel| make_channel_object(discord.clone(), channel))
};
if let Err(err) = sender.send(event::Event::Message { message: message }) {
error!("Error sending message event: {:?}", err)
}
}
Ok(_) => {}
Err(discord::Error::Closed(code, body)) => {
error!("Gateway closed on us with code {:?}: {}", code, body);
break
}
Err(err) => error!("Received error: {:?}", err)
}
}
});
}
}

51
src/modules/echo.rs Normal file
View File

@ -0,0 +1,51 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use helpers::command::split_command;
use event::{Event, Envelope};
pub struct EchoModule {
prefix: String
}
impl EchoModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
let prefix = configuration.get("prefix")
.and_then(|value| value.as_str())
.unwrap_or("!echo");
Box::new(EchoModule {
prefix: String::from(prefix)
})
}
}
impl EventLoop for EchoModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Message { ref message } => {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
match split_command(&message.content) {
Some((command, argument)) => {
if command == self.prefix {
message.reply(argument);
}
},
_ => {}
}
}
_ => ()
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

67
src/modules/echobox.rs Normal file
View File

@ -0,0 +1,67 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use helpers::command::split_command;
use event::{Event, Envelope};
use echobox::Echobox;
pub struct EchoboxModule {
prefix: String,
file: String
}
impl EchoboxModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
let prefix = configuration.get("prefix")
.and_then(|value| value.as_str())
.unwrap_or("?echobox");
let file = configuration.get("responses")
.and_then(|value| value.as_str())
.unwrap_or("echobox.db");
Box::new(EchoboxModule {
prefix: String::from(prefix),
file: String::from(file)
})
}
}
impl EventLoop for EchoboxModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let echobox = Echobox::with_file(&self.file).unwrap();
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Message { ref message } => {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
match split_command(&message.content) {
Some((command, in_quote)) => {
if command == self.prefix {
if !in_quote.is_empty() {
match echobox.echo(in_quote) {
Ok(out_quote) => message.reply(&format!("**Echobox, quote #{}:** {}", out_quote.id, out_quote.content)),
Err(error) => { error!("Error from echobox.echo(): {:?}", error) }
}
} else {
message.reply("**Echobox:** Please enter a quote.");
}
}
},
_ => {}
}
}
_ => ()
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

154
src/modules/irc.rs Normal file
View File

@ -0,0 +1,154 @@
use irc::client::prelude::*;
use irc::client::data::command::Command;
use modules::EventLoop;
use toml::value::Table;
use event;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use std::fmt;
use std::fmt::{Debug, Formatter};
use crossbeam;
use thread_local::CachedThreadLocal;
use {MessageSender, Message, User, Channel};
pub struct IrcHandler {
config: Config
}
const DEFAULT_NICK: &'static str = "tenquestionmarks";
impl IrcHandler {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(IrcHandler {
config: Config {
nickname: Some(configuration.get("nickname")
.and_then(|value| value.as_str())
.unwrap_or(DEFAULT_NICK)
.to_owned()),
server: Some(configuration.get("server")
.and_then(|value| value.as_str())
.expect("Expected server in IRC config")
.to_owned()),
channels: Some(configuration.get("channels")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.into_iter()
.map(|value| { String::from(value.as_str().unwrap()) })
.collect()),
.. Default::default()
}
})
}
}
pub struct IrcMessageSender {
irc: IrcServerWrapper,
channel: String
}
impl MessageSender for IrcMessageSender {
fn send_message (&self, message: &str) {
debug!("Send message to channel {:?}: {:?}", self.channel, message);
match self.irc.get().send_privmsg(&self.channel, message) {
Ok(_) => { debug!("Send message succeeded"); },
Err(err) => { error!("Send message failed: {:?}", err) }
}
}
}
impl Debug for IrcMessageSender {
fn fmt (&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "IrcMessageSender {{ channel: {:?} }}", self.channel)
}
}
fn make_user (prefix: &Option<String>, server: &IrcServer) -> Option<User> {
prefix.as_ref().and_then(|prefix| prefix.split("!").next()).map(|name| User {
name: name.to_owned(),
sender: Box::new(IrcMessageSender {
irc: IrcServerWrapper::new(&server),
channel: name.to_owned()
})
})
}
struct IrcServerWrapper {
server: IrcServer,
thread_local: CachedThreadLocal<IrcServer>
}
impl IrcServerWrapper {
pub fn new (server: &IrcServer) -> IrcServerWrapper {
IrcServerWrapper {
server: server.clone(),
thread_local: CachedThreadLocal::new()
}
}
pub fn get (&self) -> &IrcServer {
self.thread_local.get_or(|| Box::new(self.server.clone()))
}
}
unsafe impl Sync for IrcServerWrapper {}
impl EventLoop for IrcHandler {
fn run (&self, sender: Box<ExtSender<event::Event>>, receiver: Receiver<Arc<event::Envelope>>) {
let server = IrcServer::from_config(self.config.clone()).unwrap();
server.identify().unwrap();
crossbeam::scope(|scope| {
let server_sender = server.clone();
scope.spawn(move || {
loop {
if let Ok(envelope) = receiver.recv() {
if let event::Event::Message { ref message } = envelope.event {
if let Some(ref channel) = message.channel {
server_sender.send_privmsg(&channel.name, &message.content);
}
}
} else {
break;
}
}
});
for server_message in server.iter() {
if let Ok(irc_message) = server_message {
match irc_message.command {
Command::PRIVMSG(channel, message) => {
if let Some(author) = make_user(&irc_message.prefix, &server) {
let message = Message {
author: author,
content: message,
channel: Some(Channel {
name: channel.clone(),
description: "".to_owned(),
topic: "".to_owned(),
sender: Box::new(IrcMessageSender {
irc: IrcServerWrapper::new(&server),
channel: channel.clone()
})
})
};
if let Err(err) = sender.send(event::Event::Message { message: message }) {
error!("Error sending message event: {:?}", err)
}
}
},
_ => {}
}
}
}
});
}
}

98
src/modules/loader.rs Normal file
View File

@ -0,0 +1,98 @@
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use toml::value::Table;
use modules::{Module, EventLoop};
use modules::discord::DiscordModule;
use modules::lua::LuaModule;
use modules::stdin::StdinModule;
use modules::echo::EchoModule;
use modules::random::RandomModule;
use modules::pvn::PvnModule;
use modules::echobox::EchoboxModule;
use modules::autolink::AutolinkModule;
use modules::logger::LoggerModule;
use modules::irc::IrcHandler;
use std::sync::{Arc, Mutex};
pub struct ModuleLoader {
types: BTreeMap<&'static str, fn(&Table, &Table) -> Box<EventLoop>>
}
impl ModuleLoader {
pub fn new () -> ModuleLoader {
let mut types = BTreeMap::new();
types.insert("discord", DiscordModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("lua", LuaModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("stdin", StdinModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("echo", EchoModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("random", RandomModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("pvn", PvnModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("echobox", EchoboxModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("autolink", AutolinkModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("logger", LoggerModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("irc", IrcHandler::new as fn(&Table, &Table) -> Box<EventLoop>);
ModuleLoader {
types: types
}
}
pub fn load_from_configuration (&self, configuration: Table) -> Result<BTreeMap<String, Module>, ModuleLoaderError> {
let general_config = configuration.get("general")
.and_then(|value| value.as_table())
.map(|value| value.clone())
.unwrap_or_else(|| BTreeMap::new());
configuration.into_iter().filter(|&(ref key, _)| {
key != "general"
}).map(|(key, value)| {
match value.as_table() {
Some(table) => {
let module = self.load_single_module(&key, &general_config, table)?;
Result::Ok((key, module))
},
None => Result::Err(ModuleLoaderError { message: format!("Bad configuration parameters for module instance: {}. Configuration for a Module must be a table.", key) })
}
}).collect()
}
pub fn load_single_module (&self, name: &str, general_configuration: &Table, module_configuration: &Table) -> Result<Module, ModuleLoaderError> {
/*
* The Module type defaults to the instance name (in the tenquestionmarks configuration)
* but can explicitly be set by using the special "type" parameter.
*/
let module_type: &str = module_configuration.get("type")
.and_then(|value| value.as_str())
.unwrap_or(name);
match self.types.get(module_type) {
Some(constructor) => Result::Ok(Module {
module_type: module_type.to_owned(),
config: module_configuration.clone(),
event_loop: constructor(general_configuration, module_configuration),
sender: Mutex::new(None)
}),
None => Result::Err(ModuleLoaderError { message: format!("No such module type: {}", module_type) })
}
}
}
#[derive(Debug)]
pub struct ModuleLoaderError {
message: String
}
impl Error for ModuleLoaderError {
fn description(&self) -> &str {
&self.message[..]
}
}
impl fmt::Display for ModuleLoaderError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ModuleLoaderError: {}", self.message)
}
}

29
src/modules/logger.rs Normal file
View File

@ -0,0 +1,29 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use event::{Event, Envelope};
pub struct LoggerModule {}
impl LoggerModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(LoggerModule {})
}
}
impl EventLoop for LoggerModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
loop {
match receiver.recv() {
Ok(envelope) => {
info!("Received event envelope: {:?}", envelope);
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

201
src/modules/lua.rs Normal file
View File

@ -0,0 +1,201 @@
use modules::EventLoop;
use toml::Value;
use toml::value::Table;
use hlua;
use hlua::{Lua, LuaFunction, AnyHashableLuaValue, AnyLuaValue, AsMutLua};
use {User, Message, Channel, NullMessageSender};
use event::{Event, Envelope};
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use std::path::Path;
use std::fs::File;
use std::collections::HashMap;
pub struct LuaModule {
code: Option<String>,
file: Option<String>,
variables: Table
}
impl LuaModule {
pub fn new (_: &Table, config: &Table) -> Box<EventLoop> {
Box::new(LuaModule {
code: config.get("code").and_then(|value| value.as_str()).map(String::from),
file: config.get("file").and_then(|value| value.as_str()).map(String::from),
variables: config.clone()
})
}
}
struct TomlValueWrapper(Value);
implement_lua_read!(TomlValueWrapper);
impl TomlValueWrapper {
pub fn lua_set (self, lua: &mut Lua, key: &str) {
match self.0 {
Value::String(string) => lua.set(key, string),
Value::Integer(integer) => lua.set(key, (integer as i32)),
Value::Float(float) => lua.set(key, float),
Value::Boolean(boolean) => lua.set(key, boolean),
Value::Table(table) => {},
Value::Array(array) => {},
Value::Datetime(datetime) => lua.set(key, datetime.to_string())
}
}
}
/*impl<'lua, L: AsMutLua<'lua>> hlua::Push<L> for TomlValueWrapper {
type Err = hlua::Void;
fn push_to_lua (self, lua: L) -> Result<hlua::PushGuard<L>, (hlua::Void, L)> {
match self.0 {
Value::String(string) => string.push_to_lua(lua),
Value::Integer(integer) => (integer as i32).push_to_lua(lua),
Value::Float(float) => float.push_to_lua(lua),
Value::Boolean(boolean) => boolean.push_to_lua(lua),
Value::Table(table) => {
let hashmap: HashMap<_, _> = table.into_iter().map(|(key, value)| {
(key, TomlValueWrapper(value))
}).collect();
hashmap.push_to_lua(lua)
},
Value::Array(array) => {
let vec: Vec<_> = array.into_iter().map(TomlValueWrapper).collect();
vec.push_to_lua(lua)
},
Value::Datetime(datetime) => datetime.to_string().push_to_lua(lua)
}
}
}
impl<'lua, L: AsMutLua<'lua>> hlua::PushOne<L> for TomlValueWrapper {}*/
struct MessageWrapper {
envelope: Arc<Envelope>
}
implement_lua_read!(MessageWrapper);
implement_lua_push!(MessageWrapper, |mut metatable| {
let mut index = metatable.empty_array("__index");
index.set("content", hlua::function1(|wrapper: &MessageWrapper| {
if let Event::Message { ref message } = wrapper.envelope.event {
Some(message.content.clone())
} else {
None
}
}));
index.set("reply", hlua::function2(|wrapper: &MessageWrapper, reply: String| {
if let Event::Message { ref message } = wrapper.envelope.event {
message.reply(&reply);
}
}));
});
struct SenderWrapper {
sender: Box<ExtSender<Event>>
}
implement_lua_read!(SenderWrapper);
implement_lua_push!(SenderWrapper, |mut metatable| {
let mut index = metatable.empty_array("__index");
index.set("send", hlua::function2(|wrapper: &SenderWrapper, data: HashMap<AnyHashableLuaValue, AnyLuaValue>| {
if let Some(&AnyLuaValue::LuaString(ref event_type)) = data.get(&AnyHashableLuaValue::LuaString("type".to_owned())) {
match event_type.as_ref() {
"message" => {
wrapper.sender.send(Event::Message {
message: Message {
author: User {
name: data.get(&AnyHashableLuaValue::LuaString("username".to_owned()))
.and_then(|value| match value {
&AnyLuaValue::LuaString (ref string_value) => Some(string_value.to_owned()),
_ => None
})
.unwrap_or_else(|| "".to_owned()),
sender: Box::new(NullMessageSender {})
},
channel: data.get(&AnyHashableLuaValue::LuaString("channel".to_owned()))
.and_then(|value| match value {
&AnyLuaValue::LuaString (ref string_value) => Some(string_value.to_owned()),
&AnyLuaValue::LuaNumber (ref number_value) => Some(format!("{}", number_value)),
_ => None
}).map(|channel| Channel {
name: channel,
description: "".to_owned(),
sender: Box::new(NullMessageSender {}),
topic: "".to_owned()
}),
content: data.get(&AnyHashableLuaValue::LuaString("message".to_owned()))
.and_then(|value| match value {
&AnyLuaValue::LuaString (ref string_value) => Some(string_value.to_owned()),
_ => None
})
.expect("Invalid message event passed in")
}
});
},
_ => {}
}
}
}));
});
impl EventLoop for LuaModule {
fn run (&self, sender: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let mut lua = Lua::new();
lua.openlibs();
for (key, value) in &self.variables {
if key != "type" {
TomlValueWrapper(value.clone()).lua_set(&mut lua, key);
}
}
lua.set("sender", SenderWrapper {
sender: Box::new(sender.clone())
});
if let Some(ref file) = self.file {
lua.execute_from_reader::<(), _>(File::open(&Path::new(file)).unwrap()).unwrap();
}
if let Some(ref code) = self.code {
lua.execute(&code).unwrap()
}
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Configure { ref configuration } => {
for (key, value) in configuration {
if key != "type" {
TomlValueWrapper(value.clone()).lua_set(&mut lua, key);
}
}
},
Event::Message { ref message } => {
let on_message: Option<LuaFunction<_>> = lua.get("on_message");
match on_message {
Some(mut on_message) => {
on_message.call_with_args::<(), _, _>(MessageWrapper {
envelope: envelope.clone()
}).unwrap();
},
None => {}
}
}
_ => ()
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

84
src/modules/mod.rs Normal file
View File

@ -0,0 +1,84 @@
pub mod lua;
pub mod discord;
pub mod stdin;
pub mod echo;
pub mod random;
pub mod pvn;
pub mod echobox;
pub mod autolink;
pub mod logger;
pub mod irc;
pub mod loader;
use event::{Event, Envelope};
use std::sync::{Mutex, Arc};
use std::sync::mpsc::{Sender, Receiver};
use transformable_channels::mpsc::ExtSender;
use toml::value::Table;
pub struct Module {
event_loop: Box<EventLoop>,
module_type: String,
sender: Mutex<Option<Sender<Arc<Envelope>>>>,
pub config: Table,
}
impl Module {
pub fn run (&self, sender: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
self.event_loop.run(sender, receiver);
}
pub fn reconfigure (&self, configuration: Table) {
self.send(Envelope {
from: "reconfigure".to_owned(),
event: Event::Configure {
configuration: configuration
}
});
}
pub fn send (&self, event: Envelope) {
if let Ok(sender_lock) = self.sender.lock() {
if let Some(ref sender) = *sender_lock {
sender.send(Arc::new(event));
}
}
}
pub fn set_sender (&self, sender: &Sender<Arc<Envelope>>) {
if let Ok(ref mut sender_lock) = self.sender.lock() {
**sender_lock = Some(sender.clone());
}
}
pub fn parents (&self) -> Vec<String> {
self.config.get("parents")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.iter()
.map(|value| value.as_str())
.filter(|value| value.is_some())
.map(|value| value.unwrap().to_owned())
.collect()
}
pub fn children (&self) -> Vec<String> {
self.config.get("children")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.iter()
.map(|value| value.as_str())
.filter(|value| value.is_some())
.map(|value| value.unwrap().to_owned())
.collect()
}
}
pub trait EventLoop : Sync + Send {
fn run (&self, _: Box<ExtSender<Event>>, _: Receiver<Arc<Envelope>>) {}
}

169
src/modules/pvn.rs Normal file
View File

@ -0,0 +1,169 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use event::{Event, Envelope};
use Message;
use helpers::command::split_command;
use pvn::Fighter;
use pvn::pirates::{Pirate, Pirates};
use pvn::ninjas::{Ninja, Ninjas};
pub struct PvnModule {}
impl PvnModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(PvnModule {})
}
}
fn split_combatants (input: &str) -> Option<(&str, &str)> {
let fighters: Vec<&str> = input.split("|").map(|item| item.trim()).collect();
if fighters.len() != 2 {
return None;
}
Option::Some((
*fighters.get(0).expect("should be exactly two fighters"),
*fighters.get(1).expect("should be exactly two fighters")
))
}
trait PvnFighter: Fighter {
fn name (&self) -> &str;
fn print (&self, message: &Message);
fn fight (&self, other: &PvnFighter, message: &Message) {
self.print(message);
other.print(message);
if self.power() == other.power() {
message.reply("**It's a tie!**");
} else if self.power() > other.power() {
message.reply(&format!("**Winner: {}!**", self.name()));
} else {
message.reply(&format!("**Winner: {}!**", other.name()));
}
}
}
impl PvnFighter for Pirate {
fn print (&self, message: &Message) {
message.reply(&format!("**{}**", self.name));
message.reply(&format!("Swashbuckling: {}", self.swashbuckling));
message.reply(&format!("Drunkenness: {}", self.drunkenness));
message.reply(&format!("Booty: {}", self.booty));
message.reply(&format!("Weapons: {}", self.weapons.join(", ")));
message.reply(&format!("**TOTAL POWER: {}**", self.power()));
}
fn name (&self) -> &str {
&self.name[..]
}
}
impl PvnFighter for Ninja {
fn print (&self, message: &Message) {
message.reply(&format!("**{}**", self.name));
message.reply(&format!("Sneakiness: {}", self.sneakiness));
message.reply(&format!("Pajamas: {}", self.pajamas));
message.reply(&format!("Pointy Things: {}", self.pointy_things));
message.reply(&format!("Weapons: {}", self.weapons.join(", ")));
message.reply(&format!("**TOTAL POWER: {}**", self.power()));
}
fn name (&self) -> &str {
&self.name[..]
}
}
struct PirateVsNinja {
pirates: Pirates,
ninjas: Ninjas
}
impl PirateVsNinja {
fn pvn_command (&mut self, argument: &str, message: &Message) {
match split_combatants(argument) {
Some((pirate_name, ninja_name)) => {
match self.pirates.get(pirate_name) {
Ok(pirate) => {
match self.ninjas.get(ninja_name) {
Ok(ninja) => {
pirate.fight(ninja, message);
},
Err(error) => {
error!("Error getting ninja: {:?}", error);
}
}
},
Err(error) => {
error!("Error getting pirate: {:?}", error);
}
}
},
None => {
message.reply("Expected two arguments of the form: {pirate} | {ninja}");
}
}
}
fn pirate_command (&mut self, name: &str, message: &Message) {
match self.pirates.get(name) {
Ok(pirate) => {
pirate.print(message);
},
Err(error) => {
error!("Error getting pirate: {:?}", error);
}
}
}
fn ninja_command (&mut self, name: &str, message: &Message) {
match self.ninjas.get(name) {
Ok(ninja) => {
ninja.print(message);
},
Err(error) => {
error!("Error getting ninja: {:?}", error);
}
}
}
}
impl EventLoop for PvnModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let mut pvn = PirateVsNinja {
pirates: Pirates::new(),
ninjas: Ninjas::new()
};
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Message { ref message } => {
let command = split_command(&message.content);
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
match command {
Some(("?pvn", argument)) => { pvn.pvn_command(argument, message) },
Some(("?pirate", name)) => { pvn.pirate_command(name, message) },
Some(("?ninja", name)) => { pvn.ninja_command(name, message) },
_ => {}
}
}
_ => ()
}
}
Err(error) => {
error!("Error {:?}", error);
}
}
}
}
}

76
src/modules/random.rs Normal file
View File

@ -0,0 +1,76 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use regex;
use regex::Regex;
use event::{Event, Envelope};
use rand;
pub struct RandomModule {
initial_configuration: Table,
}
impl RandomModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(RandomModule {
initial_configuration: configuration.clone()
})
}
fn pattern_from_config (configuration: &Table) -> Regex {
configuration.get("pattern")
.and_then(|value| value.as_str())
.map(String::from)
.or_else(|| configuration.get("prefix")
.and_then(|value| value.as_str())
.map(|value| format!("^{}", regex::escape(value))))
.and_then(|value| Regex::new(&value).ok())
.expect("Invalid value for pattern")
}
fn responses_from_config (configuration: &Table) -> Vec<String> {
configuration.get("responses")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.into_iter()
.map(|value| { String::from(value.as_str().unwrap()) })
.collect()
}
}
impl EventLoop for RandomModule {
fn run (&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let mut rng = rand::thread_rng();
let mut pattern = RandomModule::pattern_from_config(&self.initial_configuration);
let mut responses = RandomModule::responses_from_config(&self.initial_configuration);
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Configure { ref configuration } => {
pattern = RandomModule::pattern_from_config(configuration);
responses = RandomModule::responses_from_config(configuration);
},
Event::Message { ref message } => {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
if let Some(captures) = pattern.captures(&message.content) {
let mut response = String::new();
captures.expand(&rand::sample(&mut rng, &responses, 1)[0], &mut response);
message.reply(&response);
}
},
_ => {}
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

83
src/modules/stdin.rs Normal file
View File

@ -0,0 +1,83 @@
use std::io;
use modules::EventLoop;
use toml::value::Table;
use {MessageSender, Message, User, Channel};
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use event::{Event, Envelope};
pub struct StdinModule {
name: String,
channel: Option<String>
}
const DEFAULT_NICK: &'static str = "user";
#[derive(Debug)]
pub struct StdinMessageSender {
name: String
}
impl MessageSender for StdinMessageSender {
fn send_message (&self, message: &str) {
debug!("Send message to stdout: {:?}", message);
println!("@{}: {}", self.name, message);
}
}
impl StdinModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(StdinModule {
name: configuration.get("name")
.and_then(|value| value.as_str())
.unwrap_or(DEFAULT_NICK)
.to_owned(),
channel: configuration.get("channel")
.and_then(|value| value.as_str())
.map(String::from)
})
}
}
impl EventLoop for StdinModule {
fn run(&self, sender: Box<ExtSender<Event>>, _: Receiver<Arc<Envelope>>) {
let name = self.name.clone();
let channel = self.channel.clone();
loop {
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(_) => {
let message = Message {
author: User {
name: name.clone(),
sender: Box::new(StdinMessageSender {
name: name.clone()
})
},
content: input,
channel: channel.as_ref().map(|channel| Channel {
name: channel.to_owned(),
description: "".to_owned(),
sender: Box::new(StdinMessageSender {
name: channel.clone()
}),
topic: "".to_owned()
})
};
match sender.send(Event::Message { message: message }) {
Err(err) => error!("Error sending message event: {:?}", err),
Ok(_) => {}
}
}
Err(error) => error!("error: {}", error),
}
}
}
}

193
tenquestionmarks.toml Normal file
View File

@ -0,0 +1,193 @@
[general]
foo = "bar"
[dailies]
type = "lua"
children = ["irc"]
file = "lua/dailies.lua"
hour = 8
minute = 0
second = 0
channel = "#eightbar"
code = """
function chrimbus (time)
if time.day == 25 and time.month == 12 then
return "https://i.imgur.com/cDiJxrV.gif"
end
end
function februaryween (time)
if time.day == 14 and time.month == 2 then
return "http://s3images.coroflot.com/user_files/individual_files/302239_CauyLQoHZTkSJkkGnr3kVbFtw.jpg"
end
end
function monday (time)
if time.weekday == "monday" then
return "https://memegenerator.net/img/instances/66733493/you-dont-hate-mondays-you-hate-capitalism.jpg"
end
end
function tuesday (time)
if time.weekday == "tuesday" then
return "https://78.media.tumblr.com/996c6866874691590558dce00b394416/tumblr_nxyfp54u121rp1193o1_1280.png"
end
end
function wednesday (time)
if time.weekday == "wednesday" then
return "http://i0.kym-cdn.com/photos/images/original/001/091/264/665.jpg"
end
end
function thursday (time)
if time.weekday == "thursday" then
if time.day == 20 then
return "http://i1.kym-cdn.com/photos/images/newsfeed/001/245/590/bd8.jpg"
else
return "https://78.media.tumblr.com/b05de5acb40dfb4eca044526eed5bbfa/tumblr_inline_p59be6mrQp1scg9wt_540.png"
end
end
end
function friday (time)
if time.weekday == "friday" then
if time.day == 13 then
return "https://www.youtube.com/watch?v=9cPDdQs7iHs"
else
return "https://www.youtube.com/watch?v=kfVsfOSbJY0"
end
end
end
run_dailies({chrimbus, februaryween, monday, tuesday, wednesday, thursday, friday})
"""
[irc]
nickname = "tenquestionmarks2"
server = "irc.rizon.net"
channels = ["#eightbar"]
#[discord]
#token = "your token here"
[stdin]
[echo]
parents = ["stdin", "discord", "irc"]
prefix = "?echo"
[no]
type = "random"
parents = ["stdin", "discord", "irc"]
prefix = "?no"
responses = [
"https://www.youtube.com/watch?v=WWaLxFIVX1s", # Darth Vader
"https://www.youtube.com/watch?v=Hwz7YN1AQmQ", # Mario
"https://www.youtube.com/watch?v=O-ycQlfOqyY", # Dr. Robotnik
"https://www.youtube.com/watch?v=FSWiMoO8zNE", # Luke Skywalker
"https://www.youtube.com/watch?v=31g0YE61PLQ", # Michael Scott
"https://www.youtube.com/watch?v=xFGfWrJR5Ck", # Captain Picard
"https://www.youtube.com/watch?v=gvdf5n-zI14", # Nope.avi
"https://www.youtube.com/watch?v=2HJxya0CWco", # Dr. Evil
"https://www.youtube.com/watch?v=HIAql1AfSSU", # Finn the Human
"https://www.youtube.com/watch?v=4LSJJeR6MEU", # Trunks
"https://www.youtube.com/watch?v=6h7clHdeg6g", # Vegeta
"https://www.youtube.com/watch?v=cYTzynLuEuk", # Freakazoid
"https://www.youtube.com/watch?v=6BoVUpSsA1A", # Ganon
"https://www.youtube.com/watch?v=Oz7b7uYG0pk", # Robbie Rotten's Clone/Brother (We Are #1)
"https://www.youtube.com/watch?v=iabC7-9YUG4", # Cleveland Brown
"https://www.youtube.com/watch?v=iGLh9hRmRcM", # Homer Simpson
"https://www.youtube.com/watch?v=zfbK_dbsCu0", # Nathan Explosion
"https://www.youtube.com/watch?v=WfpyGyb1J4I", # Eric Cartman
"https://www.youtube.com/watch?v=wOxt9PoJNkg", # Nostalgia Critic
]
[yes]
type = "random"
parents = ["stdin", "discord", "irc"]
prefix = "?yes"
responses = [
"https://www.youtube.com/watch?v=JPVaDaynNKM", # Captain Falcon
"https://www.youtube.com/watch?v=P3ALwKeSEYs", # M. Bison
"https://www.youtube.com/watch?v=FJbmB9k2Y88", # Daniel Bryan
"https://www.youtube.com/watch?v=kfk5NIG7AhY", # Plankton
"https://www.youtube.com/watch?v=5v15U2uaV6k", # Jerry's boss (Rick and Morty)
"https://www.youtube.com/watch?v=DrKmo0YAZEo", # Simpsons Yes Guy
"https://www.youtube.com/watch?v=6VU1Kb7k0cs", # Majin Vegeta
"https://www.youtube.com/watch?v=jyJyI3z_tcc", # Scouter Vegeta
"https://www.youtube.com/watch?v=jcreG-bhRRA", # Piccolo
"https://www.youtube.com/watch?v=XFDGnmQyDf4", # Algernop Krieger
"https://www.youtube.com/watch?v=gSnfdncZCYo", # Twilight Sparkle
"https://www.youtube.com/watch?v=IPjvDE-rKo0", # William Forester (YTMND)
"https://www.youtube.com/watch?v=CBuIqmpeAm0", # Finn Hudson
"https://www.youtube.com/watch?v=q6EoRBvdVPQ", # Oro (Yee Dinosaur)
"https://www.youtube.com/watch?v=j44nP2J23Jk", # Austin Powers
"https://www.youtube.com/watch?v=b4mJtqqfMrQ", # Data
]
[chk]
type = "random"
parents = ["stdin", "discord", "irc"]
prefix = "?chk"
responses = ["ack"]
[pvn]
parents = ["stdin", "discord", "irc"]
[echobox]
parents = ["stdin", "discord", "irc"]
[lua]
parents = ["stdin", "discord"]
code = """
function on_message (message)
message:reply("Lua says: " .. message:content())
end
"""
foo = "bar"
[lua2]
type = "lua"
parents = ["stdin", "discord"]
filters = [{ username = "David" }]
code = """
function on_message (message)
message:reply("Lua2 says: " .. message:content())
end
"""
[lua3]
type = "lua"
#children = ["lua", "irc"]
code = """
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
while true do
sender:send({type = "message", channel = "#eightbar", message = "Hello world!"})
sleep(10)
end
"""
[autolink]
parents = ["stdin", "discord", "irc"]
[logger]
parents = ["stdin", "discord", "irc"]
#filters = [{ username = "Dave" }, { username = "Kevin" }]
[icced]
type = "random"
parents = ["stdin", "discord", "irc"]
pattern = "(?i)\\bicc?ed?\\b"
responses = ["Did some carbon-based lifeform just say **I C E**?"]
[trout]
type = "random"
parents = ["stdin", "discord", "irc"]
pattern = "^\\?slap (.*)"
responses = ["/me slaps $1 around a bit with a large trout", "/me slaps $1 around a bit with a large brick"]