diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3f4e93f5ea49f5178ff0f424682cfb436f04500e
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,19 @@
+image: "rust:latest"
+
+before_script:
+  - rustup component add rustfmt
+  - rustup component add clippy
+  - cargo install cargo-deb
+
+test:
+  script:
+    - cargo fmt -- --check
+    - cargo clippy --all-targets --all-features -- -D warnings
+    - cargo test
+
+build:
+  script:
+    - cargo deb
+  artifacts:
+    paths:
+      - target/debian/*.deb
diff --git a/Cargo.lock b/Cargo.lock
index c6fcb43f71f797f5345fda78890a168849c3436e..ab6188dd69e175688d8df09bab5aba45d1d0fa63 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -103,7 +103,7 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
 
 [[package]]
 name = "cat-disruptor-6500"
-version = "0.1.1"
+version = "0.1.2"
 dependencies = [
  "bigdecimal",
  "diesel",
diff --git a/Cargo.toml b/Cargo.toml
index 316909dc15d4db2e979d0d599ec2032f1cb828cc..c738368f406adb776162ba684964814c61dcc44e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "cat-disruptor-6500"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Stephen <stephen@stephendownward.ca>"]
 edition = "2018"
 
diff --git a/rustfmt.toml b/rustfmt.toml
index 230e0ae89494351df23c454dbe3793de06fbc531..3be9fc2661eac1f064b07b34b820549b2247da0e 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,2 +1,3 @@
 tab_spaces = 4
-hard_tabs = true
\ No newline at end of file
+hard_tabs = true
+edition = "2018"
diff --git a/src/handlers/joke.rs b/src/handlers/joke.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7fc73ac6633b5e9d2c881213abcf91b7c52bd2c8
--- /dev/null
+++ b/src/handlers/joke.rs
@@ -0,0 +1,29 @@
+use crate::handlers::LineHandler;
+use crate::joker::tell_joke;
+use serenity::async_trait;
+use serenity::model::channel::Message;
+use serenity::prelude::*;
+
+pub struct JokeHandler;
+
+#[async_trait]
+impl LineHandler for JokeHandler {
+	async fn line(&self, ctx: &Context, msg: &Message, line: &str) {
+		if line == "!JOKE" {
+			match tell_joke().await {
+				Some(s) => msg.channel_id.say(&ctx, s).await.unwrap(),
+				None => msg
+					.channel_id
+					.say(&ctx, "There was an error while fetching a joke.")
+					.await
+					.unwrap(),
+			};
+		}
+	}
+}
+
+impl Default for JokeHandler {
+	fn default() -> Self {
+		Self
+	}
+}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..730a63e8b2d6a1d2130b04a4c0fc8bdb0aa05827
--- /dev/null
+++ b/src/handlers/mod.rs
@@ -0,0 +1,52 @@
+mod joke;
+mod react;
+mod xbasic;
+
+use crate::handlers::joke::*;
+use crate::handlers::react::*;
+use crate::handlers::xbasic::*;
+
+use serenity::async_trait;
+use serenity::model::channel::Message;
+use serenity::model::prelude::Ready;
+use serenity::prelude::*;
+
+#[async_trait]
+pub(crate) trait LineHandler: Send + Sync {
+	async fn message(&self, ctx: &Context, msg: &Message) {
+		for line in msg.content.split('\n') {
+			self.line(ctx, msg, line).await
+		}
+	}
+
+	async fn line(&self, _ctx: &Context, _msg: &Message, _line: &str) {}
+}
+
+pub(crate) struct Dispatcher {
+	handlers: Vec<Box<dyn LineHandler>>,
+}
+
+#[async_trait]
+impl EventHandler for Dispatcher {
+	async fn message(&self, ctx: Context, msg: Message) {
+		for h in &self.handlers {
+			h.message(&ctx, &msg).await;
+		}
+	}
+
+	async fn ready(&self, _: Context, ready: Ready) {
+		println!("{} is connected", ready.user.name);
+	}
+}
+
+impl Default for Dispatcher {
+	fn default() -> Self {
+		Self {
+			handlers: vec![
+				Box::new(XbasicHandler::default()),
+				Box::new(JokeHandler::default()),
+				Box::new(ReactHandler::default()),
+			],
+		}
+	}
+}
diff --git a/src/handlers/react.rs b/src/handlers/react.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b40159761f0ac24423ddcd7b7fe27f99a48abd14
--- /dev/null
+++ b/src/handlers/react.rs
@@ -0,0 +1,48 @@
+use crate::handlers::LineHandler;
+use phf::phf_map;
+use serenity::async_trait;
+use serenity::model::channel::{Message, ReactionType};
+use serenity::prelude::*;
+use std::str::FromStr;
+
+static EMOJI_MAP: phf::Map<&'static str, &'static str> = phf_map! {
+	"cat" => "🐈",
+	"chicken" => "🐔",
+	"spaghetti" => "🍝",
+	"dog" => "🐕",
+	"bot" => "🤖",
+	"mango" => "🥭",
+	"banana" => "🍌",
+	"bee" => "🐝"
+};
+
+pub struct ReactHandler;
+
+#[async_trait]
+impl LineHandler for ReactHandler {
+	async fn message(&self, ctx: &Context, msg: &Message) {
+		for (key, value) in EMOJI_MAP.entries() {
+			let msg_lower = format!(" {} ", msg.content.to_lowercase());
+			if msg_lower.contains(&format!(" {} ", key))
+				|| msg_lower.contains(&format!(" {}s ", key))
+			{
+				let reaction_type = match ReactionType::from_str(value) {
+					Ok(x) => x,
+					Err(x) => {
+						println!("Could not react: {}", x);
+						return;
+					}
+				};
+				if let Err(e) = msg.react(&ctx, reaction_type).await {
+					println!("Error reacting: {}", e);
+				}
+			}
+		}
+	}
+}
+
+impl Default for ReactHandler {
+	fn default() -> Self {
+		Self
+	}
+}
diff --git a/src/handler.rs b/src/handlers/xbasic.rs
similarity index 83%
rename from src/handler.rs
rename to src/handlers/xbasic.rs
index fb79bc8c04c0570c5a6b1f8f7940b98c3a477a00..90474747d159de2fa3a1e0c8f3400c570369e58e 100644
--- a/src/handler.rs
+++ b/src/handlers/xbasic.rs
@@ -1,16 +1,16 @@
 use crate::framebuffer::FrameBuffer;
-use crate::joker::tell_joke;
+use crate::handlers::LineHandler;
 use crate::program::Program;
-use diesel::PgConnection;
-use phf::phf_map;
+use diesel::{Connection, PgConnection};
+use dotenv::dotenv;
 use serenity::async_trait;
 use serenity::http::AttachmentType;
 use serenity::model::channel::{Message, ReactionType};
 use serenity::model::id::UserId;
-use serenity::model::prelude::Ready;
 use serenity::prelude::*;
 use std::borrow::{Borrow, Cow};
 use std::collections::HashMap;
+use std::env;
 use std::str::FromStr;
 use std::sync::{Arc, Mutex};
 use tokio::task;
@@ -18,24 +18,6 @@ use xbasic::basic_io::BasicIO;
 use xbasic::expr::ExprValue;
 use xbasic::xbasic::XBasicBuilder;
 
-// TODO - this file should be broken into 4 modules
-// 1 that takes input messages and redirects them appropriately
-// 1 that handles reacts
-// 1 that handles the interpreter
-// 1 that handles jokes
-// Right now it's a mess
-
-static EMOJI_MAP: phf::Map<&'static str, &'static str> = phf_map! {
-"cat" => "🐈",
-"chicken" => "🐔",
-"spaghetti" => "🍝",
-"dog" => "🐕",
-"bot" => "🤖",
-"mango" => "🥭",
-"banana" => "🍌",
-"bee" => "🐝"
-};
-
 struct DiscordIO {
 	s: String,
 	frame: Option<FrameBuffer>,
@@ -66,19 +48,12 @@ macro_rules! get_user_programs {
 	};
 }
 
-pub(crate) struct Handler {
+pub struct XbasicHandler {
 	programs: Arc<Mutex<HashMap<UserId, Program>>>,
 	conn: Arc<Mutex<PgConnection>>,
 }
 
-impl Handler {
-	pub fn new(conn: PgConnection) -> Self {
-		Self {
-			programs: Arc::new(Mutex::new(HashMap::new())),
-			conn: Arc::new(Mutex::new(conn)),
-		}
-	}
-
+impl XbasicHandler {
 	async fn interpret_line(&self, msg: &Message, ctx: &Context, line: &str) {
 		// TODO we lock the mutex to check, but unlock before locking again later
 		// allows another thread to screw it up
@@ -183,19 +158,8 @@ impl Handler {
 			}
 		}
 
-		match line {
-			"!START" => self.interpreter_start(msg),
-			"!JOKE" => {
-				match tell_joke().await {
-					Some(s) => msg.channel_id.say(&ctx, s).await.unwrap(),
-					None => msg
-						.channel_id
-						.say(&ctx, "There was an error while fetching a joke.")
-						.await
-						.unwrap(),
-				};
-			} // FIXME
-			_ => {}
+		if line == "!START" {
+			self.interpreter_start(msg);
 		}
 	}
 
@@ -282,7 +246,7 @@ impl Handler {
 
 	async fn load_published_program(&self, msg: &Message, ctx: &Context, id: i32) -> Option<()> {
 		let name = get_user_programs!(&self, &msg.author.id)
-			.load_published_program(&self.conn.lock().ok()?.borrow(), id)?;
+			.load_published_program(self.conn.lock().ok()?.borrow(), id)?;
 
 		msg.channel_id
 			.say(&ctx, format!("Loaded {} (\"{}\") into memory.", id, name))
@@ -441,32 +405,24 @@ impl Handler {
 }
 
 #[async_trait]
-impl EventHandler for Handler {
-	async fn message(&self, ctx: Context, msg: Message) {
-		for (key, value) in EMOJI_MAP.entries() {
-			let msg_lower = format!(" {} ", msg.content.to_lowercase());
-			if msg_lower.contains(&format!(" {} ", key))
-				|| msg_lower.contains(&format!(" {}s ", key))
-			{
-				let reaction_type = match ReactionType::from_str(value) {
-					Ok(x) => x,
-					Err(x) => {
-						println!("Could not react: {}", x);
-						return;
-					}
-				};
-				if let Err(e) = msg.react(&ctx, reaction_type).await {
-					println!("Error reacting: {}", e);
-				}
-			}
-		}
+impl LineHandler for XbasicHandler {
+	async fn line(&self, ctx: &Context, msg: &Message, line: &str) {
+		self.interpret_line(msg, ctx, line).await;
+	}
+}
 
-		for line in msg.content.split('\n') {
-			self.interpret_line(&msg, &ctx, line).await;
+impl Default for XbasicHandler {
+	fn default() -> Self {
+		Self {
+			programs: Arc::new(Mutex::new(HashMap::new())),
+			conn: Arc::new(Mutex::new(establish_connection())),
 		}
 	}
+}
 
-	async fn ready(&self, _: Context, ready: Ready) {
-		println!("{} is connected", ready.user.name);
-	}
+fn establish_connection() -> PgConnection {
+	dotenv().ok();
+
+	let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
+	PgConnection::establish(&database_url).expect("Error connecting to database")
 }
diff --git a/src/main.rs b/src/main.rs
index f748286586088c1c882f8a597e9ec86904597cd1..11cc71d446e2da1954d8ce6c7cb281a65be1640c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,34 +3,23 @@ extern crate diesel;
 
 mod config;
 mod framebuffer;
-mod handler;
+mod handlers;
 mod joker;
 mod models;
 mod program;
 mod schema;
 
-use crate::handler::Handler;
-use diesel::{Connection, PgConnection};
-use dotenv::dotenv;
-use serenity::Client;
-use std::env;
-
-fn establish_connection() -> PgConnection {
-	dotenv().ok();
+use crate::handlers::Dispatcher;
 
-	let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
-	PgConnection::establish(&database_url).expect("Error connecting to database")
-}
+use serenity::Client;
 
 #[tokio::main]
 async fn main() {
 	let config = config::get_conf();
 	let token = config.token;
 
-	let conn = establish_connection();
-
 	let mut client = Client::builder(&token)
-		.event_handler(Handler::new(conn))
+		.event_handler(Dispatcher::default())
 		.await
 		.expect("Error creating client");
 	if let Err(e) = client.start().await {