Compare commits
No commits in common. "master" and "box_messagesender" have entirely different histories.
master
...
box_messag
23
Cargo.toml
23
Cargo.toml
@ -1,23 +1,10 @@
|
||||
[package]
|
||||
name="tenquestionmarks"
|
||||
version="0.0.3"
|
||||
version="0.0.1"
|
||||
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" }
|
||||
hlua = "0.3"
|
||||
discord = "0.7.0"
|
||||
toml = "0.2.1"
|
||||
crossbeam = "0.2"
|
||||
|
@ -4,14 +4,10 @@ tenquestionmarks is an extensible, scriptable chat bot. This iteration is writte
|
||||
## 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.
|
||||
In this particular iteration of tenquestionmarks, there are at most two threads spawned for a module: an event consumer thread and an event producer thread. However, most modules will either produce or consume, not both.
|
||||
|
||||
## Events
|
||||
Events are things such as message, join, quit.
|
||||
|
11
TODO.md
11
TODO.md
@ -1,11 +0,0 @@
|
||||
# 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" }]`
|
@ -1,52 +0,0 @@
|
||||
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
|
@ -1,96 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -1,29 +1,8 @@
|
||||
pub mod filter;
|
||||
use {Channel, User};
|
||||
|
||||
use toml::value::Table;
|
||||
use {Message, Channel, User};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone)]
|
||||
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
|
||||
Message { sender: User, channel: Option<Channel>, content: String },
|
||||
Join { channel: Channel },
|
||||
Quit { channel: Channel }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Envelope {
|
||||
pub from: String,
|
||||
pub event: Event,
|
||||
}
|
||||
|
||||
impl Envelope {}
|
||||
|
@ -1,6 +0,0 @@
|
||||
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 +0,0 @@
|
||||
pub mod command;
|
243
src/lib.rs
243
src/lib.rs
@ -1,179 +1,90 @@
|
||||
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;
|
||||
use toml::Table;
|
||||
|
||||
mod modules;
|
||||
use modules::Module;
|
||||
use modules::loader::{ModuleLoader, ModuleLoaderError};
|
||||
mod plugins;
|
||||
use plugins::Plugin;
|
||||
use plugins::loader::{PluginLoader, PluginLoaderError};
|
||||
|
||||
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>
|
||||
plugins: BTreeMap<String, Box<Plugin>>
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
pub fn with_plugins (plugins: BTreeMap<String, Box<Plugin>>) -> Tenquestionmarks {
|
||||
let tqm = Tenquestionmarks {
|
||||
plugins: plugins
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
for (key, plugin) in &tqm.plugins {
|
||||
plugin.register(&tqm);
|
||||
}
|
||||
|
||||
Tenquestionmarks {
|
||||
modules: modules,
|
||||
subscriptions: subscriptions
|
||||
}
|
||||
tqm
|
||||
}
|
||||
|
||||
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 from_configuration (configuration: Table) -> Result<Tenquestionmarks, PluginLoaderError> {
|
||||
let loader = PluginLoader::new();
|
||||
let plugins = loader.load_from_configuration(configuration)?;
|
||||
Result::Ok(Tenquestionmarks::with_plugins(plugins))
|
||||
}
|
||||
|
||||
pub fn run (&self) {
|
||||
crossbeam::scope(|scope| {
|
||||
let mut dispatchers: BTreeMap<&str, Receiver<Envelope>> = BTreeMap::new();
|
||||
// Our event channel.
|
||||
// Plugins push events to tenquestionmarks using this channel.
|
||||
let (ref sender, ref receiver) = mpsc::channel();
|
||||
|
||||
// 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)
|
||||
// Plugin event consumer threads.
|
||||
// tenquestionmarks propagates all events to each plugin through these
|
||||
// channels.
|
||||
let senders: Vec<Sender<Event>> = self.plugins.values().map(|plugin| {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
scope.spawn(move || plugin.consume_events(receiver));
|
||||
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();
|
||||
// Plugin event producer threads.
|
||||
// Each plugin will produce events which tenquestionmarks will push
|
||||
// into all other plugins.
|
||||
for plugin in self.plugins.values() {
|
||||
let plugin_sender = sender.clone();
|
||||
scope.spawn(move || plugin.produce_events(plugin_sender));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// tenquestionmarks main event loop.
|
||||
// tenquestionmarks receives events produced by plugins and pushes them
|
||||
// into all other plugins
|
||||
loop {
|
||||
match receiver.recv() {
|
||||
Ok(event) => {
|
||||
for sender in &senders {
|
||||
sender.send(event.clone());
|
||||
}
|
||||
|
||||
info!("Dispatcher thread for \"{}\" is exiting", from);
|
||||
});
|
||||
},
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[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)]
|
||||
#[derive(Clone)]
|
||||
pub struct Channel {
|
||||
name: String,
|
||||
description: String,
|
||||
topic: String,
|
||||
sender: Box<MessageSender>
|
||||
sender: Arc<MessageSender>
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
@ -182,10 +93,10 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct User {
|
||||
name: String,
|
||||
sender: Box<MessageSender>
|
||||
sender: Arc<MessageSender>
|
||||
}
|
||||
|
||||
impl User {
|
||||
@ -194,62 +105,6 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
pub trait MessageSender : Sync + Send {
|
||||
fn send_message (&self, message: &str) {}
|
||||
}
|
||||
|
94
src/main.rs
94
src/main.rs
@ -1,84 +1,44 @@
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use std::io::Read;
|
||||
|
||||
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();
|
||||
}
|
||||
use toml::Parser;
|
||||
|
||||
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));
|
||||
let configFileName = env::args().nth(1).unwrap_or("tenquestionmarks.toml".into());
|
||||
match File::open(&configFileName) {
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
match file.read_to_string(&mut contents) {
|
||||
Ok(value) => {
|
||||
let mut parser = Parser::new(&contents);
|
||||
match parser.parse() {
|
||||
Some(configuration) => {
|
||||
println!("Loaded configuration from: {}", configFileName);
|
||||
match Tenquestionmarks::from_configuration(configuration) {
|
||||
Ok(tqm) => {
|
||||
println!("tenquestionmarks initialized successfully");
|
||||
tqm.run();
|
||||
},
|
||||
Err(e) => println!("Failed to initialize tenquestionmarks: {:?}", e)
|
||||
}
|
||||
},
|
||||
None => {
|
||||
println!("Failed to parse config file {}: {:?}. Config file must be a valid TOML file.", configFileName, parser.errors);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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)
|
||||
Err(e) => {
|
||||
println!("Failed to open config file {}: {:?}", configFileName, e);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Failed to open config file! Please specify path to a config file.");
|
||||
}
|
||||
} else {
|
||||
panic!("Failed to open config file! Please specify path to a config file.");
|
||||
}
|
||||
}
|
||||
|
@ -1,117 +0,0 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
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>>) {}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
src/plugins/discord.rs
Normal file
80
src/plugins/discord.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use discord;
|
||||
use discord::Discord;
|
||||
use discord::model::Event;
|
||||
|
||||
use plugins::Plugin;
|
||||
use toml::Table;
|
||||
|
||||
use event;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use MessageSender;
|
||||
use User;
|
||||
use Channel;
|
||||
|
||||
pub struct DiscordPlugin {
|
||||
token: String
|
||||
}
|
||||
|
||||
impl DiscordPlugin {
|
||||
pub fn new (configuration: &Table) -> Box<Plugin> {
|
||||
let token = configuration.get("token")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
Box::new(DiscordPlugin {
|
||||
token: String::from(token)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiscordMessageSender {
|
||||
discord: Arc<Discord>,
|
||||
channel_id: discord::model::ChannelId
|
||||
}
|
||||
|
||||
impl MessageSender for DiscordMessageSender {
|
||||
fn send_message (&self, message: &str) {
|
||||
self.discord.send_message(&self.channel_id, message, "", false);
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for DiscordPlugin {
|
||||
fn produce_events<'a>(&'a self, sender: Sender<event::Event>) {
|
||||
let discord = Arc::new(Discord::from_bot_token(&self.token[..]).expect("Login failed"));
|
||||
let (mut connection, _) = discord.connect().expect("Connection failed");
|
||||
loop {
|
||||
match connection.recv_event() {
|
||||
Ok(Event::MessageCreate(message)) => {
|
||||
let author = User {
|
||||
name: message.author.name.clone(),
|
||||
sender: Arc::new(DiscordMessageSender {
|
||||
discord: discord.clone(),
|
||||
channel_id: message.channel_id
|
||||
})
|
||||
};
|
||||
|
||||
let channel = Channel {
|
||||
name: String::from("channel"),
|
||||
description: String::from(""),
|
||||
topic: String::from(""),
|
||||
sender: Arc::new(DiscordMessageSender {
|
||||
discord: discord.clone(),
|
||||
channel_id: message.channel_id
|
||||
})
|
||||
};
|
||||
|
||||
sender.send(event::Event::Message { sender: author, content: message.content, channel: Option::Some(channel) });
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(discord::Error::Closed(code, body)) => {
|
||||
println!("Gateway closed on us with code {:?}: {}", code, body);
|
||||
break
|
||||
}
|
||||
Err(err) => println!("Receive error: {:?}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
src/plugins/echo.rs
Normal file
45
src/plugins/echo.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use plugins::Plugin;
|
||||
use toml::Table;
|
||||
|
||||
use std::sync::mpsc::Receiver;
|
||||
use event::Event;
|
||||
|
||||
pub struct EchoPlugin {
|
||||
prefix: String
|
||||
}
|
||||
|
||||
impl EchoPlugin {
|
||||
pub fn new (configuration: &Table) -> Box<Plugin> {
|
||||
let prefix = configuration.get("prefix")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("!echo ");
|
||||
|
||||
Box::new(EchoPlugin {
|
||||
prefix: String::from(prefix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for EchoPlugin {
|
||||
fn consume_events (&self, receiver: Receiver<Event>) {
|
||||
loop {
|
||||
match receiver.recv() {
|
||||
Ok(event) => {
|
||||
match event {
|
||||
Event::Message { content: message, channel, sender } => {
|
||||
if message.starts_with(self.prefix.as_str()) {
|
||||
let substring = &message[self.prefix.chars().count()..];
|
||||
match channel {
|
||||
Some(channel) => channel.send(substring),
|
||||
None => sender.send(substring)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
Err(error) => { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
src/plugins/hello.rs
Normal file
26
src/plugins/hello.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use plugins::Plugin;
|
||||
use toml::Table;
|
||||
|
||||
use Tenquestionmarks;
|
||||
|
||||
pub struct Hello {
|
||||
name: String
|
||||
}
|
||||
|
||||
impl Hello {
|
||||
pub fn new (configuration: &Table) -> Box<Plugin> {
|
||||
let name = configuration.get("name")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("world");
|
||||
|
||||
Box::new(Hello {
|
||||
name: String::from(name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Hello {
|
||||
fn register (&self, tenquestionmarks: &Tenquestionmarks) {
|
||||
println!("Hello, {}!", self.name);
|
||||
}
|
||||
}
|
74
src/plugins/loader.rs
Normal file
74
src/plugins/loader.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use toml::Table;
|
||||
|
||||
use plugins::Plugin;
|
||||
use plugins::hello::Hello;
|
||||
use plugins::discord::DiscordPlugin;
|
||||
use plugins::lua::LuaPlugin;
|
||||
use plugins::stdin::StdinPlugin;
|
||||
use plugins::echo::EchoPlugin;
|
||||
|
||||
pub struct PluginLoader {
|
||||
types: BTreeMap<&'static str, fn(&Table) -> Box<Plugin>>
|
||||
}
|
||||
|
||||
impl PluginLoader {
|
||||
pub fn new () -> PluginLoader {
|
||||
let mut types = BTreeMap::new();
|
||||
types.insert("hello", Hello::new as fn(&Table) -> Box<Plugin>);
|
||||
types.insert("discord", DiscordPlugin::new as fn(&Table) -> Box<Plugin>);
|
||||
types.insert("lua", LuaPlugin::new as fn(&Table) -> Box<Plugin>);
|
||||
types.insert("stdin", StdinPlugin::new as fn(&Table) -> Box<Plugin>);
|
||||
types.insert("echo", EchoPlugin::new as fn(&Table) -> Box<Plugin>);
|
||||
PluginLoader {
|
||||
types: types
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_from_configuration (&self, configuration: Table) -> Result<BTreeMap<String, Box<Plugin>>, PluginLoaderError> {
|
||||
configuration.into_iter().map(|(key, value)| {
|
||||
match value.as_table() {
|
||||
Some(table) => {
|
||||
let plugin = self.load_single_plugin(&key, table)?;
|
||||
Result::Ok((key, plugin))
|
||||
},
|
||||
None => Result::Err(PluginLoaderError { message: format!("Bad configuration parameters for plugin instance: {}. Configuration for a plugin must be a table.", key) })
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn load_single_plugin (&self, name: &str, configuration: &Table) -> Result<Box<Plugin>, PluginLoaderError> {
|
||||
/*
|
||||
* The plugin type defaults to the instance name (in the tenquestionmarks configuration)
|
||||
* but can explicitly be set by using the special "type" parameter.
|
||||
*/
|
||||
let plugin_type: &str = configuration.get("type")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or(name);
|
||||
|
||||
match self.types.get(plugin_type) {
|
||||
Some(constructor) => Result::Ok(constructor(configuration)),
|
||||
None => Result::Err(PluginLoaderError { message: format!("No such plugin type: {}", plugin_type) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PluginLoaderError {
|
||||
message: String
|
||||
}
|
||||
|
||||
impl Error for PluginLoaderError {
|
||||
fn description(&self) -> &str {
|
||||
&self.message[..]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PluginLoaderError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "PluginLoaderError: {}", self.message)
|
||||
}
|
||||
}
|
14
src/plugins/lua.rs
Normal file
14
src/plugins/lua.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use plugins::Plugin;
|
||||
use toml::Table;
|
||||
|
||||
pub struct LuaPlugin {
|
||||
|
||||
}
|
||||
|
||||
impl LuaPlugin {
|
||||
pub fn new (configuration: &Table) -> Box<Plugin> {
|
||||
Box::new(LuaPlugin {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for LuaPlugin {}
|
18
src/plugins/mod.rs
Normal file
18
src/plugins/mod.rs
Normal file
@ -0,0 +1,18 @@
|
||||
pub mod hello;
|
||||
pub mod lua;
|
||||
pub mod discord;
|
||||
pub mod stdin;
|
||||
pub mod echo;
|
||||
|
||||
pub mod loader;
|
||||
|
||||
use Tenquestionmarks;
|
||||
use event::Event;
|
||||
|
||||
use std::sync::mpsc::{Sender, Receiver};
|
||||
|
||||
pub trait Plugin : Sync {
|
||||
fn register (&self, tenquestionmarks: &Tenquestionmarks) {}
|
||||
fn consume_events (&self, receiver: Receiver<Event>) {}
|
||||
fn produce_events<'a>(&'a self, sender: Sender<Event>) {}
|
||||
}
|
51
src/plugins/stdin.rs
Normal file
51
src/plugins/stdin.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use std::io;
|
||||
|
||||
use plugins::Plugin;
|
||||
use toml::Table;
|
||||
|
||||
use User;
|
||||
use MessageSender;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use event::Event;
|
||||
|
||||
pub struct StdinPlugin {}
|
||||
|
||||
pub struct StdinMessageSender {
|
||||
name: String
|
||||
}
|
||||
|
||||
impl MessageSender for StdinMessageSender {
|
||||
fn send_message (&self, message: &str) {
|
||||
println!("send to {:?}: {:?}", self.name, message);
|
||||
}
|
||||
}
|
||||
|
||||
impl StdinPlugin {
|
||||
pub fn new (configuration: &Table) -> Box<Plugin> {
|
||||
Box::new(StdinPlugin {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for StdinPlugin {
|
||||
fn produce_events<'a>(&'a self, sender: Sender<Event>) {
|
||||
let user = User {
|
||||
name: String::from("Dave"),
|
||||
sender: Arc::new(StdinMessageSender {
|
||||
name: String::from("Dave")
|
||||
})
|
||||
};
|
||||
|
||||
loop {
|
||||
let mut input = String::new();
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(n) => {
|
||||
let message = Event::Message { sender: user.clone(), content: input, channel: None };
|
||||
sender.send(message);
|
||||
}
|
||||
Err(error) => println!("error: {}", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,193 +1,16 @@
|
||||
[general]
|
||||
foo = "bar"
|
||||
[hello]
|
||||
|
||||
[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
|
||||
[hello-dave]
|
||||
type = "hello"
|
||||
name = "Dave"
|
||||
|
||||
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
|
||||
[hello-fred]
|
||||
type = "hello"
|
||||
name = "Fred"
|
||||
|
||||
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"
|
||||
[discord]
|
||||
token = "MjgwNTYxNjUyOTM4ODMzOTIw.C4QQmw.0VO9PBBolmMyr4rreAL6VSkUut8"
|
||||
|
||||
[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"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user