diff --git a/Cargo.toml b/Cargo.toml
index 45e271a9e470a3eba801a44f5cac5fc142b9d191..1b9c56495fb82eae7590595cddff250658718e67 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,3 +10,5 @@ rand = "^0.7.2"
 lazy_static = "^1.4.0"
 log = "^0.4.8"
 simplelog = "^0.7.4"
+toml = "^0.5.6"
+serde = "^1.0.104"
diff --git a/src/config.rs b/src/config.rs
index 2c1170b4e5c3ddc1634b077bd309f9a2144abbbb..d7aad5d132ecec266496bbfe850b1d7fde027ade 120000
--- a/src/config.rs
+++ b/src/config.rs
@@ -1 +1,49 @@
-./config.test.rs
\ No newline at end of file
+use serenity;
+use std::fs;
+use serde::Deserialize;
+use toml;
+
+lazy_static! {
+    static ref CONFIG_FILE: String = fs::read_to_string("config.toml").unwrap();
+}
+
+lazy_static! {
+    pub static ref CONFIG: UccbotConfig = toml::from_str(&CONFIG_FILE).unwrap();
+}
+
+pub static DISCORD_TOKEN: &str = include_str!("discord_token");
+
+#[derive(Deserialize)]
+pub struct UccbotConfig {
+    pub server_id: u64,
+    // #general
+    pub main_channel: serenity::model::id::ChannelId,
+    // #the-corner
+    pub welcome_channel: serenity::model::id::ChannelId,
+    // #general
+    pub announcement_channel: serenity::model::id::ChannelId,
+    pub bot_id: u64,
+    pub vote_pool_size: i8,
+    pub vote_role: u64,
+    pub tiebreaker_role: u64,
+    pub unregistered_member_role: u64,
+    pub registered_member_role: u64,
+    pub command_prefix: &'static str,
+    pub for_vote: &'static str,
+    pub against_vote: &'static str,
+    pub abstain_vote: &'static str,
+    pub approve_react: &'static str,
+    pub disapprove_react: &'static str,
+    pub unsure_react: &'static str,
+}
+
+impl UccbotConfig {
+    pub fn allowed_reacts(&self) -> Vec<String> {
+        vec!(self.for_vote.to_string(),
+        self.against_vote.to_string(),
+        self.abstain_vote.to_string(),
+        self.approve_react.to_string(),
+        self.disapprove_react.to_string(),
+        self.unsure_react.to_string())
+    }
+}
\ No newline at end of file
diff --git a/src/config.test.rs b/src/config.test.rs
deleted file mode 100644
index 5fc3bb7609b2223a34b248e2df3b244c263e1cd3..0000000000000000000000000000000000000000
--- a/src/config.test.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-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 COMMAND_PREFIX: &str = "!";
-
-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/config.test.toml b/src/config.test.toml
new file mode 100644
index 0000000000000000000000000000000000000000..bb0b38e6f59fdd20fd1638c7690b9171d7b5df32
--- /dev/null
+++ b/src/config.test.toml
@@ -0,0 +1,23 @@
+server_id = 606351521117896704
+#general
+main_channel = 606351521117896706
+#the-corner
+welcome_channel = 606351613816209418
+#general
+announcement_channel = 606351521117896706
+
+bot_id = 607078903969742848
+
+vote_pool_size = 2
+vote_role = 607478818038480937
+tiebreaker_role = 607509283483025409
+unregistered_member_role = 608282247350714408
+registered_member_role = 608282133118582815
+command_prefix = "!"
+
+for_vote = "👍"
+against_vote = "👎"
+abstain_vote = "🙊"
+approve_react = "⬆"
+disapprove_react = "⬇"
+unsure_react = "❔"
diff --git a/src/config.toml b/src/config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..bb0b38e6f59fdd20fd1638c7690b9171d7b5df32
--- /dev/null
+++ b/src/config.toml
@@ -0,0 +1,23 @@
+server_id = 606351521117896704
+#general
+main_channel = 606351521117896706
+#the-corner
+welcome_channel = 606351613816209418
+#general
+announcement_channel = 606351521117896706
+
+bot_id = 607078903969742848
+
+vote_pool_size = 2
+vote_role = 607478818038480937
+tiebreaker_role = 607509283483025409
+unregistered_member_role = 608282247350714408
+registered_member_role = 608282133118582815
+command_prefix = "!"
+
+for_vote = "👍"
+against_vote = "👎"
+abstain_vote = "🙊"
+approve_react = "⬆"
+disapprove_react = "⬇"
+unsure_react = "❔"
diff --git a/src/config.ucc.rs b/src/config.ucc.rs
deleted file mode 100644
index 58aa01cc349d5631851a18104d290637f88abbcc..0000000000000000000000000000000000000000
--- a/src/config.ucc.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use serenity;
-
-pub static DISCORD_TOKEN: &str = include_str!("discord_token");
-
-pub static SERVER_ID: u64 = 264401248676085760;
-// #ucc
-pub static MAIN_CHANNEL: serenity::model::id::ChannelId =
-    serenity::model::id::ChannelId(264401248676085760);
-// #welcome
-pub static WELCOME_CHANNEL: serenity::model::id::ChannelId =
-    serenity::model::id::ChannelId(606750983699300372);
-// #committee
-pub static ANNOUNCEMENT_CHANNEL: serenity::model::id::ChannelId =
-    serenity::model::id::ChannelId(264411219627212801);
-
-pub static BOT_ID: u64 = 635407267881156618;
-
-pub static VOTE_POOL_SIZE: i8 = 7;
-pub static VOTE_ROLE: u64 = 269817189966544896;
-pub static TIEBREAKER_ROLE: u64 = 635370432568098817;
-pub static UNREGISTERED_MEMBER_ROLE: u64 = 0; // does not exist
-pub static REGISTERED_MEMBER_ROLE: u64 = 0; // does not exist
-
-pub static COMMAND_PREFIX: &str = "!";
-
-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/config.ucc.toml b/src/config.ucc.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2c995db97c8425192c1a85d41715ee5bada28960
--- /dev/null
+++ b/src/config.ucc.toml
@@ -0,0 +1,24 @@
+server_id = 264401248676085760
+#ucc
+main_channel    = 264401248676085760
+#welcome
+welcome_channel    = 606750983699300372
+#committee
+announcement_channel    = 264411219627212801
+
+bot_id = 635407267881156618
+
+vote_pool_size = 7
+vote_role = 269817189966544896
+tiebreaker_role = 635370432568098817
+unregistered_member_role = 0 # does not exist
+registered_member_role = 0 # does not exist
+
+command_prefix = "!"
+
+for_vote = "👍"
+against_vote = "👎"
+abstain_vote = "🙊"
+approve_react = "⬆"
+disapprove_react = "⬇"
+unsure_react = "❔"
diff --git a/src/main.rs b/src/main.rs
index 4898db678298c7a071e2f4bc9b07a5e84bbacb27..53cb2ffd779581d0cfb3dcf5e5e3886413b27697 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,6 +17,8 @@ mod config;
 mod user_management;
 mod voting;
 
