diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0790fd8042775e68268ce93eb792fba29b59fcb2
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,37 @@
+use serenity;
+
+pub static DISCORD_TOKEN: &str = include_str!("discord_token");
+
+pub static SERVER_ID: u64 = 606351521117896704;
+// #general
+pub static MAIN_CHANNEL: serenity::model::id::ChannelId =
+    serenity::model::id::ChannelId(606351521117896706);
+// #the-corner
+pub static WELCOME_CHANNEL: serenity::model::id::ChannelId =
+    serenity::model::id::ChannelId(606351613816209418);
+// #general
+pub static ANNOUNCEMENT_CHANNEL: serenity::model::id::ChannelId =
+    serenity::model::id::ChannelId(606351521117896706);
+
+pub static BOT_ID: u64 = 607078903969742848;
+
+pub static VOTE_POOL_SIZE: i8 = 2;
+pub static VOTE_ROLE: u64 = 607478818038480937;
+pub static TIEBREAKER_ROLE: u64 = 607509283483025409;
+pub static UNREGISTERED_MEMBER_ROLE: u64 = 608282247350714408;
+pub static REGISTERED_MEMBER_ROLE: u64 = 608282133118582815;
+
+pub static FOR_VOTE: &str = "👍";
+pub static AGAINST_VOTE: &str = "👎";
+pub static ABSTAIN_VOTE: &str = "🙊";
+pub static APPROVE_REACT: &str = "⬆";
+pub static DISAPPROVE_REACT: &str = "⬇";
+pub static UNSURE_REACT: &str = "❔";
+pub static ALLOWED_REACTS: &[&'static str] = &[
+    FOR_VOTE,
+    AGAINST_VOTE,
+    ABSTAIN_VOTE,
+    APPROVE_REACT,
+    DISAPPROVE_REACT,
+    UNSURE_REACT,
+];
diff --git a/src/main.rs b/src/main.rs
index 2729df1b811a3460a68ad081eab8a21ece92d502..c14064f4b4b20497ca1ef8902ccdb43940bc7a46 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,43 +4,9 @@ use serenity::{
     utils::MessageBuilder,
 };
 
