...
 
Commits (2)
......@@ -863,6 +863,15 @@ dependencies = [
"regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "simple_logger"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "slab"
version = "0.3.0"
......@@ -891,6 +900,7 @@ dependencies = [
"irc 0.13.5 (git+https://github.com/aatxe/irc.git?branch=0.14)",
"itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"ratelimit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
......@@ -898,6 +908,7 @@ dependencies = [
"serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_yaml 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)",
"shellwords 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"simple_logger 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
......@@ -1395,6 +1406,7 @@ dependencies = [
"checksum serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "c37ccd6be3ed1fdf419ee848f7c758eb31b054d7cd3ae3600e3bae0adf569811"
"checksum serde_yaml 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8099d3df28273c99a1728190c7a9f19d444c941044f64adf986bee7ec53051"
"checksum shellwords 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "571a866c016c55899a4c7846f0dc483396e7354e1c8a9666e6744cfb10d2e5fe"
"checksum simple_logger 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "25111f1d77db1ac3ee11b62ba4b7a162e6bb3be43e28273f0d3935cc8d3ff7fb"
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
"checksum slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5f9776d6b986f77b35c6cf846c11ad986ff128fe0b2b63a3628e3755e8d3102d"
"checksum smallvec 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b73ea3738b47563803ef814925e69be00799a8c07420be8b996f8e98fb2336db"
......
......@@ -17,19 +17,21 @@ name = "spacebot-pizza"
path = "src/pizza/main.rs"
[dependencies]
failure = {version="0.1"}
lazy_static = {version="1.0"}
failure = "0.1"
lazy_static = "1.0"
irc = {git = "https://github.com/aatxe/irc.git", branch = "0.14"}
serde = {version="1"}
serde_derive = {version="1"}
serde_json = {version="1"}
serde_yaml = {version="0.7"}
shellwords = {version="0.1"}
clap = {version="2"}
serde = "1"
serde_derive = "1"
serde_json = "1"
serde_yaml = "0.7"
shellwords = "0.1"
clap = "2"
chrono = {version="0.4", features=["serde"]}
time = {version="0.1.40"}
itertools = {version="0.7"}
regex = {version="1"}
ratelimit = {version="0.4"}
url = {version="1.6"}
time = "0.1.40"
itertools = "0.7"
regex = "1"
ratelimit = "0.4"
url = "1.6"
tokio = "0.1"
log = "0.4"
simple_logger = "1"
This diff is collapsed.
......@@ -57,6 +57,9 @@ impl Repository {
self.pull();
let path = self.0.join("resolutions").join(format!("{}", resolution.date().year()));
println!("Appending to resolution file: {:?}", path);
let mut file = OpenOptions::new()
.create(true)
.write(true)
......
This diff is collapsed.
......@@ -24,5 +24,5 @@ pub fn bootstrap<M>(name: &str)
let config = config::load(config).expect(&format!("Failed to load config: {}", config));
let bot: Bot<M> = Bot::new(config);
bot.run();
bot.run().expect("Error in bot");
}
use crate::config::ModuleConfig;
use failure::{Error, Fail};
use failure::{Fail,Error};
use irc::client::Client as ClientX;
use irc::client::PackedIrcClient;
use irc::client::prelude::*;
use irc::error::IrcError;
use tokio::prelude::{future, IntoFuture};
use tokio::runtime::current_thread::Runtime;
use log::{trace, debug, info, warn, error};
pub struct Bot<M>
where M: Module {
config: M::Config,
}
#[derive(Clone)]
pub struct Request<'a> {
/// The message text (with prefix removed)
message: &'a str,
/// The nickname who sent the message
source_nickname: &'a str,
source: &'a str,
/// The target nickname or channel of the message
target: &'a str,
}
#[derive(Clone)]
pub struct Response<'a> {
connection: &'a IrcClient,
......@@ -29,7 +35,7 @@ pub struct Response<'a> {
pub struct Initializer {
connection: IrcClient,
futures: Vec<Box<Future<Item=(), Error=failure::Error>>>,
futures: Vec<Box<Future<Item=(), Error=Error>>>,
}
impl Initializer {
......@@ -49,12 +55,12 @@ pub struct Client {
connection: IrcClient,
}
pub trait Module where {
pub trait Module where Self: std::marker::Sized {
type Config: ModuleConfig;
fn init(config: &Self::Config, initializer: &mut Initializer) -> Self;
fn init(config: &Self::Config, initializer: &mut Initializer) -> Result<Self, Error>;
fn handle(&mut self, request: Request, response: Response); // FIXME: Error handling
fn handle(&mut self, request: Request, response: Response) -> Result<(), Error>;
}
impl<'a> Request<'a> {
......@@ -62,14 +68,17 @@ impl<'a> Request<'a> {
return self.message;
}
pub fn source_nickname(&self) -> &'a str {
return self.source_nickname;
pub fn source(&self) -> &'a str {
return self.source;
}
pub fn target(&self) -> &'a str {
return self.target;
}
}
impl<'a> Response<'a> {
pub fn reply(&self, message: &str) {
// FIXME: Error handling
pub fn reply(&self, message: &str) -> Result<(), Error> {
return self.send(self.response_target, message);
}
......@@ -77,47 +86,47 @@ impl<'a> Response<'a> {
return self.response_target;
}
pub fn send(&self, target: &str, message: &str) {
// FIXME: Error handling
pub fn send(&self, target: &str, message: &str) -> Result<(), Error> {
for line in message.split("\n") {
if line != "" {
self.connection.send_privmsg(target, line)
.expect("Failed to send message");
self.connection.send_privmsg(target, line)?;
}
}
return Ok(());
}
pub fn notice(&self, target: &str, message: &str) {
// FIXME: Error handling
pub fn notice(&self, target: &str, message: &str) -> Result<(), Error> {
for line in message.split("\n") {
if line != "" {
self.connection.send_notice(target, line)
.expect("Failed to send notice");
self.connection.send_notice(target, line)?;
}
}
return Ok(());
}
}
impl Client {
// FIXME: Get rid of duplicated code: Client vs Response and line splitting
pub fn send(&self, target: &str, message: &str) {
// FIXME: Error handling
pub fn send(&self, target: &str, message: &str) -> Result<(), Error> {
for line in message.split("\n") {
if line != "" {
self.connection.send_privmsg(target, line)
.expect("Failed to send message");
self.connection.send_privmsg(target, line)?;
}
}
return Ok(());
}
pub fn notice(&self, target: &str, message: &str) {
// FIXME: Error handling
pub fn notice(&self, target: &str, message: &str) -> Result<(), Error> {
for line in message.split("\n") {
if line != "" {
self.connection.send_notice(target, line)
.expect("Failed to send notice");
self.connection.send_notice(target, line)?;
}
}
return Ok(());
}
}
......@@ -129,8 +138,23 @@ impl<M> Bot<M>
};
}
fn run_once(&mut self, config: Config) -> Result<(), IrcError> {
println!("Lets go!");
pub fn run(self) -> Result<(), Error> {
let config = self.config.connection();
let config = Config {
server: Some(config.host.to_owned()),
port: Some(config.port),
use_ssl: Some(config.use_ssl),
cert_path: config.server_cert_path.to_owned(),
client_cert_path: config.client_cert_path.to_owned(),
nickname: Some(config.nickname.to_owned()),
username: Some(config.username.to_owned()),
channels: Some(self.config.channels()),
burst_window_length: Some(config.burst_window),
max_messages_in_burst: Some(config.burst_limit),
..Default::default()
};
info!("Lets go!");
let mut reactor = Runtime::new().unwrap();
......@@ -139,106 +163,47 @@ impl<M> Bot<M>
let PackedIrcClient(connection, connection_future) = reactor.block_on(connection)?;
connection.identify()?;
println!("Connected");
debug!("Connected");
// Initialize the module
let mut initializer = Initializer { connection: connection.clone(), futures: vec![] };
let mut module = M::init(&self.config, &mut initializer); // FIXME: Error handling
println!("Module loaded");
let mut module = M::init(&self.config, &mut initializer)?;
debug!("Module loaded");
// Build joined future for all futures from module
let module_future = future::join_all(initializer.futures)
.map_err(|err| { IrcError::Custom { inner: err } });
let module_future = future::join_all(initializer.futures);
// Handle incoming messages
let messages_future = connection.stream().for_each(move |message| {
print!("{}", message); // FIXME: Debugging only
let messages_future = connection.stream().from_err().for_each(move |message| {
trace!("{}", message);
if let Command::PRIVMSG(ref target, ref text) = message.command {
let nickname = connection.current_nickname();
let text = if target == nickname {
text.trim()
} else if let Some(text) = prefixed(&text, &format!("{}:", nickname)) {
text.trim()
} else {
// Message not intended for bot
return Ok(());
};
let request = Request {
message: text,
source_nickname: message.source_nickname().expect("No source nick found"), // FIXME: Error handling
source: message.source_nickname().expect("No source nick found"), // TODO: Ignore such messages
target,
};
let response = Response {
connection: &connection,
response_target: message.response_target().expect("Unknown sender"), // FIXME: Error handling
response_target: message.response_target().expect("Unknown sender"), // TODO: Ignore such messages
};
module.handle(request, response); // FIXME: Error handling
return Ok(()); // FIXME: Error handling
return module.handle(request, response);
} else {
return Ok(());
}
});
println!("Registered");
debug!("Initialized");
reactor.block_on(Future::join3(
connection_future,
connection_future.from_err(),
messages_future,
module_future,
))?;
return Ok(());
}
pub fn run(mut self) {
let config = {
let config = self.config.connection();
Config {
server: Some(config.host.to_owned()),
port: Some(config.port),
use_ssl: Some(config.use_ssl),
cert_path: config.server_cert_path.to_owned(),
client_cert_path: config.client_cert_path.to_owned(),
nickname: Some(config.nickname.to_owned()),
username: Some(config.username.to_owned()),
channels: Some(self.config.channels()),
burst_window_length: Some(config.burst_window),
max_messages_in_burst: Some(config.burst_limit),
..Default::default()
}
};
while let Err(err) = self.run_once(config.clone()) {
eprintln!("Client Error: {:?}", err);
};
}
}
fn prefixed<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
if s.starts_with(prefix) {
return Some(&s[prefix.len()..]);
} else {
return None;
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_prefixed() {
assert_eq!(prefixed("foo:bar", "foo:"), Some("bar"));
assert_eq!(prefixed("foo:", "foo:"), Some(""));
assert_eq!(prefixed("", "bar"), Some("bar"));
assert_eq!(prefixed("", ""), Some(""));
assert_eq!(prefixed("xyz:", "asd"), None);
assert_eq!(prefixed(":asd", "asd"), None);
assert_eq!(prefixed("", "asd"), None);
}
}
pub use clap::{App, AppSettings, Arg, ArgMatches, SubCommand, Values, ErrorKind as AppErrorKind};
use crate::bot::{Initializer, Module, Request, Response};
use crate::config::ModuleConfig;
use failure::Fail;
use shellwords;
pub struct Command<'a> {
request: Request<'a>,
response: Response<'a>,
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display="command error: {}", msg)]
CommandError{msg: String},
matches: ArgMatches<'a>,
#[fail(display="bot error")]
BotError(#[fail(cause)] failure::Error),
}
impl<'a> Command<'a> {
pub fn request(&self) -> &Request { return &self.request; }
pub fn response(&self) -> &Response { return &self.response; }
pub fn matches(&self) -> &ArgMatches<'a> { return &self.matches; }
pub fn subcommand(&self) -> (&str, Option<&ArgMatches<'a>>) {
return self.matches.subcommand();
impl From<failure::Error> for Error {
fn from(err: failure::Error) -> Self {
Error::BotError(err)
}
}
pub fn value_of<S: AsRef<str>>(&self, name: S) -> Option<&str> {
return self.matches.value_of(name);
}
pub struct Command<'a> {
/// The message text (with prefix removed)
pub message: &'a str,
pub fn values_of<S: AsRef<str>>(&'a self, name: S) -> Option<Values<'a>> {
return self.matches.values_of(name);
}
/// The nickname who sent the message
pub source: &'a str,
pub fn source(&self) -> &str {
return self.request.source_nickname();
}
/// The target nickname or channel of the message
pub target: &'a str,
/// Direct message to bot
pub direct: bool,
/// The matched command line
pub matches: ArgMatches<'a>,
}
pub trait CommandModule {
pub trait CommandModule where Self: std::marker::Sized {
type Config: ModuleConfig;
fn init(config: &Self::Config, initializer: &mut Initializer) -> Self; // FIXME: Error handling
fn init(config: &Self::Config, initializer: &mut Initializer) -> Result<Self, failure::Error>;
fn register<'app_a, 'app_b>(&self, app: App<'app_a, 'app_b>) -> App<'app_a, 'app_b>; // FIXME: Develop better interface to define app
fn exec(&mut self, command: Command);
fn exec(&mut self, command: Command, response: &Response) -> Result<(), Error>;
}
impl<M> Module for M
where M: CommandModule {
type Config = M::Config;
fn init(config: &Self::Config, initializer: &mut Initializer) -> Self {
return M::init(config, initializer);
fn init(config: &Self::Config, initializer: &mut Initializer) -> Result<Self, failure::Error> {
return M::init(config, initializer)
}
fn handle(&mut self, request: Request, response: Response) {
fn handle(&mut self, request: Request, response: Response) -> Result<(), failure::Error> {
let source = request.source();
let (message, direct) = if request.target() == source {
(request.message().trim(), true)
} else if let Some(message) = prefixed(&request.message(), &format!("{}:", source)) {
(message.trim(), false)
} else {
// Message not intended for bot
return Ok(());
};
if let Ok(args) = shellwords::split(request.message()) { // FIXME: Error handling of mismatched quotes
let app = App::new("")
.bin_name("FIXME") // FIXME: Use right name
.bin_name("FIXME") // FIXME: Use right name and set width to max line length
.setting(AppSettings::ArgRequiredElseHelp)
.setting(AppSettings::DisableVersion)
.setting(AppSettings::DeriveDisplayOrder)
......@@ -66,23 +81,62 @@ impl<M> Module for M
// Parse args and dispatch
match app.get_matches_from_safe(args) {
Ok(matches) => {
self.exec(Command {
request,
response,
let result = self.exec(Command {
message,
source,
target: request.target(),
direct,
matches,
});
}, &response);
match result {
Err(Error::CommandError {msg}) => {
response.reply(&format!("Error: {}", &msg))?;
}
Err(Error::BotError(err)) => return Err(err),
Ok(result) => return Ok(result),
}
}
Err(err) => {
match err.kind {
// TODO: Show help only for query - send a one-line answer to channels
AppErrorKind::HelpDisplayed => response.send(
request.source_nickname(),
request.source(),
&err.message
),
)?,
_ => response.reply(&format!("Error: {}", &err.message)),
}
_ => response.reply(&format!("Error: {}", &err.message))?,
};
}
}
};
}
return Ok(());
}
}
fn prefixed<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
if s.starts_with(prefix) {
return Some(&s[prefix.len()..]);
} else {
return None;
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_prefixed() {
assert_eq!(prefixed("foo:bar", "foo:"), Some("bar"));
assert_eq!(prefixed("foo:", "foo:"), Some(""));
assert_eq!(prefixed("", "bar"), Some("bar"));
assert_eq!(prefixed("", ""), Some(""));
assert_eq!(prefixed("xyz:", "asd"), None);
assert_eq!(prefixed(":asd", "asd"), None);
assert_eq!(prefixed("", "asd"), None);
}
}
pub use crate::bootstrap::bootstrap;
pub use crate::bot::{Bot, Client, Initializer, Request, Response};
pub use crate::cmd::{App, AppSettings, Arg, ArgMatches, Command, CommandModule, SubCommand, Values};
pub use crate::cmd::{App, AppSettings, Arg, ArgMatches, Command, CommandModule, SubCommand, Values, Error as CmdError};
pub use crate::config::{ConnectionConfig, ModuleConfig};
pub use crate::store::Store;