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