11 Commits

13 changed files with 509 additions and 0 deletions

10
Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name="tenquestionmarks"
version="0.0.1"
authors=["Adrian Malacoda <adrian.malacoda@monarch-pass.net>"]
[dependencies]
hlua = "0.3"
discord = "0.7.0"
toml = "0.2.1"
crossbeam = "0.2"

View File

@@ -0,0 +1,13 @@
# 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`.
## 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 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.

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

@@ -0,0 +1,8 @@
use {Channel, User};
#[derive(Clone)]
pub enum Event {
Message { sender: User, channel: Option<Channel>, content: String },
Join { channel: Channel },
Quit { channel: Channel }
}

110
src/lib.rs Normal file
View File

@@ -0,0 +1,110 @@
extern crate toml;
extern crate crossbeam;
extern crate discord;
use std::collections::BTreeMap;
use toml::Table;
mod plugins;
use plugins::Plugin;
use plugins::loader::{PluginLoader, PluginLoaderError};
mod event;
use event::Event;
use std::sync::Arc;
use std::sync::mpsc;
use std::sync::mpsc::Sender;
pub struct Tenquestionmarks {
plugins: BTreeMap<String, Box<Plugin>>
}
impl Tenquestionmarks {
pub fn with_plugins (plugins: BTreeMap<String, Box<Plugin>>) -> Tenquestionmarks {
let tqm = Tenquestionmarks {
plugins: plugins
};
for (key, plugin) in &tqm.plugins {
plugin.register(&tqm);
}
tqm
}
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| {
// Our event channel.
// Plugins push events to tenquestionmarks using this channel.
let (ref sender, ref receiver) = mpsc::channel();
// 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();
// 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));
}
// 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());
}
},
Err(_) => {}
}
}
});
}
}
#[derive(Clone)]
pub struct Channel {
name: String,
description: String,
topic: String,
sender: Arc<MessageSender>
}
impl Channel {
pub fn send (&self, message: &str) {
self.sender.send_message(message);
}
}
#[derive(Clone)]
pub struct User {
name: String,
sender: Arc<MessageSender>
}
impl User {
pub fn send (&self, message: &str) {
self.sender.send_message(message);
}
}
pub trait MessageSender : Sync + Send {
fn send_message (&self, message: &str) {}
}

44
src/main.rs Normal file
View File

@@ -0,0 +1,44 @@
use std::env;
use std::fs::File;
use std::io::Read;
extern crate tenquestionmarks;
use tenquestionmarks::Tenquestionmarks;
extern crate toml;
use toml::Parser;
fn main () {
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);
}
}
},
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.");
}
}
}

80
src/plugins/discord.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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),
}
}
}
}

16
tenquestionmarks.toml Normal file
View File

@@ -0,0 +1,16 @@
[hello]
[hello-dave]
type = "hello"
name = "Dave"
[hello-fred]
type = "hello"
name = "Fred"
[discord]
token = "MjgwNTYxNjUyOTM4ODMzOTIw.C4QQmw.0VO9PBBolmMyr4rreAL6VSkUut8"
[stdin]
[echo]