+use config::CONFIG;
+
 macro_rules! e {
     ($error: literal, $x:expr) => {
         match $x {
@@ -35,7 +37,7 @@ 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.content.starts_with(config::COMMAND_PREFIX)) {
+        if !(msg.content.starts_with(CONFIG.command_prefix)) {
             return;
         }
         let message_content: Vec<_> = msg.content[1..].splitn(2, ' ').collect();
@@ -63,11 +65,11 @@ impl EventHandler for Handler {
                 let mut message = MessageBuilder::new();
                 message.push_line(format!(
                     "Use {}move <action> to make a circular motion",
-                    config::COMMAND_PREFIX
+                    &CONFIG.command_prefix
                 ));
                 message.push_line(format!(
                     "Use {}poll <proposal> to see what people think about something",
-                    config::COMMAND_PREFIX
+                    &CONFIG.command_prefix
                 ));
                 e!(
                     "Error sending message: {:?}",
@@ -79,7 +81,7 @@ impl EventHandler for Handler {
                     "Error sending message: {:?}",
                     msg.channel_id.say(
                         &ctx.http,
-                        format!("Unrecognised command. Try {}help", config::COMMAND_PREFIX)
+                        format!("Unrecognised command. Try {}help", &CONFIG.command_prefix)
                     )
                 );
             }
@@ -89,7 +91,7 @@ impl EventHandler for Handler {
     fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) {
         match add_reaction.message(&ctx.http) {
             Ok(message) => {
-                if message.author.id.0 != config::BOT_ID || add_reaction.user_id == config::BOT_ID {
+                if message.author.id.0 != CONFIG.bot_id || add_reaction.user_id == CONFIG.bot_id {
                     return;
                 }
                 match message_type(&message) {
@@ -106,7 +108,7 @@ impl EventHandler for Handler {
     fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) {
         match removed_reaction.message(&ctx.http) {
             Ok(message) => {
-                if message.author.id.0 != config::BOT_ID || removed_reaction.user_id == config::BOT_ID {
+                if message.author.id.0 != CONFIG.bot_id || removed_reaction.user_id == CONFIG.bot_id {
                     return;
                 }
                 match message_type(&message) {
@@ -150,6 +152,8 @@ fn main() {
         ),
     ])
     .unwrap();
+
+
     // Configure the client with your Discord bot token in the environment.
     let token = config::DISCORD_TOKEN;
 
diff --git a/src/user_management.rs b/src/user_management.rs
index a95d50029efe80add6f8b835afcf691d3628df1e..ea1b93b8af5b11b99c2a823f9e5d02633ecedc81 100644
--- a/src/user_management.rs
+++ b/src/user_management.rs
@@ -5,7 +5,7 @@ use serenity::{
     utils::MessageBuilder,
 };
 
-use crate::config;
+use crate::config::CONFIG;
 
 macro_rules! e {
     ($error: literal, $x:expr) => {
@@ -23,18 +23,18 @@ pub fn new_member(ctx: &Context, mut new_member: 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()) {
+    if let Err(why) = CONFIG.welcome_channel.say(&ctx, message.build()) {
         error!("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()) {
+    message.mention(&CONFIG.welcome_channel);
+    if let Err(why) = CONFIG.main_channel.say(&ctx, message.build()) {
         error!("Error sending message: {:?}", why);
     }
 
-    if let Err(why) = new_member.add_role(&ctx.http, config::UNREGISTERED_MEMBER_ROLE) {
+    if let Err(why) = new_member.add_role(&ctx.http, CONFIG.unregistered_member_role) {
         error!("Error adding user role: {:?}", why);
     };
 }
@@ -44,7 +44,7 @@ impl Commands {
     pub fn join(ctx: Context, msg: Message, _content: &str) {
         e!(
             "Unable to get user: {:?}",
-            serenity::model::id::GuildId(config::SERVER_ID)
+            serenity::model::id::GuildId(CONFIG.server_id)
                 .member(ctx.http.clone(), msg.author.id)
                 .map(|member| new_member(&ctx, member))
         );
@@ -61,12 +61,12 @@ impl Commands {
         }
         e!(
             "Unable to get member: {:?}",
-            serenity::model::id::GuildId(config::SERVER_ID)
+            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)
+                        member.remove_role(&ctx.http, CONFIG.unregistered_member_role)
                     );
                     e!(
                         "Unable to edit nickname: {:?}",
@@ -90,7 +90,7 @@ impl Commands {
                             .map(|()| {
                                 e!(
                                     "Unable to add role: {:?}",
-                                    member.add_role(&ctx.http, config::REGISTERED_MEMBER_ROLE)
+                                    member.add_role(&ctx.http, CONFIG.registered_member_role)
                                 );
                             })
                     );
diff --git a/src/voting.rs b/src/voting.rs
index 40573c3661bcd9ad6b917da83f5d17a4ad61cc94..d7dd907d6baaa335bb2d4a047a205d182f07f0a2 100644
--- a/src/voting.rs
+++ b/src/voting.rs
@@ -6,7 +6,7 @@ use serenity::{
 use std::collections::HashMap;
 use std::sync::Mutex;
 
-use crate::config;
+use crate::config::CONFIG;
 
 macro_rules! e {
     ($error: literal, $x:expr) => {
@@ -96,7 +96,7 @@ fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
             embed.colour(serenity::utils::Colour::GOLD);
             embed.title(format!("Motion to {}", topic));
             let mut desc = MessageBuilder::new();
-            desc.role(config::VOTE_ROLE);
+            desc.role(CONFIG.vote_role);
             desc.push(" take a look at this motion from ");
             desc.mention(&msg.author);
             embed.description(desc.build());
@@ -106,11 +106,11 @@ fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
             embed
         });
         m.reactions(vec![
-            config::FOR_VOTE,
-            config::AGAINST_VOTE,
-            config::ABSTAIN_VOTE,
-            config::APPROVE_REACT,
-            config::DISAPPROVE_REACT,
+            CONFIG.for_vote.to_string(),
+            CONFIG.against_vote.to_string(),
+            CONFIG.abstain_vote.to_string(),
+            CONFIG.approve_react.to_string(),
+            CONFIG.disapprove_react.to_string(),
         ]);
         m
     }) {
@@ -144,9 +144,9 @@ fn create_poll(ctx: &Context, msg: &Message, topic: &str) {
             embed
         });
         m.reactions(vec![
-            config::APPROVE_REACT,
-            config::DISAPPROVE_REACT,
-            config::UNSURE_REACT,
+            CONFIG.approve_react.to_string(),
+            CONFIG.disapprove_react.to_string(),
+            CONFIG.unsure_react.to_string(),
         ]);
         m
     }) {
@@ -179,18 +179,18 @@ fn get_cached_motion(ctx: &Context, msg: &Message) -> MotionInfo {
             votes: {
                 let mut m = HashMap::new();
                 m.insert(
-                    config::FOR_VOTE,
-                    msg.reaction_users(ctx, config::FOR_VOTE, None, None)
+                    CONFIG.for_vote,
+                    msg.reaction_users(ctx, CONFIG.for_vote, None, None)
                         .unwrap(),
                 );
                 m.insert(
-                    config::AGAINST_VOTE,
-                    msg.reaction_users(ctx, config::AGAINST_VOTE, None, None)
+                    CONFIG.against_vote,
+                    msg.reaction_users(ctx, CONFIG.against_vote, None, None)
                         .unwrap(),
                 );
                 m.insert(
-                    config::ABSTAIN_VOTE,
-                    msg.reaction_users(ctx, config::ABSTAIN_VOTE, None, None)
+                    CONFIG.abstain_vote,
+                    msg.reaction_users(ctx, CONFIG.abstain_vote, None, None)
                         .unwrap(),
                 );
                 m
@@ -217,31 +217,31 @@ fn update_motion(
 ) {
     let motion_info: MotionInfo = get_cached_motion(ctx, msg);
 
-    let for_votes = motion_info.votes.get(config::FOR_VOTE).unwrap().len() as isize - 1;
-    let against_votes = motion_info.votes.get(config::AGAINST_VOTE).unwrap().len() as isize - 1;
-    let abstain_votes = motion_info.votes.get(config::ABSTAIN_VOTE).unwrap().len() as isize - 1;
+    let for_votes = motion_info.votes.get(&CONFIG.for_vote).unwrap().len() as isize - 1;
+    let against_votes = motion_info.votes.get(&CONFIG.against_vote).unwrap().len() as isize - 1;
+    let abstain_votes = motion_info.votes.get(&CONFIG.abstain_vote).unwrap().len() as isize - 1;
 
     let has_tiebreaker = |users: &Vec<serenity::model::user::User>| {
         users.iter().any(|u| {
-            u.has_role(ctx, config::SERVER_ID, config::TIEBREAKER_ROLE)
+            u.has_role(ctx, CONFIG.server_id, CONFIG.tiebreaker_role)
                 .unwrap()
         })
     };
 
     let for_strength = for_votes as f32
-        + (if has_tiebreaker(motion_info.votes.get(config::FOR_VOTE).unwrap()) {
+        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.for_vote).unwrap()) {
             0.25
         } else {
             0.0
         });
     let against_strength = against_votes as f32
-        + (if has_tiebreaker(motion_info.votes.get(config::AGAINST_VOTE).unwrap()) {
+        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.against_vote).unwrap()) {
             0.25
         } else {
             0.0
         });
     let abstain_strength = abstain_votes as f32
-        + (if has_tiebreaker(motion_info.votes.get(config::ABSTAIN_VOTE).unwrap()) {
+        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.abstain_vote).unwrap()) {
             0.25
         } else {
             0.0
@@ -278,7 +278,7 @@ fn update_motion(
             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()) {
+            if let Err(why) = CONFIG.announcement_channel.say(&ctx.http, message.build()) {
                 error!("Error sending message: {:?}", why);
             };
         }
@@ -306,10 +306,10 @@ fn update_motion(
                 .expect("No previous status")
                 .clone()
                 .value;
-            if for_strength > (config::VOTE_POOL_SIZE as f32 / 2.0) {
+            if for_strength > (CONFIG.vote_pool_size as f32 / 2.0) {
                 e.colour(serenity::utils::Colour::TEAL);
                 update_status(e, "Passed", last_status_full, &topic);
-            } else if against_strength + abstain_strength > (config::VOTE_POOL_SIZE as f32 / 2.0) {
+            } else if against_strength + abstain_strength > (CONFIG.vote_pool_size as f32 / 2.0) {
                 e.colour(serenity::utils::Colour::RED);
                 update_status(e, "Failed", last_status_full, &topic);
             } else {
@@ -320,7 +320,7 @@ fn update_motion(
                 format!(
                     "Votes ({}/{})",
                     for_votes + against_votes + abstain_votes,
-                    config::VOTE_POOL_SIZE
+                    CONFIG.vote_pool_size
                 ),
                 format!(
                     "For: {}\nAgainst: {}\nAbstain: {}",
@@ -344,10 +344,10 @@ pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
     match add_reaction.message(&ctx.http) {
         Ok(mut message) => {
             if let Ok(user) = add_reaction.user(&ctx) {
-                match user.has_role(&ctx, config::SERVER_ID, config::VOTE_ROLE) {
+                match user.has_role(&ctx, CONFIG.server_id, CONFIG.vote_role) {
                     Ok(true) => {
                         // remove vote if already voted
-                        for react in [config::FOR_VOTE, config::AGAINST_VOTE, config::ABSTAIN_VOTE]
+                        for react in [CONFIG.for_vote, CONFIG.against_vote, CONFIG.abstain_vote]
                             .iter()
                             .filter(|r| r != &&add_reaction.emoji.as_data().as_str())
                         {
@@ -362,7 +362,7 @@ pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
                             }
                         }
                         // remove 'illegal' reacts
-                        if !config::ALLOWED_REACTS.contains(&add_reaction.emoji.as_data().as_str())
+                        if !CONFIG.allowed_reacts().contains(&add_reaction.emoji.as_data())
                         {
                             if let Err(why) = add_reaction.delete(&ctx) {
                                 error!("Error deleting react: {:?}", why);
@@ -382,7 +382,7 @@ pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
                         update_motion(&ctx, &mut message, &user, "add", add_reaction);
                     }
                     Ok(false) => {
-                        if ![config::APPROVE_REACT, config::DISAPPROVE_REACT]
+                        if ![CONFIG.approve_react, CONFIG.disapprove_react]
                             .contains(&add_reaction.emoji.as_data().as_str())
                         {
                             if let Err(why) = add_reaction.delete(&ctx) {