Compare commits
11 Commits
legacy
...
box_messag
Author | SHA1 | Date | |
---|---|---|---|
|
bdee07143b | ||
|
7581521b61 | ||
|
b238b98b82 | ||
|
66180578d6 | ||
|
5b9f1610dd | ||
|
d414e65fd9 | ||
|
6424a7a37f | ||
|
9bb6887bed | ||
|
edef05d123 | ||
|
26a6b77632 | ||
|
a31b060dd3 |
10
Cargo.toml
Normal file
10
Cargo.toml
Normal 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"
|
13
README.md
13
README.md
@@ -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
8
src/event/mod.rs
Normal 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
110
src/lib.rs
Normal 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
44
src/main.rs
Normal 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
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
tenquestionmarks.toml
Normal file
16
tenquestionmarks.toml
Normal 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]
|
Reference in New Issue
Block a user