-use rand::Rng;
-
-static DISCORD_TOKEN: &str = include_str!("discord_token");
-
-static SERVER_ID: u64 = 606351521117896704;
-// #general
-static MAIN_CHANNEL: serenity::model::id::ChannelId =
-    serenity::model::id::ChannelId(606351521117896706);
-// #the-corner
-static WELCOME_CHANNEL: serenity::model::id::ChannelId =
-    serenity::model::id::ChannelId(606351613816209418);
-// #general
-static ANNOUNCEMENT_CHANNEL: serenity::model::id::ChannelId =
-    serenity::model::id::ChannelId(606351521117896706);
-
-static BOT_ID: u64 = 607078903969742848;
-
-static VOTE_POOL_SIZE: i8 = 2;
-static VOTE_ROLE: u64 = 607478818038480937;
-static TIEBREAKER_ROLE: u64 = 607509283483025409;
-static UNREGISTERED_MEMBER_ROLE: u64 = 608282247350714408;
-static REGISTERED_MEMBER_ROLE: u64 = 608282133118582815;
-
-static FOR_VOTE: &str = "👍";
-static AGAINST_VOTE: &str = "👎";
-static ABSTAIN_VOTE: &str = "🙊";
-static APPROVE_REACT: &str = "⬆";
-static DISAPPROVE_REACT: &str = "⬇";
-static UNSURE_REACT: &str = "❔";
-static ALLOWED_REACTS: &[&'static str] = &[
-    FOR_VOTE,
-    AGAINST_VOTE,
-    ABSTAIN_VOTE,
-    APPROVE_REACT,
-    DISAPPROVE_REACT,
-    UNSURE_REACT,
-];
+mod config;
+mod user_management;
+mod voting;
 
 macro_rules! e {
     ($error: literal, $x:expr) => {
@@ -60,215 +26,54 @@ impl EventHandler for Handler {
     // Event handlers are dispatched through a threadpool, and so multiple
     // events can be dispatched simultaneously.
     fn message(&self, ctx: Context, msg: Message) {
-        if msg.author.id.0 == 159652921083035648 {
-            let mut rng = rand::thread_rng();
-            let mut message = MessageBuilder::new();
-            message.push(
-                [
-                    "That's quite enough from you ",
-                    "Why do you continue to bother us ",
-                    "Oh. It's you again ",
-                    "What are you doing ",
-                ][rng.gen_range(0, 4)],
-            );
-            message.mention(&msg.author);
-            if let Err(why) = msg.channel_id.say(&ctx.http, message.build()) {
-                println!("Error sending message: {:?}", why);
-            }
-        }
-
-        let message_content: Vec<_> = msg.content.splitn(2, ' ').collect();
-        match message_content[0] {
-            "!join" => {
-                e!(
-                    "Unable to get user: {:?}",
-                    serenity::model::id::GuildId(SERVER_ID)
-                        .member(ctx.http.clone(), msg.author.id)
-                        .map(|member| new_member(&ctx, member))
-                );
-            }
-            "!move" => {
-                let motion = message_content[1];
-                if motion.len() > 0 {
-                    create_motion(&ctx, &msg, motion);
-                } else {
-                    e!("Error sending message: {:?}",
-                       msg.channel_id.say(
-                        &ctx.http,
-                        "If there's something you want to motion, put it after the !move keyword",
-                    ));
+        if msg.content.starts_with("!") {
+            let message_content: Vec<_> = msg.content[1..].splitn(2, ' ').collect();
+            match message_content[0] {
+                "register" => {
+                    user_management::Commands::register(ctx, msg.clone(), message_content[1])
                 }
-            }
-            "!motion" => {
-                e!("Error sending message: {:?}",
-                   msg.channel_id.say(
-                    &ctx.http,
-                    "I hope you're not having a motion. You may have wanted to !move something instead."
-                ));
-            }
-            "!poll" => {
-                let topic = message_content[1];
-                if topic.len() > 0 {
-                    create_poll(&ctx, &msg, topic);
-                } else {
-                    e!("Error sending message: {:?}",
-                       msg.channel_id.say(
-                        &ctx.http,
-                        "If there's something you want to motion, put it after the !move keyword",
-                    ));
+                "join" => {
+                    user_management::Commands::join(ctx, msg.clone(), message_content[1]);
                 }
-            }
-            "!register" => {
-                let name = message_content[1];
-                if name.len() > 0 {
+                "move" => {
+                    voting::Commands::move_something(ctx, msg.clone(), message_content[1]);
+                }
+                "motion" => {
+                    voting::Commands::motion(ctx, msg.clone(), message_content[1]);
+                }
+                "poll" => {
+                    voting::Commands::poll(ctx, msg.clone(), message_content[1]);
+                }
+                "cowsay" => {
+                    voting::Commands::cowsay(ctx, msg.clone(), message_content[1]);
+                }
+                "help" => {
+                    let mut message = MessageBuilder::new();
+                    message.push_line("Use !move <action> to make a circular motion");
+                    message
+                        .push_line("Use !poll <proposal> to see what people think about something");
                     e!(
-                        "Unable to get member: {:?}",
-                        serenity::model::id::GuildId(SERVER_ID)
-                            .member(ctx.http.clone(), msg.author.id)
-                            .map(|mut member| {
-                                e!(
-                                    "Unable to remove role: {:?}",
-                                    member.remove_role(&ctx.http, UNREGISTERED_MEMBER_ROLE)
-                                );
-                                e!(
-                                    "Unable to edit nickname: {:?}",
-                                    member
-                                        .edit(&ctx.http, |m| {
-                                            let mut rng = rand::thread_rng();
-                                            m.nickname(format!(
-                                                "{}, {}",
-                                                name,
-                                                [
-                                                    "The Big Cheese",
-                                                    "The One and Only",
-                                                    "The Exalted One",
-                                                    "not to be trusted",
-                                                    "The Scoundrel",
-                                                    "A big fish in a small pond",
-                                                ][rng.gen_range(0, 5)]
-                                            ));
-                                            m
-                                        })
-                                        .map(|()| {
-                                            e!(
-                                                "Unable to add role: {:?}",
-                                                member.add_role(&ctx.http, REGISTERED_MEMBER_ROLE)
-                                            );
-                                        })
-                                );
-                            })
+                        "Error sending message: {:?}",
+                        msg.channel_id.say(&ctx.http, message.build())
                     );
-                    e!("Error deleting register message: {:?}", msg.delete(ctx));
-                } else {
+                }
+                _ => {
                     e!(
                         "Error sending message: {:?}",
                         msg.channel_id
-                            .say(&ctx.http, "Usage: !register <ucc username>")
+                            .say(&ctx.http, "Unrecognised command. Try !help")
                     );
                 }
             }
-            "!cowsay" => {
-                let mut text = message_content[1].to_owned();
-                text.escape_default();
-                // Guess what buddy! You definitely are passing a string to cowsay
-                text.insert(0, '\'');
-                text.insert(text.len(), '\'');
-                let output = std::process::Command::new("cowsay")
-                    .arg(text)
-                    .output()
-                    // btw, if we can't execute cowsay we crash
-                    .expect("failed to execute cowsay");
-                let mut message = MessageBuilder::new();
-                message.push_codeblock(
-                    String::from_utf8(output.stdout).expect("unable to parse stdout to String"),
-                    None,
-                );
-                e!(
-                    "Error sending message: {:?}",
-                    msg.channel_id.say(&ctx.http, message.build())
-                );
-            }
-            "!help" => {
-                let mut message = MessageBuilder::new();
-                message.push_line("Use !move <action> to make a circular motion");
-                message.push_line("Use !poll <proposal> to see what people think about something");
-                e!(
-                    "Error sending message: {:?}",
-                    msg.channel_id.say(&ctx.http, message.build())
-                );
-            }
-            _ => {}
         }
     }
 
     fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) {
-        match add_reaction.message(&ctx.http) {
-            Ok(mut message) => {
-                if message.author.id.0 == BOT_ID {
-                    if let Ok(user) = add_reaction.user(&ctx) {
-                        match user.has_role(&ctx, SERVER_ID, VOTE_ROLE) {
-                            Ok(true) => {
-                                for react in [FOR_VOTE, AGAINST_VOTE, ABSTAIN_VOTE]
-                                    .iter()
-                                    .filter(|r| r != &&add_reaction.emoji.as_data().as_str())
-                                {
-                                    for a_user in
-                                        message.reaction_users(&ctx, *react, None, None).unwrap()
-                                    {
-                                        if a_user.id.0 == user.id.0 {
-                                            if let Err(why) = add_reaction.delete(&ctx) {
-                                                println!("Error deleting react: {:?}", why);
-                                            };
-                                        }
-                                    }
-                                }
-                                if !ALLOWED_REACTS.contains(&add_reaction.emoji.as_data().as_str())
-                                {
-                                    if let Err(why) = add_reaction.delete(&ctx) {
-                                        println!("Error deleting react: {:?}", why);
-                                    };
-                                }
-                                if user.id.0 != BOT_ID {
-                                    update_motion(&ctx, &mut message, &user, "add", add_reaction);
-                                }
-                            }
-                            Ok(false) => {
-                                if user.id.0 != BOT_ID {
-                                    if ![APPROVE_REACT, DISAPPROVE_REACT]
-                                        .contains(&add_reaction.emoji.as_data().as_str())
-                                    {
-                                        if let Err(why) = add_reaction.delete(&ctx) {
-                                            println!("Error deleting react: {:?}", why);
-                                        };
-                                    }
-                                }
-                            }
-                            Err(why) => {
-                                println!("Error getting user role: {:?}", why);
-                            }
-                        }
-                    }
-                }
-            }
-            Err(why) => {
-                println!("Error processing react: {:?}", why);
-            }
-        }
+        voting::reaction_add(ctx, add_reaction);
     }
 
     fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) {
-        match removed_reaction.message(&ctx.http) {
-            Ok(mut message) => {
-                if message.author.id.0 == BOT_ID {
-                    if let Ok(user) = removed_reaction.user(&ctx) {
-                        update_motion(&ctx, &mut message, &user, "remove", removed_reaction);
-                    }
-                }
-            }
-            Err(why) => {
-                println!("Error getting user role: {:?}", why);
-            }
-        }
+        voting::reaction_remove(ctx, removed_reaction);
     }
 
     fn guild_member_addition(
@@ -277,7 +82,7 @@ impl EventHandler for Handler {
         _guild_id: serenity::model::id::GuildId,
         the_new_member: Member,
     ) {
-        new_member(&ctx, the_new_member);
+        user_management::new_member(&ctx, the_new_member);
     }
 
     // Set a handler to be called on the `ready` event. This is called when a
@@ -293,7 +98,7 @@ impl EventHandler for Handler {
 
 fn main() {
     // Configure the client with your Discord bot token in the environment.
-    let token = DISCORD_TOKEN;
+    let token = config::DISCORD_TOKEN;
 
     // Create a new instance of the Client, logging in as a bot. This will
     // automatically prepend your bot token with "Bot ", which is a requirement
@@ -308,245 +113,3 @@ fn main() {
         println!("Client error: {:?}", why);
     }
 }
-
-fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
-    println!("{} created a motion {}", msg.author.name, topic);
-    match msg.channel_id.send_message(&ctx.http, |m| {
-        m.embed(|embed| {
-            embed.author(|a| {
-                a.name(&msg.author.name);
-                a.icon_url(
-                    msg.author
-                        .static_avatar_url()
-                        .expect("Expected author to have avatar"),
-                );
-                a
-            });
-            embed.colour(serenity::utils::Colour::GOLD);
-            embed.title(format!("Motion to {}", topic));
-            let mut desc = MessageBuilder::new();
-            desc.role(VOTE_ROLE);
-            desc.push(" take a look at this motion from ");
-            desc.mention(&msg.author);
-            embed.description(desc.build());
-            embed.field("Status", "Under Consideration", true);
-            embed.field("Votes", "For: 0\nAgainst: 0\nAbstain: 0", true);
-            embed.timestamp(msg.timestamp.to_rfc3339());
-            embed
-        });
-        m.reactions(vec![
-            FOR_VOTE,
-            AGAINST_VOTE,
-            ABSTAIN_VOTE,
-            APPROVE_REACT,
-            DISAPPROVE_REACT,
-        ]);
-        m
-    }) {
-        Err(why) => {
-            println!("Error sending message: {:?}", why);
-        }
-        Ok(_) => {
-            if let Err(why) = msg.delete(ctx) {
-                println!("Error deleting motion prompt: {:?}", why);
-            }
-        }
-    }
-}
-
-fn create_poll(ctx: &Context, msg: &Message, topic: &str) {
-    println!("{} created a poll {}", msg.author.name, topic);
-    match msg.channel_id.send_message(&ctx.http, |m| {
-        m.embed(|embed| {
-            embed.author(|a| {
-                a.name(&msg.author.name);
-                a.icon_url(
-                    msg.author
-                        .static_avatar_url()
-                        .expect("Expected author to have avatar"),
-                );
-                a
-            });
-            embed.colour(serenity::utils::Colour::BLUE);
-            embed.title(format!("Poll {}", topic));
-            let mut desc = MessageBuilder::new();
-            desc.mention(&msg.author);
-            desc.push(" wants to know what you think.");
-            embed.description(desc.build());
-            embed.timestamp(msg.timestamp.to_rfc3339());
-            embed
-        });
-        m.reactions(vec![APPROVE_REACT, DISAPPROVE_REACT, UNSURE_REACT]);
-        m
-    }) {
-        Err(why) => {
-            println!("Error sending message: {:?}", why);
-        }
-        Ok(_) => {
-            if let Err(why) = msg.delete(ctx) {
-                println!("Error deleting motion prompt: {:?}", why);
-            }
-        }
-    }
-}
-
-fn update_motion(
-    ctx: &Context,
-    msg: &mut Message,
-    user: &serenity::model::user::User,
-    change: &str,
-    reaction: channel::Reaction,
-) {
-    let for_votes = msg.reaction_users(ctx, FOR_VOTE, None, None).unwrap().len() as isize - 1;
-    let against_votes = msg
-        .reaction_users(ctx, AGAINST_VOTE, None, None)
-        .unwrap()
-        .len() as isize
-        - 1;
-    let abstain_votes = msg
-        .reaction_users(ctx, ABSTAIN_VOTE, None, None)
-        .unwrap()
-        .len() as isize
-        - 1;
-
-    let strength_buff = |react: &str| {
-        msg.reaction_users(ctx, react, None, None)
-            .unwrap()
-            .iter()
-            .filter(|u| match u.has_role(ctx, SERVER_ID, TIEBREAKER_ROLE) {
-                Ok(true) => true,
-                _ => false,
-            })
-            .count()
-            > 0
-    };
-
-    let for_strength = for_votes as f32 + (if strength_buff(FOR_VOTE) { 0.5 } else { 0.0 });
-    let against_strength = against_votes as f32
-        + (if strength_buff(AGAINST_VOTE) {
-            0.5
-        } else {
-            0.0
-        });
-    let abstain_strength = abstain_votes as f32
-        + (if strength_buff(ABSTAIN_VOTE) {
-            0.5
-        } else {
-            0.0
-        });
-
-    let old_embed = msg.embeds[0].clone();
-    let topic = old_embed.clone().title.unwrap();
-
-    println!(
-        "  {:10} {:6} {} on {}",
-        user.name,
-        change,
-        reaction.emoji.as_data().as_str(),
-        topic
-    );
-
-    let update_status = |e: &mut serenity::builder::CreateEmbed,
-                         status: &str,
-                         last_status_full: String,
-                         topic: &str| {
-        let last_status = last_status_full.lines().next().expect("No previous status");
-        if last_status == status {
-            e.field("Status", last_status_full, true);
-        } else {
-            e.field(
-                "Status",
-                format!("{}\n_was_ {}", status, last_status_full),
-                true,
-            );
-            println!("Motion to {} now {}", topic, status);
-            //
-            let mut message = MessageBuilder::new();
-            message.push_bold(topic);
-            message.push(" is now ");
-            message.push_bold(status);
-            message.push_italic(format!(" (was {})", last_status));
-            if let Err(why) = ANNOUNCEMENT_CHANNEL.say(&ctx.http, message.build()) {
-                println!("Error sending message: {:?}", why);
-            };
-        }
-    };
-
-    if let Err(why) = msg.edit(ctx, |m| {
-        m.embed(|e| {
-            e.author(|a| {
-                let old_author = old_embed.clone().author.expect("Expected author in embed");
-                a.name(old_author.name);
-                a.icon_url(
-                    old_author
-                        .icon_url
-                        .expect("Expected embed author to have icon"),
-                );
-                a
-            });
-            e.title(&topic);
-            e.description(old_embed.description.unwrap());
-            let last_status_full = old_embed
-                .fields
-                .iter()
-                .filter(|f| f.name == "Status")
-                .next()
-                .expect("No previous status")
-                .clone()
-                .value;
-            if for_strength > (VOTE_POOL_SIZE / 2) as f32 {
-                e.colour(serenity::utils::Colour::TEAL);
-                update_status(e, "Passed", last_status_full, &topic);
-            } else if against_strength + abstain_strength > (VOTE_POOL_SIZE / 2) as f32 {
-                e.colour(serenity::utils::Colour::RED);
-                update_status(e, "Failed", last_status_full, &topic);
-            } else {
-                e.colour(serenity::utils::Colour::GOLD);
-                update_status(e, "Under Consideration", last_status_full, &topic);
-            }
-            e.field(
-                format!(
-                    "Votes ({}/{})",
-                    for_votes + against_votes + abstain_votes,
-                    VOTE_POOL_SIZE
-                ),
-                format!(
-                    "For: {}\nAgainst: {}\nAbstain: {}",
-                    for_votes, against_votes, abstain_votes
-                ),
-                true,
-            );
-            e.timestamp(
-                old_embed
-                    .timestamp
-                    .expect("Expected embed to have timestamp"),
-            );
-            e
-        })
-    }) {
-        println!("Error updating motion: {:?}", why);
-    }
-}
-
-fn new_member(ctx: &Context, mut new_member: Member) {
-    let mut message = MessageBuilder::new();
-    message.push("Nice to see you here ");
-    message.mention(&new_member);
-    message.push_line("! Would you care to introduce yourself?");
-    message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…");
-    message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide");
-    if let Err(why) = WELCOME_CHANNEL.say(&ctx, message.build()) {
-        println!("Error sending message: {:?}", why);
-    }
-
-    let mut message = MessageBuilder::new();
-    message.push(format!("Say hi to {} in ", new_member.display_name()));
-    message.mention(&WELCOME_CHANNEL);
-    if let Err(why) = MAIN_CHANNEL.say(&ctx, message.build()) {
-        println!("Error sending message: {:?}", why);
-    }
-
-    if let Err(why) = new_member.add_role(&ctx.http, UNREGISTERED_MEMBER_ROLE) {
-        println!("Error adding user role: {:?}", why);
-    };
-}
diff --git a/src/user_management.rs b/src/user_management.rs
new file mode 100644
index 0000000000000000000000000000000000000000..be715e3e7f4d55c694f140a8e8b725dc3f361473
--- /dev/null
+++ b/src/user_management.rs
@@ -0,0 +1,101 @@
+use rand::Rng;
+use serenity::{
+    model::{channel::Message, guild::Member},
+    prelude::*,
+    utils::MessageBuilder,
+};
+
+use crate::config;
+
+macro_rules! e {
+    ($error: literal, $x:expr) => {
+        match $x {
+            Ok(_) => (),
+            Err(why) => eprintln!($error, why),
+        }
+    };
+}
+
+pub fn new_member(ctx: &Context, mut new_member: Member) {
+    let mut message = MessageBuilder::new();
+    message.push("Nice to see you here ");
+    message.mention(&new_member);
+    message.push_line("! Would you care to introduce yourself?");
+    message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…");
+    message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide, and try to avoid defamatory content");
+    if let Err(why) = config::WELCOME_CHANNEL.say(&ctx, message.build()) {
+        println!("Error sending message: {:?}", why);
+    }
+
+    let mut message = MessageBuilder::new();
+    message.push(format!("Say hi to {} in ", new_member.display_name()));
+    message.mention(&config::WELCOME_CHANNEL);
+    if let Err(why) = config::MAIN_CHANNEL.say(&ctx, message.build()) {
+        println!("Error sending message: {:?}", why);
+    }
+
+    if let Err(why) = new_member.add_role(&ctx.http, config::UNREGISTERED_MEMBER_ROLE) {
+        println!("Error adding user role: {:?}", why);
+    };
+}
+
+pub struct Commands;
+impl Commands {
+    pub fn join(ctx: Context, msg: Message, _content: &str) {
+        e!(
+            "Unable to get user: {:?}",
+            serenity::model::id::GuildId(config::SERVER_ID)
+                .member(ctx.http.clone(), msg.author.id)
+                .map(|member| new_member(&ctx, member))
+        );
+    }
+    pub fn register(ctx: Context, msg: Message, content: &str) {
+        let name = content;
+        if name.len() > 0 {
+            e!(
+                "Unable to get member: {:?}",
+                serenity::model::id::GuildId(config::SERVER_ID)
+                    .member(ctx.http.clone(), msg.author.id)
+                    .map(|mut member| {
+                        e!(
+                            "Unable to remove role: {:?}",
+                            member.remove_role(&ctx.http, config::UNREGISTERED_MEMBER_ROLE)
+                        );
+                        e!(
+                            "Unable to edit nickname: {:?}",
+                            member
+                                .edit(&ctx.http, |m| {
+                                    let mut rng = rand::thread_rng();
+                                    m.nickname(format!(
+                                        "{}, {}",
+                                        name,
+                                        [
+                                            "The Big Cheese",
+                                            "The One and Only",
+                                            "The Exalted One",
+                                            "not to be trusted",
+                                            "The Scoundrel",
+                                            "A big fish in a small pond",
+                                        ][rng.gen_range(0, 5)]
+                                    ));
+                                    m
+                                })
+                                .map(|()| {
+                                    e!(
+                                        "Unable to add role: {:?}",
+                                        member.add_role(&ctx.http, config::REGISTERED_MEMBER_ROLE)
+                                    );
+                                })
+                        );
+                    })
+            );
+            e!("Error deleting register message: {:?}", msg.delete(ctx));
+        } else {
+            e!(
+                "Error sending message: {:?}",
+                msg.channel_id
+                    .say(&ctx.http, "Usage: !register <ucc username>")
+            );
+        }
+    }
+}
diff --git a/src/voting.rs b/src/voting.rs
new file mode 100644
index 0000000000000000000000000000000000000000..97aa58ea8632cb227661bf171efb11d37560d8f7
--- /dev/null
+++ b/src/voting.rs
@@ -0,0 +1,382 @@
+use serenity::{
+    model::{channel, channel::Message},
+    prelude::*,
+    utils::MessageBuilder,
+};
+
+use crate::config;
+
+macro_rules! e {
+    ($error: literal, $x:expr) => {
+        match $x {
+            Ok(_) => (),
+            Err(why) => eprintln!($error, why),
+        }
+    };
+}
+
+pub struct Commands;
+impl Commands {
+    pub fn move_something(ctx: Context, msg: Message, content: &str) {
+        let motion = content;
+        if motion.len() > 0 {
+            create_motion(&ctx, &msg, motion);
+        } else {
+            e!(
+                "Error sending message: {:?}",
+                msg.channel_id.say(
+                    &ctx.http,
+                    "If there's something you want to motion, put it after the !move keyword",
+                )
+            );
+        }
+    }
+    pub fn motion(ctx: Context, msg: Message, _content: &str) {
+        e!("Error sending message: {:?}",
+                msg.channel_id.say(
+                &ctx.http,
+                "I hope you're not having a motion. You may have wanted to !move something instead."
+            ));
+    }
+    pub fn poll(ctx: Context, msg: Message, content: &str) {
+        let topic = content;
+        if topic.len() > 0 {
+            create_poll(&ctx, &msg, topic);
+        } else {
+            e!(
+                "Error sending message: {:?}",
+                msg.channel_id.say(
+                    &ctx.http,
+                    "If there's something you want to motion, put it after the !move keyword",
+                )
+            );
+        }
+    }
+    pub fn cowsay(ctx: Context, msg: Message, content: &str) {
+        let mut text = content.to_owned();
+        text.escape_default();
+        // Guess what buddy! You definitely are passing a string to cowsay
+        text.insert(0, '\'');
+        text.insert(text.len(), '\'');
+        let output = std::process::Command::new("cowsay")
+            .arg(text)
+            .output()
+            // btw, if we can't execute cowsay we crash
+            .expect("failed to execute cowsay");
+        let mut message = MessageBuilder::new();
+        message.push_codeblock(
+            String::from_utf8(output.stdout).expect("unable to parse stdout to String"),
+            None,
+        );
+        e!(
+            "Error sending message: {:?}",
+            msg.channel_id.say(&ctx.http, message.build())
+        );
+    }
+}
+
+fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
+    println!("{} created a motion {}", msg.author.name, topic);
+    match msg.channel_id.send_message(&ctx.http, |m| {
+        m.embed(|embed| {
+            embed.author(|a| {
+                a.name(&msg.author.name);
+                a.icon_url(
+                    msg.author
+                        .static_avatar_url()
+                        .expect("Expected author to have avatar"),
+                );
+                a
+            });
+            embed.colour(serenity::utils::Colour::GOLD);
+            embed.title(format!("Motion to {}", topic));
+            let mut desc = MessageBuilder::new();
+            desc.role(config::VOTE_ROLE);
+            desc.push(" take a look at this motion from ");
+            desc.mention(&msg.author);
+            embed.description(desc.build());
+            embed.field("Status", "Under Consideration", true);
+            embed.field("Votes", "For: 0\nAgainst: 0\nAbstain: 0", true);
+            embed.timestamp(msg.timestamp.to_rfc3339());
+            embed
+        });
+        m.reactions(vec![
+            config::FOR_VOTE,
+            config::AGAINST_VOTE,
+            config::ABSTAIN_VOTE,
+            config::APPROVE_REACT,
+            config::DISAPPROVE_REACT,
+        ]);
+        m
+    }) {
+        Err(why) => {
+            println!("Error sending message: {:?}", why);
+        }
+        Ok(_) => {
+            if let Err(why) = msg.delete(ctx) {
+                println!("Error deleting motion prompt: {:?}", why);
+            }
+        }
+    }
+}
+
+fn create_poll(ctx: &Context, msg: &Message, topic: &str) {
+    println!("{} created a poll {}", msg.author.name, topic);
+    match msg.channel_id.send_message(&ctx.http, |m| {
+        m.embed(|embed| {
+            embed.author(|a| {
+                a.name(&msg.author.name);
+                a.icon_url(
+                    msg.author
+                        .static_avatar_url()
+                        .expect("Expected author to have avatar"),
+                );
+                a
+            });
+            embed.colour(serenity::utils::Colour::BLUE);
+            embed.title(format!("Poll {}", topic));
+            let mut desc = MessageBuilder::new();
+            desc.mention(&msg.author);
+            desc.push(" wants to know what you think.");
+            embed.description(desc.build());
+            embed.timestamp(msg.timestamp.to_rfc3339());
+            embed
+        });
+        m.reactions(vec![
+            config::APPROVE_REACT,
+            config::DISAPPROVE_REACT,
+            config::UNSURE_REACT,
+        ]);
+        m
+    }) {
+        Err(why) => {
+            println!("Error sending message: {:?}", why);
+        }
+        Ok(_) => {
+            if let Err(why) = msg.delete(ctx) {
+                println!("Error deleting motion prompt: {:?}", why);
+            }
+        }
+    }
+}
+
+fn update_motion(
+    ctx: &Context,
+    msg: &mut Message,
+    user: &serenity::model::user::User,
+    change: &str,
+    reaction: channel::Reaction,
+) {
+    let for_votes = msg
+        .reaction_users(ctx, config::FOR_VOTE, None, None)
+        .unwrap()
+        .len() as isize
+        - 1;
+    let against_votes = msg
+        .reaction_users(ctx, config::AGAINST_VOTE, None, None)
+        .unwrap()
+        .len() as isize
+        - 1;
+    let abstain_votes = msg
+        .reaction_users(ctx, config::ABSTAIN_VOTE, None, None)
+        .unwrap()
+        .len() as isize
+        - 1;
+
+    let strength_buff = |react: &str| {
+        msg.reaction_users(ctx, react, None, None)
+            .unwrap()
+            .iter()
+            .filter(
+                |u| match u.has_role(ctx, config::SERVER_ID, config::TIEBREAKER_ROLE) {
+                    Ok(true) => true,
+                    _ => false,
+                },
+            )
+            .count()
+            > 0
+    };
+
+    let for_strength = for_votes as f32
+        + (if strength_buff(config::FOR_VOTE) {
+            0.5
+        } else {
+            0.0
+        });
+    let against_strength = against_votes as f32
+        + (if strength_buff(config::AGAINST_VOTE) {
+            0.5
+        } else {
+            0.0
+        });
+    let abstain_strength = abstain_votes as f32
+        + (if strength_buff(config::ABSTAIN_VOTE) {
+            0.5
+        } else {
+            0.0
+        });
+
+    let old_embed = msg.embeds[0].clone();
+    let topic = old_embed.clone().title.unwrap();
+
+    println!(
+        "  {:10} {:6} {} on {}",
+        user.name,
+        change,
+        reaction.emoji.as_data().as_str(),
+        topic
+    );
+
+    let update_status = |e: &mut serenity::builder::CreateEmbed,
+                         status: &str,
+                         last_status_full: String,
+                         topic: &str| {
+        let last_status = last_status_full.lines().next().expect("No previous status");
+        if last_status == status {
+            e.field("Status", last_status_full, true);
+        } else {
+            e.field(
+                "Status",
+                format!("{}\n_was_ {}", status, last_status_full),
+                true,
+            );
+            println!("Motion to {} now {}", topic, status);
+            //
+            let mut message = MessageBuilder::new();
+            message.push_bold(topic);
+            message.push(" is now ");
+            message.push_bold(status);
+            message.push_italic(format!(" (was {})", last_status));
+            if let Err(why) = config::ANNOUNCEMENT_CHANNEL.say(&ctx.http, message.build()) {
+                println!("Error sending message: {:?}", why);
+            };
+        }
+    };
+
+    if let Err(why) = msg.edit(ctx, |m| {
+        m.embed(|e| {
+            e.author(|a| {
+                let old_author = old_embed.clone().author.expect("Expected author in embed");
+                a.name(old_author.name);
+                a.icon_url(
+                    old_author
+                        .icon_url
+                        .expect("Expected embed author to have icon"),
+                );
+                a
+            });
+            e.title(&topic);
+            e.description(old_embed.description.unwrap());
+            let last_status_full = old_embed
+                .fields
+                .iter()
+                .filter(|f| f.name == "Status")
+                .next()
+                .expect("No previous status")
+                .clone()
+                .value;
+            if for_strength > (config::VOTE_POOL_SIZE / 2) as f32 {
+                e.colour(serenity::utils::Colour::TEAL);
+                update_status(e, "Passed", last_status_full, &topic);
+            } else if against_strength + abstain_strength > (config::VOTE_POOL_SIZE / 2) as f32 {
+                e.colour(serenity::utils::Colour::RED);
+                update_status(e, "Failed", last_status_full, &topic);
+            } else {
+                e.colour(serenity::utils::Colour::GOLD);
+                update_status(e, "Under Consideration", last_status_full, &topic);
+            }
+            e.field(
+                format!(
+                    "Votes ({}/{})",
+                    for_votes + against_votes + abstain_votes,
+                    config::VOTE_POOL_SIZE
+                ),
+                format!(
+                    "For: {}\nAgainst: {}\nAbstain: {}",
+                    for_votes, against_votes, abstain_votes
+                ),
+                true,
+            );
+            e.timestamp(
+                old_embed
+                    .timestamp
+                    .expect("Expected embed to have timestamp"),
+            );
+            e
+        })
+    }) {
+        println!("Error updating motion: {:?}", why);
+    }
+}
+
+pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
+    match add_reaction.message(&ctx.http) {
+        Ok(mut message) => {
+            if message.author.id.0 == config::BOT_ID {
+                if let Ok(user) = add_reaction.user(&ctx) {
+                    match user.has_role(&ctx, config::SERVER_ID, config::VOTE_ROLE) {
+                        Ok(true) => {
+                            for react in
+                                [config::FOR_VOTE, config::AGAINST_VOTE, config::ABSTAIN_VOTE]
+                                    .iter()
+                                    .filter(|r| r != &&add_reaction.emoji.as_data().as_str())
+                            {
+                                for a_user in
+                                    message.reaction_users(&ctx, *react, None, None).unwrap()
+                                {
+                                    if a_user.id.0 == user.id.0 {
+                                        if let Err(why) = add_reaction.delete(&ctx) {
+                                            println!("Error deleting react: {:?}", why);
+                                        };
+                                    }
+                                }
+                            }
+                            if !config::ALLOWED_REACTS
+                                .contains(&add_reaction.emoji.as_data().as_str())
+                            {
+                                if let Err(why) = add_reaction.delete(&ctx) {
+                                    println!("Error deleting react: {:?}", why);
+                                };
+                            }
+                            if user.id.0 != config::BOT_ID {
+                                update_motion(&ctx, &mut message, &user, "add", add_reaction);
+                            }
+                        }
+                        Ok(false) => {
+                            if user.id.0 != config::BOT_ID {
+                                if ![config::APPROVE_REACT, config::DISAPPROVE_REACT]
+                                    .contains(&add_reaction.emoji.as_data().as_str())
+                                {
+                                    if let Err(why) = add_reaction.delete(&ctx) {
+                                        println!("Error deleting react: {:?}", why);
+                                    };
+                                }
+                            }
+                        }
+                        Err(why) => {
+                            println!("Error getting user role: {:?}", why);
+                        }
+                    }
+                }
+            }
+        }
+        Err(why) => {
+            println!("Error processing react: {:?}", why);
+        }
+    }
+}
+
+pub fn reaction_remove(ctx: Context, removed_reaction: channel::Reaction) {
+    match removed_reaction.message(&ctx.http) {
+        Ok(mut message) => {
+            if message.author.id.0 == config::BOT_ID {
+                if let Ok(user) = removed_reaction.user(&ctx) {
+                    update_motion(&ctx, &mut message, &user, "remove", removed_reaction);
+                }
+            }
+        }
+        Err(why) => {
+            println!("Error getting user role: {:?}", why);
+        }
+    }
+}