diff --git a/.gitignore b/.gitignore
index a934c91bc29de91fcf5774930e87997fedd337bf..53d274c4bf9aab0f31579f35cb7a06ef9d99de16 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ __pycache__/*
 
 # Secret files
 */tokens.ini
+
+*/config.py
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/admin.py b/All-In-One-bot/cogs/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e4202d49033d1fc90b1e44b14f25d8bba36012a
--- /dev/null
+++ b/All-In-One-bot/cogs/admin.py
@@ -0,0 +1,250 @@
+import asyncio
+import inspect
+import io
+import textwrap
+import traceback
+from contextlib import redirect_stdout
+
+import discord
+from discord.ext import commands
+
+
+# to expose to the eval command
+
+class Admin:
+	"""Admin-only commands that make the bot dynamic."""
+
+	def __init__(self, bot):
+		self.bot = bot
+		self._last_result = None
+		self.sessions = set()
+
+	def cleanup_code(self, content):
+		"""Automatically removes code blocks from the code."""
+		# remove ```py\n```
+		if content.startswith('```') and content.endswith('```'):
+			return '\n'.join(content.split('\n')[1:-1])
+
+		# remove `foo`
+		return content.strip('` \n')
+
+	async def __local_check(self, ctx):
+		return await self.bot.is_owner(ctx.author)
+	def get_syntax_error(self, e):
+		if e.text is None:
+			return f'```py\n{e.__class__.__name__}: {e}\n```'
+		return f'```py\n{e.text}{"^":>{e.offset}}\n{e.__class__.__name__}: {e}```'
+
+	@commands.command(hidden=True)
+	async def load(self, ctx, *, module):
+		"""Loads a module."""
+		try:
+			self.bot.load_extension(module)
+		except Exception as e:
+			await ctx.send(f'```py\n{traceback.format_exc()}\n```')
+		else:
+			await ctx.send('\N{OK HAND SIGN}')
+
+	@commands.command(hidden=True)
+	async def unload(self, ctx, *, module):
+		"""Unloads a module."""
+		try:
+			self.bot.unload_extension(module)
+		except Exception as e:
+			await ctx.send(f'```py\n{traceback.format_exc()}\n```')
+		else:
+			await ctx.send('\N{OK HAND SIGN}')
+
+	@commands.command(name='reload', hidden=True)
+	async def _reload(self, ctx, *, module):
+		"""Reloads a module."""
+		try:
+			self.bot.unload_extension(module)
+			self.bot.load_extension(module)
+		except Exception as e:
+			await ctx.send(f'```py\n{traceback.format_exc()}\n```')
+		else:
+			await ctx.send('\N{OK HAND SIGN}')
+
+	@commands.command(pass_context=True, hidden=True, name='eval')
+	async def _eval(self, ctx, *, body: str):
+		"""Evaluates a code"""
+
+		env = {
+			'bot': self.bot,
+			'ctx': ctx,
+			'channel': ctx.channel,
+			'author': ctx.author,
+			'guild': ctx.guild,
+			'message': ctx.message,
+			'_': self._last_result
+		}
+
+		env.update(globals())
+
+		body = self.cleanup_code(body)
+		stdout = io.StringIO()
+
+		to_compile = f'async def func():\n{textwrap.indent(body, "  ")}'
+
+		try:
+			exec(to_compile, env)
+		except Exception as e:
+			return await ctx.send(f'```py\n{e.__class__.__name__}: {e}\n```')
+
+		func = env['func']
+		try:
+			with redirect_stdout(stdout):
+				ret = await func()
+		except Exception as e:
+			value = stdout.getvalue()
+			await ctx.send(f'```py\n{value}{traceback.format_exc()}\n```')
+		else:
+			value = stdout.getvalue()
+			try:
+				await ctx.message.add_reaction('\u2705')
+			except:
+				pass
+
+			if ret is None:
+				if value:
+					await ctx.send(f'```py\n{value}\n```')
+			else:
+				self._last_result = ret
+				await ctx.send(f'```py\n{value}{ret}\n```')
+
+	@commands.command(pass_context=True, hidden=True)
+	async def repl(self, ctx):
+		"""Launches an interactive REPL session."""
+		variables = {
+			'ctx': ctx,
+			'bot': self.bot,
+			'message': ctx.message,
+			'guild': ctx.guild,
+			'channel': ctx.channel,
+			'author': ctx.author,
+			'_': None,
+		}
+
+		if ctx.channel.id in self.sessions:
+			await ctx.send('Already running a REPL session in this channel. Exit it with `quit`.')
+			return
+
+		self.sessions.add(ctx.channel.id)
+		await ctx.send('Enter code to execute or evaluate. `exit()` or `quit` to exit.')
+
+		def check(m):
+			return m.author.id == ctx.author.id and \
+				   m.channel.id == ctx.channel.id and \
+				   m.content.startswith('`')
+
+		while True:
+			try:
+				response = await self.bot.wait_for('message', check=check, timeout=10.0 * 60.0)
+			except asyncio.TimeoutError:
+				await ctx.send('Exiting REPL session.')
+				self.sessions.remove(ctx.channel.id)
+				break
+
+			cleaned = self.cleanup_code(response.content)
+
+			if cleaned in ('quit', 'exit', 'exit()'):
+				await ctx.send('Exiting.')
+				self.sessions.remove(ctx.channel.id)
+				return
+
+			executor = exec
+			if cleaned.count('\n') == 0:
+				# single statement, potentially 'eval'
+				try:
+					code = compile(cleaned, '<repl session>', 'eval')
+				except SyntaxError:
+					pass
+				else:
+					executor = eval
+
+			if executor is exec:
+				try:
+					code = compile(cleaned, '<repl session>', 'exec')
+				except SyntaxError as e:
+					await ctx.send(self.get_syntax_error(e))
+					continue
+
+			variables['message'] = response
+
+			fmt = None
+			stdout = io.StringIO()
+
+			try:
+				with redirect_stdout(stdout):
+					result = executor(code, variables)
+					if inspect.isawaitable(result):
+						result = await result
+			except Exception as e:
+				value = stdout.getvalue()
+				fmt = f'```py\n{value}{traceback.format_exc()}\n```'
+			else:
+				value = stdout.getvalue()
+				if result is not None:
+					fmt = f'```py\n{value}{result}\n```'
+					variables['_'] = result
+				elif value:
+					fmt = f'```py\n{value}\n```'
+
+			try:
+				if fmt is not None:
+					if len(fmt) > 2000:
+						await ctx.send('Content too big to be printed.')
+					else:
+						await ctx.send(fmt)
+			except discord.Forbidden:
+				pass
+			except discord.HTTPException as e:
+				await ctx.send(f'Unexpected error: `{e}`')
+
+	@commands.command(hidden=True)
+	async def sql(self, ctx, *, query: str):
+		"""Run some SQL."""
+		# the imports are here because I imagine some people would want to use
+		# this cog as a base for their other cog, and since this one is kinda
+		# odd and unnecessary for most people, I will make it easy to remove
+		# for those people.
+		from .utils.formats import TabularData, Plural
+		import time
+
+		query = self.cleanup_code(query)
+
+		is_multistatement = query.count(';') > 1
+		if is_multistatement:
+			# fetch does not support multiple statements
+			strategy = ctx.db.execute
+		else:
+			strategy = ctx.db.fetch
+
+		try:
+			start = time.perf_counter()
+			results = await strategy(query)
+			dt = (time.perf_counter() - start) * 1000.0
+		except Exception:
+			return await ctx.send(f'```py\n{traceback.format_exc()}\n```')
+
+		rows = len(results)
+		if is_multistatement or rows == 0:
+			return await ctx.send(f'`{dt:.2f}ms: {results}`')
+
+		headers = list(results[0].keys())
+		table = TabularData()
+		table.set_columns(headers)
+		table.add_rows(list(r.values()) for r in results)
+		render = table.render()
+
+		fmt = f'```\n{render}\n```\n*Returned {Plural(row=rows)} in {dt:.2f}ms*'
+		if len(fmt) > 2000:
+			fp = io.BytesIO(fmt.encode('utf-8'))
+			await ctx.send('Too many results...', file=discord.File(fp, 'results.txt'))
+		else:
+			await ctx.send(fmt)
+
+
+def setup(bot):
+	bot.add_cog(Admin(bot))
diff --git a/All-In-One-bot/cogs/buttons.py b/All-In-One-bot/cogs/buttons.py
new file mode 100644
index 0000000000000000000000000000000000000000..db3ba4b45c3004a7b346f8448e12817864cd37f8
--- /dev/null
+++ b/All-In-One-bot/cogs/buttons.py
@@ -0,0 +1,631 @@
+from discord.ext import commands
+from datetime import datetime
+import discord
+from .utils import checks
+from lxml import etree
+import random
+import logging
+from urllib.parse import quote as uriquote
+from lru import LRU
+import io
+
+log = logging.getLogger(__name__)
+
+
+def date(argument):
+    formats = (
+        '%Y/%m/%d',
+        '%Y-%m-%d',
+    )
+
+    for fmt in formats:
+        try:
+            return datetime.strptime(argument, fmt)
+        except ValueError:
+            continue
+
+    raise commands.BadArgument('Cannot convert to date. Expected YYYY/MM/DD or YYYY-MM-DD.')
+
+
+def can_use_spoiler():
+    def predicate(ctx):
+        if ctx.guild is None:
+            raise commands.BadArgument('Cannot be used in private messages.')
+
+        my_permissions = ctx.channel.permissions_for(ctx.guild.me)
+        if not (my_permissions.read_message_history and my_permissions.manage_messages and my_permissions.add_reactions):
+            raise commands.BadArgument('Need Read Message History, Add Reactions and Manage Messages ' \
+                                       'to permission to use this. Sorry if I spoiled you.')
+        return True
+    return commands.check(predicate)
+
+
+SPOILER_EMOJI_ID = 430469957042831371
+
+
+class SpoilerCache:
+    __slots__ = ('author_id', 'channel_id', 'title', 'text', 'attachments')
+
+    def __init__(self, data):
+        self.author_id = data['author_id']
+        self.channel_id = data['channel_id']
+        self.title = data['title']
+        self.text = data['text']
+        self.attachments = data['attachments']
+
+    def has_single_image(self):
+        return self.attachments and self.attachments[0].filename.lower().endswith(('.gif', '.png', '.jpg', '.jpeg'))
+
+    def to_embed(self, bot):
+        embed = discord.Embed(title=f'{self.title} Spoiler', colour=0x01AEEE)
+        if self.text:
+            embed.description = self.text
+
+        if self.has_single_image():
+            if self.text is None:
+                embed.title = f'{self.title} Spoiler Image'
+            embed.set_image(url=self.attachments[0].url)
+            attachments = self.attachments[1:]
+        else:
+            attachments = self.attachments
+
+        if attachments:
+            value = '\n'.join(f'[{a.filename}]({a.url})' for a in attachments)
+            embed.add_field(name='Attachments', value=value, inline=False)
+
+        user = bot.get_user(self.author_id)
+        if user:
+            embed.set_author(name=str(user), icon_url=user.avatar_url_as(format='png'))
+
+        return embed
+
+    def to_spoiler_embed(self, ctx, storage_message):
+        description = 'React with <:spoiler:430469957042831371> to reveal the spoiler.'
+        embed = discord.Embed(title=f'{self.title} Spoiler', description=description)
+        if self.has_single_image() and self.text is None:
+            embed.title = f'{self.title} Spoiler Image'
+
+        embed.set_footer(text=storage_message.id)
+        embed.colour = 0x01AEEE
+        embed.set_author(name=ctx.author, icon_url=ctx.author.avatar_url_as(format='png'))
+        return embed
+
+
+class SpoilerCooldown(commands.CooldownMapping):
+    def __init__(self):
+        super().__init__(commands.Cooldown(1, 10.0, commands.BucketType.user))
+
+    def _bucket_key(self, tup):
+        return tup
+
+    def is_rate_limited(self, message_id, user_id):
+        bucket = self.get_bucket((message_id, user_id))
+        return bucket.update_rate_limit() is not None
+
+
+class Buttons:
+    """Buttons that make you feel."""
+
+    def __init__(self, bot):
+        self.bot = bot
+        self._spoiler_cache = LRU(128)
+        self._spoiler_cooldown = SpoilerCooldown()
+
+    @commands.command(hidden=True)
+    async def feelgood(self, ctx):
+        """press"""
+        await ctx.send('*pressed*')
+
+    @commands.command(hidden=True)
+    async def feelbad(self, ctx):
+        """depress"""
+        await ctx.send('*depressed*')
+
+    @commands.command()
+    async def love(self, ctx):
+        """What is love?"""
+        action = random.choice([ctx.send('https://www.youtube.com/watch?v=HEXWRTEbj1I'), await ctx.invoke(self.g, query='define: love')])
+        await action
+
+    @commands.command(hidden=True)
+    async def bored(self, ctx):
+        """boredom looms"""
+        await ctx.send('http://i.imgur.com/BuTKSzf.png')
+
+    @commands.command(pass_context=True)
+    @checks.mod_or_permissions(manage_messages=True)
+    async def nostalgia(self, ctx, date: date, *, channel: discord.TextChannel = None):
+        """Pins an old message from a specific date.
+
+        If a channel is not given, then pins from the channel the
+        command was ran on.
+
+        The format of the date must be either YYYY-MM-DD or YYYY/MM/DD.
+        """
+        channel = channel or ctx.channel
+
+        message = await channel.history(after=date, limit=1).flatten()
+
+        if len(message) == 0:
+            return await ctx.send('Could not find message.')
+
+        message = message[0]
+
+        try:
+            await message.pin()
+        except discord.HTTPException:
+            await ctx.send('Could not pin message.')
+        else:
+            await ctx.send('Pinned message.')
+
+    @nostalgia.error
+    async def nostalgia_error(self, ctx, error):
+        if isinstance(error, commands.BadArgument):
+            await ctx.send(error)
+
+    def parse_google_card(self, node):
+        e = discord.Embed(colour=discord.Colour.blurple())
+
+        # check if it's a calculator card:
+        calculator = node.find(".//span[@class='cwclet']")
+        if calculator is not None:
+            e.title = 'Calculator'
+            result = node.find(".//span[@class='cwcot']")
+            if result is not None:
+                result = ' '.join((calculator.text, result.text.strip()))
+            else:
+                result = calculator.text + ' ???'
+            e.description = result
+            return e
+
+        # check for unit conversion card
+
+        unit_conversions = node.xpath(".//input[contains(@class, '_eif') and @value]")
+        if len(unit_conversions) == 2:
+            e.title = 'Unit Conversion'
+
+            # the <input> contains our values, first value = second value essentially.
+            # these <input> also have siblings with <select> and <option selected=1>
+            # that denote what units we're using
+
+            # We will get 2 <option selected="1"> nodes by traversing the parent
+            # The first unit being converted (e.g. Miles)
+            # The second unit being converted (e.g. Feet)
+
+            xpath = etree.XPath("parent::div/select/option[@selected='1']/text()")
+            try:
+                first_node = unit_conversions[0]
+                first_unit = xpath(first_node)[0]
+                first_value = float(first_node.get('value'))
+                second_node = unit_conversions[1]
+                second_unit = xpath(second_node)[0]
+                second_value = float(second_node.get('value'))
+                e.description = ' '.join((str(first_value), first_unit, '=', str(second_value), second_unit))
+            except Exception:
+                return None
+            else:
+                return e
+
+        # check for currency conversion card
+        if 'currency' in node.get('class', ''):
+            currency_selectors = node.xpath(".//div[@class='ccw_unit_selector_cnt']")
+            if len(currency_selectors) == 2:
+                e.title = 'Currency Conversion'
+                # Inside this <div> is a <select> with <option selected="1"> nodes
+                # just like the unit conversion card.
+
+                first_node = currency_selectors[0]
+                first_currency = first_node.find("./select/option[@selected='1']")
+
+                second_node = currency_selectors[1]
+                second_currency = second_node.find("./select/option[@selected='1']")
+
+                # The parent of the nodes have a <input class='vk_gy vk_sh ccw_data' value=...>
+                xpath = etree.XPath("parent::td/parent::tr/td/input[@class='vk_gy vk_sh ccw_data']")
+                try:
+                    first_value = float(xpath(first_node)[0].get('value'))
+                    second_value = float(xpath(second_node)[0].get('value'))
+
+                    values = (
+                        str(first_value),
+                        first_currency.text,
+                        f'({first_currency.get("value")})',
+                        '=',
+                        str(second_value),
+                        second_currency.text,
+                        f'({second_currency.get("value")})'
+                    )
+                    e.description = ' '.join(values)
+                except Exception:
+                    return None
+                else:
+                    return e
+
+        # check for generic information card
+        info = node.find(".//div[@class='_f2g']")
+        if info is not None:
+            try:
+                e.title = ''.join(info.itertext()).strip()
+                actual_information = info.xpath("parent::div/parent::div//div[@class='_XWk' or contains(@class, 'kpd-ans')]")[0]
+                e.description = ''.join(actual_information.itertext()).strip()
+            except Exception:
+                return None
+            else:
+                return e
+
+        # check for translation card
+        translation = node.find(".//div[@id='tw-ob']")
+        if translation is not None:
+            src_text = translation.find(".//pre[@id='tw-source-text']/span")
+            src_lang = translation.find(".//select[@id='tw-sl']/option[@selected='1']")
+
+            dest_text = translation.find(".//pre[@id='tw-target-text']/span")
+            dest_lang = translation.find(".//select[@id='tw-tl']/option[@selected='1']")
+
+            # TODO: bilingual dictionary nonsense?
+
+            e.title = 'Translation'
+            try:
+                e.add_field(name=src_lang.text, value=src_text.text, inline=True)
+                e.add_field(name=dest_lang.text, value=dest_text.text, inline=True)
+            except Exception:
+                return None
+            else:
+                return e
+
+        # check for "time in" card
+        time = node.find("./div[@class='vk_bk vk_ans']")
+        if time is not None:
+            date = node.find("./div[@class='vk_gy vk_sh']")
+            try:
+                e.title = node.find('span').text
+                e.description = f'{time.text}\n{"".join(date.itertext()).strip()}'
+            except Exception:
+                return None
+            else:
+                return e
+
+        # time in has an alternative form without spans
+        time = node.find("./div/div[@class='vk_bk vk_ans _nEd']")
+        if time is not None:
+            converted = "".join(time.itertext()).strip()
+            try:
+                # remove the in-between text
+                parent = time.getparent()
+                parent.remove(time)
+                original = "".join(parent.itertext()).strip()
+                e.title = 'Time Conversion'
+                e.description = f'{original}...\n{converted}'
+            except Exception:
+                return None
+            else:
+                return e
+
+        # check for definition card
+        words = node.xpath(".//span[@data-dobid='hdw']")
+        if words:
+            lex = etree.XPath(".//div[@class='lr_dct_sf_h']/i/span")
+
+            # this one is derived if we were based on the position from lex
+            xpath = etree.XPath("../../../ol[@class='lr_dct_sf_sens']//" \
+                                "div[not(@class and @class='lr_dct_sf_subsen')]/" \
+                                "div[@class='_Jig']/div[@data-dobid='dfn']/span")
+            for word in words:
+                # we must go two parents up to get the root node
+                root = word.getparent().getparent()
+
+                pronunciation = root.find(".//span[@class='lr_dct_ph']/span")
+                if pronunciation is None:
+                    continue
+
+                lexical_category = lex(root)
+                definitions = xpath(root)
+
+                for category in lexical_category:
+                    definitions = xpath(category)
+                    try:
+                        descrip = [f'*{category.text}*']
+                        for index, value in enumerate(definitions, 1):
+                            descrip.append(f'{index}. {value.text}')
+
+                        e.add_field(name=f'{word.text} /{pronunciation.text}/', value='\n'.join(descrip))
+                    except:
+                        continue
+
+            return e
+
+        # check for weather card
+        location = node.find("./div[@id='wob_loc']")
+        if location is None:
+            return None
+
+
+        # these units should be metric
+
+        date = node.find("./div[@id='wob_dts']")
+
+        # <img alt="category here" src="cool image">
+        category = node.find(".//img[@id='wob_tci']")
+
+        xpath = etree.XPath(".//div[@id='wob_d']//div[contains(@class, 'vk_bk')]//span[@class='wob_t']")
+        temperatures = xpath(node)
+
+        misc_info_node = node.find(".//div[@class='vk_gy vk_sh wob-dtl']")
+
+        if misc_info_node is None:
+            return None
+
+        precipitation = misc_info_node.find("./div/span[@id='wob_pp']")
+        humidity = misc_info_node.find("./div/span[@id='wob_hm']")
+        wind = misc_info_node.find("./div/span/span[@id='wob_tws']")
+
+
+        try:
+            e.title = 'Weather for ' + location.text.strip()
+            e.description = f'*{category.get("alt")}*'
+            e.set_thumbnail(url='https:' + category.get('src'))
+
+            if len(temperatures) == 4:
+                first_unit = temperatures[0].text + temperatures[2].text
+                second_unit = temperatures[1].text + temperatures[3].text
+                units = f'{first_unit} | {second_unit}'
+            else:
+                units = 'Unknown'
+
+            e.add_field(name='Temperature', value=units, inline=False)
+
+            if precipitation is not None:
+                e.add_field(name='Precipitation', value=precipitation.text)
+
+            if humidity is not None:
+                e.add_field(name='Humidity', value=humidity.text)
+
+            if wind is not None:
+                e.add_field(name='Wind', value=wind.text)
+        except:
+            return None
+
+        return e
+
+    async def get_google_entries(self, query):
+        url = f'https://www.google.com/search?q={uriquote(query)}'
+        params = {
+            'safe': 'on',
+            'lr': 'lang_en',
+            'hl': 'en'
+        }
+
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) Gecko/20100101 Firefox/53.0'
+        }
+
+        # list of URLs and title tuples
+        entries = []
+
+        # the result of a google card, an embed
+        card = None
+
+        async with self.bot.session.get(url, params=params, headers=headers) as resp:
+            if resp.status != 200:
+                log.info('Google failed to respond with %s status code.', resp.status)
+                raise RuntimeError('Google has failed to respond.')
+
+            root = etree.fromstring(await resp.text(), etree.HTMLParser())
+
+            # for bad in root.xpath('//style'):
+            #     bad.getparent().remove(bad)
+
+            # for bad in root.xpath('//script'):
+            #     bad.getparent().remove(bad)
+
+            # with open('google.html', 'w', encoding='utf-8') as f:
+            #     f.write(etree.tostring(root, pretty_print=True).decode('utf-8'))
+
+            """
+            Tree looks like this.. sort of..
+
+            <div class="rc">
+                <h3 class="r">
+                    <a href="url here">title here</a>
+                </h3>
+            </div>
+            """
+
+            card_node = root.xpath(".//div[@id='rso']/div[@class='_NId']//" \
+                                   "div[contains(@class, 'vk_c') or @class='g mnr-c g-blk' or @class='kp-blk']")
+
+            if card_node is None or len(card_node) == 0:
+                card = None
+            else:
+                card = self.parse_google_card(card_node[0])
+
+            search_results = root.findall(".//div[@class='rc']")
+            # print(len(search_results))
+            for node in search_results:
+                link = node.find("./h3[@class='r']/a")
+                if link is not None:
+                    # print(etree.tostring(link, pretty_print=True).decode())
+                    entries.append((link.get('href'), link.text))
+
+        return card, entries
+
+    @commands.command(aliases=['google'])
+    async def g(self, ctx, *, query):
+        """Searches google and gives you top result."""
+        await ctx.trigger_typing()
+        try:
+            card, entries = await self.get_google_entries(query)
+        except RuntimeError as e:
+            await ctx.send(str(e))
+        else:
+            if card:
+                value = '\n'.join(f'[{title}]({url.replace(")", "%29")})' for url, title in entries[:3])
+                if value:
+                    card.add_field(name='Search Results', value=value, inline=False)
+                return await ctx.send(embed=card)
+
+            if len(entries) == 0:
+                return await ctx.send('No results found... sorry.')
+
+            next_two = [x[0] for x in entries[1:3]]
+            first_entry = entries[0][0]
+            if first_entry[-1] == ')':
+                first_entry = first_entry[:-1] + '%29'
+
+            if next_two:
+                formatted = '\n'.join(f'<{x}>' for x in next_two)
+                msg = f'{first_entry}\n\n**See also:**\n{formatted}'
+            else:
+                msg = first_entry
+
+            await ctx.send(msg)
+
+    @commands.command()
+    @commands.is_owner()
+    async def pm(self, ctx, user_id: int, *, content: str):
+        user = self.bot.get_user(user_id)
+
+        fmt = content + '\n\n*This is a DM sent because you had previously requested feedback or I found a bug' \
+                        ' in a command you used, I do not monitor this DM.*'
+        try:
+            await user.send(fmt)
+        except:
+            await ctx.send(f'Could not PM user by ID {user_id}.')
+        else:
+            await ctx.send('PM successfully sent.')
+
+    async def redirect_post(self, ctx, title, text):
+        storage = self.bot.get_guild(182325885867786241).get_channel(430229522340773899)
+
+        supported_attachments = ('.png', '.jpg', '.jpeg', '.webm', '.gif', '.mp4', '.txt')
+        if not all(attach.filename.lower().endswith(supported_attachments) for attach in ctx.message.attachments):
+            raise RuntimeError(f'Unsupported file in attachments. Only {", ".join(supported_attachments)} supported.')
+
+        files = []
+        total_bytes = 0
+        eight_mib = 8 * 1024 * 1024
+        for attach in ctx.message.attachments:
+            async with ctx.session.get(attach.url) as resp:
+                if resp.status != 200:
+                    continue
+
+                content_length = int(resp.headers.get('Content-Length'))
+
+                # file too big, skip it
+                if (total_bytes + content_length) > eight_mib:
+                    continue
+
+                total_bytes += content_length
+                fp = io.BytesIO(await resp.read())
+                files.append(discord.File(fp, filename=attach.filename))
+
+            if total_bytes >= eight_mib:
+                break
+
+        await ctx.message.delete()
+        data = discord.Embed(title=title)
+        if text:
+            data.description = text
+
+        data.set_author(name=ctx.author.id)
+        data.set_footer(text=ctx.channel.id)
+
+        try:
+            message = await storage.send(embed=data, files=files)
+        except discord.HTTPException as e:
+            raise RuntimeError(f'Sorry. Could not store message due to {e.__class__.__name__}: {e}.') from e
+
+        to_dict = {
+            'author_id': ctx.author.id,
+            'channel_id': ctx.channel.id,
+            'attachments': message.attachments,
+            'title': title,
+            'text': text
+        }
+
+        cache = SpoilerCache(to_dict)
+        return message, cache
+
+    async def get_spoiler_cache(self, channel_id, message_id):
+        try:
+            return self._spoiler_cache[message_id]
+        except KeyError:
+            pass
+
+        storage = self.bot.get_guild(182325885867786241).get_channel(430229522340773899)
+
+        # slow path requires 2 lookups
+        # first is looking up the message_id of the original post
+        # to get the embed footer information which points to the storage message ID
+        # the second is getting the storage message ID and extracting the information from it
+        channel = self.bot.get_channel(channel_id)
+        if not channel:
+            return None
+
+        try:
+            original_message = await channel.get_message(message_id)
+            storage_message_id = int(original_message.embeds[0].footer.text)
+            message = await storage.get_message(storage_message_id)
+        except:
+            # this message is probably not the proper format or the storage died
+            return None
+
+        data = message.embeds[0]
+        to_dict = {
+            'author_id': int(data.author.name),
+            'channel_id': int(data.footer.text),
+            'attachments': message.attachments,
+            'title': data.title,
+            'text': None if not data.description else data.description
+        }
+        cache = SpoilerCache(to_dict)
+        self._spoiler_cache[message_id] = cache
+        return cache
+
+    async def on_raw_reaction_add(self, payload):
+        if payload.emoji.id != SPOILER_EMOJI_ID:
+            return
+
+        user = self.bot.get_user(payload.user_id)
+        if not user or user.bot:
+            return
+
+        if self._spoiler_cooldown.is_rate_limited(payload.message_id, payload.user_id):
+            return
+
+        cache = await self.get_spoiler_cache(payload.channel_id, payload.message_id)
+        embed = cache.to_embed(self.bot)
+        await user.send(embed=embed)
+
+    @commands.command()
+    @can_use_spoiler()
+    async def spoiler(self, ctx, title, *, text=None):
+        """Marks your post a spoiler with a title.
+
+        Once your post is marked as a spoiler it will be
+        automatically deleted and the bot will DM those who
+        opt-in to view the spoiler.
+
+        The only media types supported are png, gif, jpeg, mp4,
+        and webm.
+
+        Only 8MiB of total media can be uploaded at once.
+        Sorry, Discord limitation.
+
+        To opt-in to a post's spoiler you must click the reaction.
+        """
+
+        if len(title) > 100:
+            return await ctx.send('Sorry. Title has to be shorter than 100 characters.')
+
+        try:
+            storage_message, cache = await self.redirect_post(ctx, title, text)
+        except Exception as e:
+            return await ctx.send(str(e))
+
+        spoiler_message = await ctx.send(embed=cache.to_spoiler_embed(ctx, storage_message))
+        self._spoiler_cache[spoiler_message.id] = cache
+        await spoiler_message.add_reaction(':spoiler:430469957042831371')
+
+def setup(bot):
+    bot.add_cog(Buttons(bot))
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/config.py b/All-In-One-bot/cogs/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..28e2d570f42ca51ae1329685cabb5428b54f448c
--- /dev/null
+++ b/All-In-One-bot/cogs/config.py
@@ -0,0 +1,425 @@
+from discord.ext import commands
+from .utils import db, checks, cache
+from .utils.paginator import Pages
+
+from collections import defaultdict
+
+
+class LazyEntity:
+    """This is meant for use with the Paginator.
+
+    It lazily computes __str__ when requested and
+    caches it so it doesn't do the lookup again.
+    """
+    __slots__ = ('entity_id', 'guild', '_cache')
+
+    def __init__(self, guild, entity_id):
+        self.entity_id = entity_id
+        self.guild = guild
+        self._cache = None
+
+    def __str__(self):
+        if self._cache:
+            return self._cache
+
+        e = self.entity_id
+        g = self.guild
+        resolved = g.get_channel(e) or g.get_member(e)
+        if resolved is None:
+            self._cache = f'<Not Found: {e}>'
+        else:
+            self._cache = resolved.mention
+        return self._cache
+
+class ChannelOrMember(commands.Converter):
+    async def convert(self, ctx, argument):
+        try:
+            return await commands.TextChannelConverter().convert(ctx, argument)
+        except commands.BadArgument:
+            return await commands.MemberConverter().convert(ctx, argument)
+
+class Plonks(db.Table):
+    id = db.PrimaryKeyColumn()
+    guild_id = db.Column(db.Integer(big=True), index=True)
+
+    # this can either be a channel_id or an author_id
+    entity_id = db.Column(db.Integer(big=True), index=True, unique=True)
+
+class CommandConfig(db.Table, table_name='command_config'):
+    id = db.PrimaryKeyColumn()
+
+    guild_id = db.Column(db.Integer(big=True), index=True)
+    channel_id = db.Column(db.Integer(big=True))
+
+    name = db.Column(db.String)
+    whitelist = db.Column(db.Boolean)
+
+    @classmethod
+    def create_table(cls, *, exists_ok=True):
+        statement = super().create_table(exists_ok=exists_ok)
+        # create the unique index
+        sql = "CREATE UNIQUE INDEX IF NOT EXISTS command_config_uniq_idx ON command_config (channel_id, name, whitelist);"
+        return statement + '\n' + sql
+
+class CommandName(commands.Converter):
+    async def convert(self, ctx, argument):
+        lowered = argument.lower()
+
+        valid_commands = {
+            c.qualified_name
+            for c in ctx.bot.walk_commands()
+            if c.cog_name not in ('Config', 'Admin')
+        }
+
+        if lowered not in valid_commands:
+            raise commands.BadArgument('That command name is not valid.')
+
+        return lowered
+
+class ResolvedCommandPermissions:
+    class _Entry:
+        __slots__ = ('allow', 'deny')
+        def __init__(self):
+            self.allow = set()
+            self.deny = set()
+
+    def __init__(self, guild_id, records):
+        self.guild_id = guild_id
+
+        self._lookup = defaultdict(self._Entry)
+
+        # channel_id: { allow: [commands], deny: [commands] }
+
+        for name, channel_id, whitelist in records:
+            entry = self._lookup[channel_id]
+            if whitelist:
+                entry.allow.add(name)
+            else:
+                entry.deny.add(name)
+
+    def _split(self, obj):
+        # "hello there world" -> ["hello", "hello there", "hello there world"]
+        from itertools import accumulate
+        return list(accumulate(obj.split(), lambda x, y: f'{x} {y}'))
+
+    def is_blocked(self, ctx):
+        # fast path
+        if len(self._lookup) == 0:
+            return False
+
+        if ctx.author.guild_permissions.manage_guild:
+            return False
+
+        command_names = self._split(ctx.command.qualified_name)
+
+        guild = self._lookup[None] # no special channel_id
+        channel = self._lookup[ctx.channel.id]
+
+        blocked = None
+
+        # apply guild-level denies first
+        # then guild-level allow
+        # then channel-level deny
+        # then channel-level allow
+
+        # use ?foo bar
+        # ?foo bar <- guild allow
+        # ?foo <- channel block
+        # result: blocked
+        # this is why the two for loops are separate
+
+        for command in command_names:
+            if command in guild.deny:
+                blocked = True
+
+            if command in guild.allow:
+                blocked = False
+
+        for command in command_names:
+            if command in channel.deny:
+                blocked = True
+
+            if command in channel.allow:
+                blocked = False
+
+        return blocked
+
+class Config:
+    """Handles the bot's configuration system.
+
+    This is how you disable or enable certain commands
+    for your server or block certain channels or members.
+    """
+
+    def __init__(self, bot):
+        self.bot = bot
+
+    async def is_plonked(self, guild_id, member_id, *, channel_id=None, connection=None, check_bypass=True):
+        if check_bypass:
+            guild = self.bot.get_guild(guild_id)
+            if guild is not None:
+                member = guild.get_member(member_id)
+                if member is not None and member.guild_permissions.manage_guild:
+                    return False
+
+        connection = connection or self.bot.pool
+
+        if channel_id is None:
+            query = "SELECT 1 FROM plonks WHERE guild_id=$1 AND entity_id=$2;"
+            row = await connection.fetchrow(query, guild_id, member_id)
+        else:
+            query = "SELECT 1 FROM plonks WHERE guild_id=$1 AND entity_id IN ($2, $3);"
+            row = await connection.fetchrow(query, guild_id, member_id, channel_id)
+
+        return row is not None
+
+    async def __global_check_once(self, ctx):
+        if ctx.guild is None:
+            return True
+
+        is_owner = await ctx.bot.is_owner(ctx.author)
+        if is_owner:
+            return True
+
+        # see if they can bypass:
+        bypass = ctx.author.guild_permissions.manage_guild
+        if bypass:
+            return True
+
+        # check if we're plonked
+        is_plonked = await self.is_plonked(ctx.guild.id, ctx.author.id, channel_id=ctx.channel.id,
+                                                                        connection=ctx.db, check_bypass=False)
+
+        return not is_plonked
+
+    @cache.cache(strategy=cache.Strategy.raw)
+    async def get_command_permissions(self, guild_id, *, connection=None):
+        connection = connection or self.bot.pool
+        query = "SELECT name, channel_id, whitelist FROM command_config WHERE guild_id=$1;"
+
+        records = await connection.fetch(query, guild_id)
+        return ResolvedCommandPermissions(guild_id, records)
+
+    async def __global_check(self, ctx):
+        if ctx.guild is None:
+            return True
+
+        is_owner = await ctx.bot.is_owner(ctx.author)
+        if is_owner:
+            return True
+
+        resolved = await self.get_command_permissions(ctx.guild.id, connection=ctx.db)
+        return not resolved.is_blocked(ctx)
+
+    async def _bulk_ignore_entries(self, ctx, entries):
+        async with ctx.db.transaction():
+            query = "SELECT entity_id FROM plonks WHERE guild_id=$1;"
+            records = await ctx.db.fetch(query, ctx.guild.id)
+
+            # we do not want to insert duplicates
+            current_plonks = {r[0] for r in records}
+            guild_id = ctx.guild.id
+            to_insert = [(guild_id, e.id) for e in entries if e.id not in current_plonks]
+
+            # do a bulk COPY
+            await ctx.db.copy_records_to_table('plonks', columns=('guild_id', 'entity_id'), records=to_insert)
+
+    async def __error(self, ctx, error):
+        if isinstance(error, commands.BadArgument):
+            await ctx.send(error)
+
+    @commands.group()
+    async def config(self, ctx):
+        """Handles the server or channel permission configuration for the bot."""
+        if ctx.invoked_subcommand is None:
+            await ctx.show_help('config')
+
+    @config.group(invoke_without_command=True, aliases=['plonk'])
+    @checks.is_mod()
+    async def ignore(self, ctx, *entities: ChannelOrMember):
+        """Ignores text channels or members from using the bot.
+
+        If no channel or member is specified, the current channel is ignored.
+
+        Users with Manage Server can still use the bot, regardless of ignore
+        status.
+
+        To use this command you must have Manage Server permissions.
+        """
+
+        if len(entities) == 0:
+            # shortcut for a single insert
+            query = "INSERT INTO plonks (guild_id, entity_id) VALUES ($1, $2) ON CONFLICT DO NOTHING;"
+            await ctx.db.execute(query, ctx.guild.id, ctx.channel.id)
+        else:
+            await self._bulk_ignore_entries(ctx, entities)
+
+        await ctx.send(ctx.tick(True))
+
+    @ignore.command(name='list')
+    @checks.is_mod()
+    @commands.cooldown(2.0, 60.0, commands.BucketType.guild)
+    async def ignore_list(self, ctx):
+        """Tells you what channels or members are currently ignored in this server.
+
+        To use this command you must have Manage Server permissions.
+        """
+
+        query = "SELECT entity_id FROM plonks WHERE guild_id=$1;"
+
+        guild = ctx.guild
+        records = await ctx.db.fetch(query, guild.id)
+
+        if len(records) == 0:
+            return await ctx.send('I am not ignoring anything here.')
+
+        entries = [LazyEntity(guild, r[0]) for r in records]
+        await ctx.release()
+
+        try:
+            p = Pages(ctx, entries=entries, per_page=20)
+            await p.paginate()
+        except Exception as e:
+            await ctx.send(str(e))
+
+    @ignore.command(name='all')
+    @checks.is_mod()
+    async def _all(self, ctx):
+        """Ignores every channel in the server from being processed.
+
+        This works by adding every channel that the server currently has into
+        the ignore list. If more channels are added then they will have to be
+        ignored by using the ignore command.
+
+        To use this command you must have Manage Server permissions.
+        """
+        await self._bulk_ignore_entries(ctx, ctx.guild.text_channels)
+        await ctx.send('Successfully blocking all channels here.')
+
+    @ignore.command(name='clear')
+    @checks.is_mod()
+    async def ignore_clear(self, ctx):
+        """Clears all the currently set ignores.
+
+        To use this command you must have Manage Server permissions.
+        """
+
+        query = "DELETE FROM plonks WHERE guild_id=$1;"
+        await ctx.db.execute(query, ctx.guild.id)
+        await ctx.send('Successfully cleared all ignores.')
+
+    @config.group(pass_context=True, invoke_without_command=True, aliases=['unplonk'])
+    @checks.is_mod()
+    async def unignore(self, ctx, *entities: ChannelOrMember):
+        """Allows channels or members to use the bot again.
+
+        If nothing is specified, it unignores the current channel.
+
+        To use this command you must have the Manage Server permission.
+        """
+
+        if len(entities) == 0:
+            query = "DELETE FROM plonks WHERE guild_id=$1 AND entity_id=$2;"
+            await ctx.db.execute(query, ctx.guild.id, ctx.channel.id)
+        else:
+            query = "DELETE FROM plonks WHERE guild_id=$1 AND entity_id = ANY($2::bigint[]);"
+            entities = [c.id for c in entities]
+            await ctx.db.execute(query, ctx.guild.id, entities)
+
+        await ctx.send(ctx.tick(True))
+
+    @unignore.command(name='all')
+    @checks.is_mod()
+    async def unignore_all(self, ctx):
+        """An alias for ignore clear command."""
+        await ctx.invoke(self.ignore_clear)
+
+    @config.group(aliases=['guild'])
+    @checks.is_mod()
+    async def server(self, ctx):
+        """Handles the server-specific permissions."""
+        pass
+
+    @config.group()
+    @checks.is_mod()
+    async def channel(self, ctx):
+        """Handles the channel-specific permissions."""
+        pass
+
+    async def command_toggle(self, connection, guild_id, channel_id, name, *, whitelist=True):
+        # clear the cache
+        self.get_command_permissions.invalidate(self, guild_id)
+
+        query = "DELETE FROM command_config WHERE guild_id=$1 AND name=$2 AND channel_id=$3 AND whitelist=$4;"
+
+        # DELETE <num>
+        status = await connection.execute(query, guild_id, name, channel_id, whitelist)
+        if status[-1] != '0':
+            return
+
+        query = "INSERT INTO command_config (guild_id, channel_id, name, whitelist) VALUES ($1, $2, $3, $4);"
+
+        try:
+            await connection.execute(query, guild_id, channel_id, name, whitelist)
+        except asyncpg.UniqueViolationError:
+            msg = 'This command is already disabled.' if not whitelist else 'This command is already explicitly enabled.'
+            raise RuntimeError('This command is already disabled.')
+
+    @channel.command(name='disable')
+    async def channel_disable(self, ctx, *, command: CommandName):
+        """Disables a command for this channel."""
+
+        try:
+            await self.command_toggle(ctx.db, ctx.guild.id, ctx.channel.id, command, whitelist=False)
+        except RuntimeError as e:
+            await ctx.send(e)
+        else:
+            await ctx.send('Command successfully disabled for this channel.')
+
+    @channel.command(name='enable')
+    async def channel_enable(self, ctx, *, command: CommandName):
+        """Enables a command for this channel."""
+
+        try:
+            await self.command_toggle(ctx.db, ctx.guild.id, ctx.channel.id, command, whitelist=True)
+        except RuntimeError as e:
+            await ctx.send(e)
+        else:
+            await ctx.send('Command successfully enabled for this channel.')
+
+    @server.command(name='disable')
+    async def server_disable(self, ctx, *, command: CommandName):
+        """Disables a command for this server."""
+
+        try:
+            await self.command_toggle(ctx.db, ctx.guild.id, None, command, whitelist=False)
+        except RuntimeError as e:
+            await ctx.send(e)
+        else:
+            await ctx.send('Command successfully disabled for this server')
+
+    @server.command(name='enable')
+    async def server_enable(self, ctx, *, command: CommandName):
+        """Enables a command for this server."""
+
+        try:
+            await self.command_toggle(ctx.db, ctx.guild.id, None, command, whitelist=True)
+        except RuntimeError as e:
+            await ctx.send(e)
+        else:
+            await ctx.send('Command successfully enabled for this server.')
+
+    @config.command(name='enable')
+    @checks.is_mod()
+    async def config_enable(self, ctx, *, command: CommandName):
+        """Enables a command for this server."""
+        await ctx.invoke(self.server_enable, command=command)
+
+    @config.command(name='disable')
+    @checks.is_mod()
+    async def config_disable(self, ctx, *, command: CommandName):
+        """Disables a command for this server."""
+        await ctx.invoke(self.server_disable, command=command)
+
+def setup(bot):
+    bot.add_cog(Config(bot))
diff --git a/All-In-One-bot/cogs/general.py b/All-In-One-bot/cogs/general.py
index 38dc6bbc580440ae37faef9b27e9efc6ed2dd35c..9c76697a8ce41db5f1dd9a1fdbfd52329152ec04 100644
--- a/All-In-One-bot/cogs/general.py
+++ b/All-In-One-bot/cogs/general.py
@@ -10,31 +10,31 @@ class General:
 	def __init__(self, bot):
 		self.bot = bot
 
-	@commands.command(name="ping",brief="Ping pong")
-	async def ping(self):
-		await self.bot.say("Pong!")
+	@commands.command(name="ping", brief="Ping pong")
+	async def ping(self, ctx):
+		await ctx.say("Pong!")
 
 	@commands.group(name="faq", brief="Frequently Asked Questions", pass_context=True)
-	async def frequently_asked_questions(self, context):
-		if context.invoked_subcommand is None:
+	async def frequently_asked_questions(self, ctx):
+		if ctx.invoked_subcommand is None:
 			embed = discord.Embed(title="Frequently Asked Questions")
 			embed.add_field(name="Want to know about the requests system?", value="Just execute this command with with `requests` after it.", inline=False)
-			await self.bot.say(embed=embed)
+			await ctx.say(embed=embed)
 		pass
 
 	@frequently_asked_questions.command()
-	async def requests(self):
+	async def requests(self, ctx):
 		embed=discord.Embed(title="Plex Media Request System FAQ", color=0xdc1111)
 		embed.add_field(name="1. Where can I find it?", value="It's located at: https://home.samip.fi/pmrs/", inline=False)
 		embed.add_field(name="2. How do I log-on?", value="You login with your plex.tv credentials.\nThe admin wont see it as it's securely checked against plex.tv API.", inline=False)
 		embed.set_footer(text="Generated by samip5's code.")
-		await self.bot.say(embed=embed)
+		await ctx.say(embed=embed)
 
 	@commands.group(name='dm-faq', pass_context=True)
 	async def direct_message_frequently_asked_questions(self, ctx):
 		if ctx.invoked_subcommand is None:
 			message = "This is not supposed to be called without anything after it. Please fix your command and try again."
-			await self.bot.say(message)
+			await ctx.say(message)
 		pass
 
 	@direct_message_frequently_asked_questions.command(pass_context=True)
@@ -44,7 +44,7 @@ class General:
 		embed.add_field(name="1. Where can I find it?", value="It's located at: https://home.samip.fi/pmrs/", inline=False)
 		embed.add_field(name="2. How do I log-on?", value="You login with your plex.tv credentials.\nThe admin wont see it as it's securely checked against plex.tv API.", inline=False)
 		embed.set_footer(text=footer)
-		await self.bot.send_message(member, embed=embed)
+		await ctx.messegable.send(member, embed=embed)
 
 
 def setup(bot):
diff --git a/All-In-One-bot/cogs/members.py b/All-In-One-bot/cogs/members.py
deleted file mode 100644
index 3c48179fa10e60e97a9a8c74f6a7b4c5cc3382d8..0000000000000000000000000000000000000000
--- a/All-In-One-bot/cogs/members.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import discord
-from discord.ext import commands
-
-
-class Members:
-    def __init__(self, bot):
-        self.bot = bot
-
-    @commands.command()
-    async def joined(self, ctx, *, member: discord.Member):
-        """Says when a member joined."""
-        await ctx.send(f'{member.display_name} joined on {member.joined_at}')
-
-    @commands.command(name='top_role', aliases=['toprole'])
-    async def show_toprole(self, ctx, *, member: discord.Member=None):
-        """Simple command which shows the members Top Role."""
-
-        if member is None:
-            member = ctx.author
-
-        await ctx.send(f'The top role for {member.display_name} is {member.top_role.name}')
-
-    @commands.command(name='perms', aliases=['perms_for', 'permissions'])
-    async def check_permissions(self, ctx, *, member: discord.Member=None):
-        """A simple command which checks a members Guild Permissions.
-        If member is not provided, the author will be checked."""
-
-        if not member:
-            member = ctx.author
-
-        # Here we check if the value of each permission is True.
-        perms = '\n'.join(perm for perm, value in member.guild_permissions if value)
-
-        # And to make it look nice, we wrap it in an Embed.
-        embed = discord.Embed(title='Permissions for:', description=ctx.guild.name, colour=member.colour)
-        embed.set_author(icon_url=member.avatar_url, name=str(member))
-
-        # \uFEFF is a Zero-Width Space, which basically allows us to have an empty field name.
-        embed.add_field(name='\uFEFF', value=perms)
-
-        await ctx.send(content=None, embed=embed)
-        # Thanks to Gio for the Command.
-
-
-def setup(bot):
-    bot.add_cog(Members(bot))
diff --git a/All-In-One-bot/cogs/meta.py b/All-In-One-bot/cogs/meta.py
index ea9f78e8bc1c4f33b6524693fbd670da2de8319e..0d508e95cde732260446b1090fe2cb13223addea 100644
--- a/All-In-One-bot/cogs/meta.py
+++ b/All-In-One-bot/cogs/meta.py
@@ -60,13 +60,13 @@ class Meta:
 			reminder = 'Okay {0.mention}, I\'ll remind you about "{2}" in {1.seconds} seconds.'
 			completed = 'Time is up {0.mention}! You asked to be reminded about "{1}".'
 
-		await self.bot.say(reminder.format(author, time, message))
+		await ctx.send(reminder.format(author, time, message))
 		await asyncio.sleep(time.seconds)
-		await self.bot.say(completed.format(author, message))
+		await ctx.send(completed.format(author, message))
 
 	@commands.command(name='quit', hidden=True)
-	@checks.is_owner()
-	async def _quit(self):
+	@checks.am_i_owner()
+	async def _quit(self, ctx):
 		"""Quits the bot."""
 		await self.bot.logout()
 
@@ -78,7 +78,7 @@ class Meta:
 		"""
 		channel = ctx.message.channel
 		if channel.is_private:
-			await self.bot.say('You cannot use this in PMs.')
+			await ctx.send('You cannot use this in PMs.')
 			return
 
 		if member is None:
@@ -129,12 +129,12 @@ class Meta:
 		return fmt.format(d=days, h=hours, m=minutes, s=seconds)
 
 	@commands.command()
-	async def uptime(self):
+	async def uptime(self, ctx):
 		"""Tells you how long the bot has been up for."""
-		await self.bot.say('Uptime: **{}**'.format(self.get_bot_uptime()))
+		await ctx.send('Uptime: **{}**'.format(self.get_bot_uptime()))
 
 	@commands.command(name="about")
-	async def about_me(self):
+	async def about_me(self, ctx):
 		"""Tells you information about the bot itself."""
 		result = ['**About Me:**']
 		result.append('- Author: samip537 (Discord ID: 157970669261422592, Github: samip5)')
@@ -144,7 +144,7 @@ class Meta:
 		changes = os.popen(r'git show -s HEAD~3..HEAD --format="[%h](https://github.com/samip5/Discord-Bots/commit/%H) %s (%cr)"').read().strip()
 		result.append('- Changes: {}'.format(changes))
 		result.append('- Uptime: {}'.format(self.get_bot_uptime()))
-		result.append('- Servers: {}'.format(len(self.bot.servers)))
+		result.append('- Servers: {}'.format(len(self.bot.guilds)))
 		# stats
 		total_members = sum(len(s.members) for s in self.bot.servers)
 		total_online = sum(1 for m in self.bot.get_all_members() if m.status != discord.Status.offline)
@@ -156,7 +156,7 @@ class Meta:
 		result.append('- Total Members: {} ({} online)'.format(total_members, total_online))
 		result.append('- Unique Members: {} ({} online)'.format(len(unique_members), unique_online))
 		result.append('- {} text channels, {} voice channels'.format(text, voice))
-		await self.bot.say('\n'.join(result))
+		await ctx.send('\n'.join(result))
 
 
 def setup(bot):
diff --git a/All-In-One-bot/cogs/misc.py b/All-In-One-bot/cogs/misc.py
index db29bf0464c84a9a202e9576433f91943ac67aae..bbb94285e12d85b13e4b14000604f5f463eb2d3f 100644
--- a/All-In-One-bot/cogs/misc.py
+++ b/All-In-One-bot/cogs/misc.py
@@ -10,17 +10,17 @@ class Misc:
 		self.bot = bot
 
 	@commands.command(name='8ball', description="Answers a yes/no question.", brief="Answers from the beyond.", pass_context=True)
-	async def answer_machine(self, context):
+	async def answer_machine(self, ctx):
 		responses = [
 			'That is a resounding no',
 			'It is not looking likely',
 			'Too hard to tell',
 			'It is quite possible',
 			'Definitely', ]
-		await self.bot.say(random.choice(responses) + ", " + context.message.author.mention)
+		await ctx.say(random.choice(responses) + ", " + ctx.message.author.mention)
 
 	@commands.command(name='facepalmed', brief="Command says everything needed.", pass_context=True)
-	async def facepalm(self):
+	async def facepalm(self, ctx):
 		urls = [
 			'https://media1.giphy.com/media/AjYsTtVxEEBPO/giphy.gif',
 			'https://media2.giphy.com/media/3og0INyCmHlNylks9O/giphy.gif',
@@ -31,10 +31,10 @@ class Misc:
 		f = discord.Embed(color=random.randint(1, 255 ** 3 - 1))
 		f.set_image(url=thumb_url)
 		f.set_footer(text="Powered by Giphy.")
-		await self.bot.say(embed=f)
+		await ctx.say(embed=f)
 
 	@commands.command(name='cat', description="Gives you a random cat picture", brief="Returns random image of a cat")
-	async def randomcat(self):
+	async def randomcat(self, ctx):
 		async with aiohttp.ClientSession() as ses:
 			async with ses.get('https://aws.random.cat/meow') as response:
 				ret = await response.json()
@@ -42,7 +42,7 @@ class Misc:
 		e.set_image(url=ret['file'])
 		e.set_author(name="Random.cat", url='https://random.cat/')
 		e.set_footer(text="Powered by random.cat")
-		await self.bot.say(embed=e)
+		await ctx.say(embed=e)
 
 	async def on_member_update(self, before, after):
 		if before.nick != after.nick:
@@ -63,12 +63,12 @@ class Misc:
 				await self.bot.db.commit()
 
 		pass
-		print(f'Before nick: {before.nick}')
-		print(f'Before display_name: {before.display_name}')
-		print(f'Before id: {before.id}')
-		print(f'After nick: {after.nick}')
-		print(f'After display_name: {after.display_name}')
-		print(f'After id: {after.id}')
+		# print(f'Before nick: {before.nick}')
+		# print(f'Before display_name: {before.display_name}')
+		# print(f'Before id: {before.id}')
+		# print(f'After nick: {after.nick}')
+		# print(f'After display_name: {after.display_name}')
+		# print(f'After id: {after.id}')
 
 
 def setup(bot):
diff --git a/All-In-One-bot/cogs/mod.py b/All-In-One-bot/cogs/mod.py
index f7808c0903852c94a0b39f706e332404ea0a0401..06f75c9455c32286a5ea463adc181522a009ecd7 100644
--- a/All-In-One-bot/cogs/mod.py
+++ b/All-In-One-bot/cogs/mod.py
@@ -1,313 +1,960 @@
-# Borrowed from https://github.com/Rapptz/RoboDanny/tree/async
-
 from discord.ext import commands
-from .utils import config, checks
-from collections import Counter
+from .utils import checks, db, time, cache
+from collections import Counter, defaultdict
+from inspect import cleandoc
+
+import re
+import json
 import discord
+import enum
+import datetime
+import asyncio
+import argparse, shlex
+import logging
+
+log = logging.getLogger(__name__)
+
+## Misc utilities
+
+class Arguments(argparse.ArgumentParser):
+    def error(self, message):
+        raise RuntimeError(message)
+
+class RaidMode(enum.Enum):
+    off = 0
+    on = 1
+    strict = 2
+
+    def __str__(self):
+        return self.name
+
+## Tables
+
+class GuildConfig(db.Table, table_name='guild_mod_config'):
+    id = db.Column(db.Integer(big=True), primary_key=True)
+    raid_mode = db.Column(db.Integer(small=True))
+    broadcast_channel = db.Column(db.Integer(big=True))
+    mention_count = db.Column(db.Integer(small=True))
+    safe_mention_channel_ids = db.Column(db.Array(db.Integer(big=True)))
+
+## Configuration
+
+class ModConfig:
+    __slots__ = ('raid_mode', 'id', 'bot', 'broadcast_channel_id', 'mention_count', 'safe_mention_channel_ids')
+
+    @classmethod
+    async def from_record(cls, record, bot):
+        self = cls()
+
+        # the basic configuration
+        self.bot = bot
+        self.raid_mode = record['raid_mode']
+        self.id = record['id']
+        self.broadcast_channel_id = record['broadcast_channel']
+        self.mention_count = record['mention_count']
+        self.safe_mention_channel_ids = set(record['safe_mention_channel_ids'] or [])
+        return self
+
+    @property
+    def broadcast_channel(self):
+        guild = self.bot.get_guild(self.id)
+        return guild and guild.get_channel(self.broadcast_channel_id)
+
+## Converters
+
+
+class MemberID(commands.Converter):
+    async def convert(self, ctx, argument):
+        try:
+            m = await commands.MemberConverter().convert(ctx, argument)
+        except commands.BadArgument:
+            try:
+                return int(argument, base=10)
+            except ValueError:
+                raise commands.BadArgument(f"{argument} is not a valid member or member ID.") from None
+        else:
+            can_execute = ctx.author.id == ctx.bot.owner_id or \
+                          ctx.author == ctx.guild.owner or \
+                          ctx.author.top_role > m.top_role
+
+            if not can_execute:
+                raise commands.BadArgument('You cannot do this action on this user due to role hierarchy.')
+            return m.id
 
 
+class BannedMember(commands.Converter):
+    async def convert(self, ctx, argument):
+        ban_list = await ctx.guild.bans()
+        try:
+            member_id = int(argument, base=10)
+            entity = discord.utils.find(lambda u: u.user.id == member_id, ban_list)
+        except ValueError:
+            entity = discord.utils.find(lambda u: str(u.user) == argument, ban_list)
+
+        if entity is None:
+            raise commands.BadArgument("Not a valid previously-banned member.")
+        return entity
+
+
+class ActionReason(commands.Converter):
+    async def convert(self, ctx, argument):
+        ret = f'{ctx.author} (ID: {ctx.author.id}): {argument}'
+
+        if len(ret) > 512:
+            reason_max = 512 - len(ret) - len(argument)
+            raise commands.BadArgument(f'reason is too long ({len(argument)}/{reason_max})')
+        return ret
+
+
+# The actual cog
+
 class Mod:
-	"""Moderation related commands."""
-
-	def __init__(self, bot):
-		self.bot = bot
-		self.config = config.Config('mod.json', loop=bot.loop)
-
-	def bot_user(self, message):
-		return message.server.me if message.channel.is_private else self.bot.user
-
-	@commands.group(pass_context=True, no_pm=True)
-	@checks.admin_or_permissions(manage_channel=True)
-	async def ignore(self, ctx):
-		"""Handles the bot's ignore lists.
-		To use these commands, you must have the Bot Admin role or have
-		Manage Channel permissions. These commands are not allowed to be used
-		in a private message context.
-		Users with Manage Roles or Bot Admin role can still invoke the bot
-		in ignored channels.
-		"""
-		if ctx.invoked_subcommand is None:
-			await self.bot.say('Invalid subcommand passed: {0.subcommand_passed}'.format(ctx))
-
-	@ignore.command(name='channel', pass_context=True)
-	async def channel_cmd(self, ctx, *, channel: discord.Channel = None):
-		"""Ignores a specific channel from being processed.
-		If no channel is specified, the current channel is ignored.
-		If a channel is ignored then the bot does not process commands in that
-		channel until it is unignored.
-		"""
-
-		if channel is None:
-			channel = ctx.message.channel
-
-		ignored = self.config.get('ignored', [])
-		if channel.id in ignored:
-			await self.bot.say('That channel is already ignored.')
-			return
-
-		ignored.append(channel.id)
-		await self.config.put('ignored', ignored)
-		await self.bot.say('\U0001f44c')
-
-	@ignore.command(name='all', pass_context=True)
-	@checks.admin_or_permissions(manage_server=True)
-	async def _all(self, ctx):
-		"""Ignores every channel in the server from being processed.
-		This works by adding every channel that the server currently has into
-		the ignore list. If more channels are added then they will have to be
-		ignored by using the ignore command.
-		To use this command you must have Manage Server permissions along with
-		Manage Channel permissions. You could also have the Bot Admin role.
-		"""
-
-		ignored = self.config.get('ignored', [])
-		channels = ctx.message.server.channels
-		ignored.extend(c.id for c in channels if c.type == discord.ChannelType.text)
-		await self.config.put('ignored', list(set(ignored)))  # make unique
-		await self.bot.say('\U0001f44c')
-
-	@commands.command(pass_context=True, no_pm=True)
-	@checks.admin_or_permissions(manage_channel=True)
-	async def unignore(self, ctx, *, channel: discord.Channel = None):
-		"""Unignores a specific channel from being processed.
-		If no channel is specified, it unignores the current channel.
-		To use this command you must have the Manage Channel permission or have the
-		Bot Admin role.
-		"""
-
-		if channel is None:
-			channel = ctx.message.channel
-
-		# a set is the proper data type for the ignore list
-		# however, JSON only supports arrays and objects not sets.
-		ignored = self.config.get('ignored', [])
-		try:
-			ignored.remove(channel.id)
-		except ValueError:
-			await self.bot.say('Channel was not ignored in the first place.')
-		else:
-			await self.bot.say('\U0001f44c')
-
-	@commands.command(pass_context=True, no_pm=True)
-	@checks.mod_or_permissions(manage_messages=True)
-	async def cleanup(self, ctx, search: int = 100):
-		"""Cleans up the bot's messages from the channel.
-		If a search number is specified, it searches that many messages to delete.
-		If the bot has Manage Messages permissions, then it will try to delete
-		messages that look like they invoked the bot as well.
-		After the cleanup is completed, the bot will send you a message with
-		which people got their messages deleted and their count. This is useful
-		to see which users are spammers.
-		To use this command you must have Manage Messages permission or have the
-		Bot Mod role.
-		"""
-
-		spammers = Counter()
-		channel = ctx.message.channel
-		prefixes = self.bot.command_prefix
-
-		def is_possible_command_invoke(entry):
-			valid_call = any(entry.content.startswith(prefix) for prefix in prefixes)
-			return valid_call and not entry.content[1:2].isspace()
-
-		async for entry in self.bot.logs_from(channel, limit=search):
-			if entry.author == self.bot.user:
-				await self.bot.delete_message(entry)
-				spammers['Bot'] += 1
-			if is_possible_command_invoke(entry):
-				try:
-					await self.bot.delete_message(entry)
-				except discord.Forbidden:
-					continue
-				else:
-					spammers[entry.author.name] += 1
-
-		await self.bot.say('Clean up completed. {} message(s) were deleted.'.format(sum(spammers.values())))
-
-		spammers = sorted(spammers.items(), key=lambda t: t[1], reverse=True)
-		stats = '\n'.join(map(lambda t: '- **{0[0]}**: {0[1]}'.format(t), spammers))
-		await self.bot.whisper(stats)
-
-	@commands.command(no_pm=True)
-	@checks.admin_or_permissions(kick_members=True)
-	async def kick(self, *, member: discord.Member):
-		"""Kicks a member from the server.
-		In order for this to work, the bot must have Kick Member permissions.
-		To use this command you must have Kick Members permission or have the
-		Bot Admin role.
-		"""
-
-		try:
-			await self.bot.kick(member)
-		except discord.Forbidden:
-			await self.bot.say('The bot does not have permissions to kick members.')
-		except discord.HTTPException:
-			await self.bot.say('Kicking failed.')
-		else:
-			await self.bot.say('\U0001f44c')
-
-	@commands.command(no_pm=True)
-	@checks.admin_or_permissions(ban_members=True)
-	async def ban(self, *, member: discord.Member):
-		"""Bans a member from the server.
-		In order for this to work, the bot must have Ban Member permissions.
-		To use this command you must have Ban Members permission or have the
-		Bot Admin role.
-		"""
-
-		try:
-			await self.bot.ban(member)
-		except discord.Forbidden:
-			await self.bot.say('The bot does not have permissions to ban members.')
-		except discord.HTTPException:
-			await self.bot.say('Banning failed.')
-		else:
-			await self.bot.say('\U0001f44c')
-
-	@commands.command(no_pm=True)
-	@checks.admin_or_permissions(ban_members=True)
-	async def softban(self, *, member: discord.Member):
-		"""Soft bans a member from the server.
-		A softban is basically banning the member from the server but
-		then unbanning the member as well. This allows you to essentially
-		kick the member while removing their messages.
-		To use this command you must have Ban Members permissions or have
-		the Bot Admin role. Note that the bot must have the permission as well.
-		"""
-
-		try:
-			await self.bot.ban(member)
-			await self.bot.unban(member.server, member)
-		except discord.Forbidden:
-			await self.bot.say('The bot does not have permissions to ban members.')
-		except discord.HTTPException:
-			await self.bot.say('Banning failed.')
-		else:
-			await self.bot.say('\U0001f44c')
-
-	@commands.command(no_pm=True)
-	@checks.admin_or_permissions(manage_server=True)
-	async def plonk(self, *, member: discord.Member):
-		"""Bans a user from using the bot.
-		Note that this ban is **global**. So they are banned from
-		all servers that they access the bot with. So use this with
-		caution.
-		There is no way to bypass a plonk regardless of role or permissions.
-		The only person who cannot be plonked is the bot creator. So this
-		must be used with caution.
-		To use this command you must have the Manage Server permission
-		or have a Bot Admin role.
-		"""
-
-		plonks = self.config.get('plonks', [])
-		if member.id in plonks:
-			await self.bot.say('That user is already bot banned.')
-			return
-
-		plonks.append(member.id)
-		await self.config.put('plonks', plonks)
-		await self.bot.say('{0.name} has been banned from using the bot.'.format(member))
-
-	@commands.command(no_pm=True)
-	@checks.admin_or_permissions(manage_server=True)
-	async def unplonk(self, *, member: discord.Member):
-		"""Unbans a user from using the bot.
-		To use this command you must have the Manage Server permission
-		or have a Bot Admin role.
-		"""
-
-		plonks = self.config.get('plonks', [])
-
-		try:
-			plonks.remove(member.id)
-		except ValueError:
-			pass
-		else:
-			await self.config.put('plonks', plonks)
-			await self.bot.say('{0.name} has been unbanned from using the bot.'.format(member))
-
-	@commands.command(pass_context=True, no_pm=True)
-	@checks.admin_or_permissions(manage_roles=True)
-	async def colour(self, ctx, colour: discord.Colour, *, role: discord.Role):
-		"""Changes the colour of a role.
-		To use this command you must have the Manage Roles permission or
-		have the Bot Admin role. The bot must also have Manage Roles permissions.
-		This command cannot be used in a private message.
-		"""
-		try:
-			await self.bot.edit_role(ctx.message.server, role, colour=colour)
-		except discord.Forbidden:
-			await self.bot.say('The bot must have Manage Roles permissions to use this.')
-		else:
-			await self.bot.say('\U0001f44c')
-
-	@commands.group(pass_context=True, no_pm=True)
-	@checks.admin_or_permissions(manage_messages=True)
-	async def remove(self, ctx):
-		"""Removes messages that meet a criteria.
-		In order to use this command, you must have Manage Messages permissions
-		or have the Bot Admin role. Note that the bot needs Manage Messages as
-		well. These commands cannot be used in a private message.
-		When the command is done doing its work, you will get a private message
-		detailing which users got removed and how many messages got removed.
-		"""
-
-		if ctx.invoked_subcommand is None:
-			await self.bot.say('Invalid criteria passed "{0.subcommand_passed}"'.format(ctx))
-
-	async def do_removal(self, channel, limit, predicate):
-		spammers = Counter()
-		async for message in self.bot.logs_from(channel, limit=limit):
-			if predicate(message):
-				try:
-					await self.bot.delete_message(message)
-				except discord.Forbidden:
-					await self.bot.say('The bot does not have permissions to delete messages.')
-					return
-				else:
-					spammers[message.author.name] += 1
-
-		await self.bot.say('{} messages(s) were removed.'.format(sum(spammers.values())))
-		spammers = sorted(spammers.items(), key=lambda t: t[1], reverse=True)
-		stats = '\n'.join(map(lambda t: '**{0[0]}**: {0[1]}'.format(t), spammers))
-		await self.bot.whisper(stats)
-
-	@remove.command(pass_context=True)
-	async def embeds(self, ctx, search=100):
-		"""Removes messages that have embeds in them."""
-		await self.do_removal(ctx.message.channel, search, lambda e: len(e.embeds))
-
-	@remove.command(pass_context=True)
-	async def files(self, ctx, search=100):
-		"""Removes messages that have attachments in them."""
-		await self.do_removal(ctx.message.channel, search, lambda e: len(e.attachments))
-
-	@remove.command(pass_context=True)
-	async def images(self, ctx, search=100):
-		"""Removes messages that have embeds or attachments."""
-		await self.do_removal(ctx.message.channel, search, lambda e: len(e.embeds) or len(e.attachments))
-
-	@remove.command(name='all', pass_context=True)
-	async def _remove_all(self, ctx, search=100):
-		"""Removes all messages."""
-		await self.do_removal(ctx.message.channel, search, lambda e: True)
-
-	@remove.command(pass_context=True)
-	async def user(self, ctx, member: discord.Member, search=100):
-		"""Removes all messages by the member."""
-		await self.do_removal(ctx.message.channel, search, lambda e: e.author == member)
-
-	@remove.command(pass_context=True)
-	async def contains(self, ctx, *, substr: str):
-		"""Removes all messages containing a substring.
-		The substring must be at least 3 characters long.
-		"""
-		if len(substr) < 3:
-			await self.bot.say('The substring length must be at least 3 characters.')
-			return
-
-		await self.do_removal(ctx.message.channel, 100, lambda e: substr in e.content)
+    """Moderation related commands."""
+
+    def __init__(self, bot):
+        self.bot = bot
+
+        # guild_id: set(user_id)
+        self._recently_kicked = defaultdict(set)
+
+    def __repr__(self):
+        return '<cogs.Mod>'
+
+    async def __error(self, ctx, error):
+        if isinstance(error, commands.BadArgument):
+            await ctx.send(error)
+        elif isinstance(error, commands.CommandInvokeError):
+            original = error.original
+            if isinstance(original, discord.Forbidden):
+                await ctx.send('I do not have permission to execute this action.')
+            elif isinstance(original, discord.NotFound):
+                await ctx.send(f'This entity does not exist: {original.text}')
+            elif isinstance(original, discord.HTTPException):
+                await ctx.send('Somehow, an unexpected error occurred. Try again later?')
+
+    @cache.cache()
+    async def get_guild_config(self, guild_id):
+        query = """SELECT * FROM guild_mod_config WHERE id=$1;"""
+        async with self.bot.pool.acquire() as con:
+            record = await con.fetchrow(query, guild_id)
+            if record is not None:
+                return await ModConfig.from_record(record, self.bot)
+            return None
+
+    async def check_raid(self, config, guild, member, timestamp):
+        if config.raid_mode != RaidMode.strict.value:
+            return
+
+        delta  = (member.joined_at - member.created_at).total_seconds() // 60
+
+        # they must have created their account at most 30 minutes before they joined.
+        if delta > 30:
+            return
+
+        delta = (timestamp - member.joined_at).total_seconds() // 60
+
+        # check if this is their first action in the 30 minutes they joined
+        if delta > 30:
+            return
+
+        try:
+            fmt = f"""Howdy. The server {guild.name} is currently in a raid mode lockdown.
+
+                   A raid is when a server is being bombarded with trolls or low effort posts.
+                   Unfortunately, what this means is that you have been automatically kicked for
+                   meeting the suspicious thresholds currently set.
+
+                   **Do not worry though, as you will be able to join again in the future!**
+                   """
+
+            fmt = cleandoc(fmt)
+            await member.send(fmt)
+        except discord.HTTPException:
+            pass
+
+        # kick anyway
+        try:
+            await member.kick(reason='Strict raid mode')
+        except discord.HTTPException:
+            log.info(f'[Raid Mode] Failed to kick {member} (ID: {member.id}) from server {member.guild} via strict mode.')
+        else:
+            log.info(f'[Raid Mode] Kicked {member} (ID: {member.id}) from server {member.guild} via strict mode.')
+            self._recently_kicked[guild.id].add(member.id)
+
+    async def on_message(self, message):
+        author = message.author
+        if author.id in (self.bot.user.id, self.bot.owner_id):
+            return
+
+        if message.guild is None:
+            return
+
+        if not isinstance(author, discord.Member):
+            return
+
+        # we're going to ignore members with roles
+        if len(author.roles) > 1:
+            return
+
+        guild_id = message.guild.id
+        config = await self.get_guild_config(guild_id)
+        if config is None:
+            return
+
+        # check for raid mode stuff
+        await self.check_raid(config, message.guild, author, message.created_at)
+
+        # auto-ban tracking for mention spams begin here
+        if len(message.mentions) <= 3:
+            return
+
+        if not config.mention_count:
+            return
+
+        # check if it meets the thresholds required
+        mention_count = sum(not m.bot for m in message.mentions)
+        if mention_count < config.mention_count:
+            return
+
+        if message.channel.id in config.safe_mention_channel_ids:
+            return
+
+        try:
+            await author.ban(reason=f'Spamming mentions ({mention_count} mentions)')
+        except Exception as e:
+            log.info(f'Failed to autoban member {author} (ID: {author.id}) in guild ID {guild_id}')
+        else:
+            await message.channel.send(f'Banned {author} (ID: {author.id}) for spamming {mention_count} mentions.')
+            log.info(f'Member {author} (ID: {author.id}) has been autobanned from guild ID {guild_id}')
+
+    async def on_voice_state_update(self, user, before, after):
+        if not isinstance(user, discord.Member):
+            return
+
+        # joined a voice channel
+        if before.channel is None and after.channel is not None:
+            config = await self.get_guild_config(user.guild.id)
+            if config is None:
+                return
+
+            await self.check_raid(config, user.guild, user, datetime.datetime.utcnow())
+
+    async def on_member_join(self, member):
+        config = await self.get_guild_config(member.guild.id)
+        if config is None or not config.raid_mode:
+            return
+
+        now = datetime.datetime.utcnow()
+
+        # these are the dates in minutes
+        created = (now - member.created_at).total_seconds() // 60
+        was_kicked = False
+
+        if config.raid_mode == RaidMode.strict.value:
+            was_kicked = self._recently_kicked.get(member.guild.id)
+            if was_kicked is not None:
+                try:
+                    was_kicked.remove(member.id)
+                except KeyError:
+                    was_kicked = False
+                else:
+                    was_kicked = True
+
+        # Do the broadcasted message to the channel
+        if was_kicked:
+            title = 'Member Re-Joined'
+            colour = 0xdd5f53 # red
+        else:
+            title = 'Member Joined'
+            colour = 0x53dda4 # green
+
+            if created < 30:
+                colour = 0xdda453 # yellow
+
+        e = discord.Embed(title=title, colour=colour)
+        e.timestamp = now
+        e.set_footer(text='Created')
+        e.set_author(name=str(member), icon_url=member.avatar_url)
+        e.add_field(name='ID', value=member.id)
+        e.add_field(name='Joined', value=member.joined_at)
+        e.add_field(name='Created', value=time.human_timedelta(member.created_at), inline=False)
+
+        if config.broadcast_channel:
+            await config.broadcast_channel.send(embed=e)
+
+    @commands.command(aliases=['newmembers'])
+    @commands.guild_only()
+    async def newusers(self, ctx, *, count=5):
+        """Tells you the newest members of the server.
+
+        This is useful to check if any suspicious members have
+        joined.
+
+        The count parameter can only be up to 25.
+        """
+        count = max(min(count, 25), 5)
+
+        if not ctx.guild.chunked:
+            await self.bot.request_offline_members(ctx.guild)
+
+        members = sorted(ctx.guild.members, key=lambda m: m.joined_at, reverse=True)[:count]
+
+        e = discord.Embed(title='New Members', colour=discord.Colour.green())
+
+        for member in members:
+            body = f'joined {time.human_timedelta(member.joined_at)}, created {time.human_timedelta(member.created_at)}'
+            e.add_field(name=f'{member} (ID: {member.id})', value=body, inline=False)
+
+        await ctx.send(embed=e)
+
+    @commands.group(aliases=['raids'], invoke_without_command=True)
+    @checks.is_mod()
+    async def raid(self, ctx):
+        """Controls raid mode on the server.
+
+        Calling this command with no arguments will show the current raid
+        mode information.
+
+        You must have Manage Server permissions to use this command or
+        its subcommands.
+        """
+
+        query = "SELECT raid_mode, broadcast_channel FROM guild_mod_config WHERE id=$1;"
+
+        row = await ctx.db.fetchrow(query, ctx.guild.id)
+        if row is None:
+            fmt = 'Raid Mode: off\nBroadcast Channel: None'
+        else:
+            ch = f'<#{row[1]}>' if row[1] else None
+            fmt = f'Raid Mode: {RaidMode(row[0])}\nBroadcast Channel: {ch}'
+
+        await ctx.send(fmt)
+
+    @raid.command(name='on', aliases=['enable', 'enabled'])
+    @checks.is_mod()
+    async def raid_on(self, ctx, *, channel: discord.TextChannel = None):
+        """Enables basic raid mode on the server.
+
+        When enabled, server verification level is set to table flip
+        levels and allows the bot to broadcast new members joining
+        to a specified channel.
+
+        If no channel is given, then the bot will broadcast join
+        messages on the channel this command was used in.
+        """
+
+        channel = channel or ctx.channel
+
+        try:
+            await ctx.guild.edit(verification_level=discord.VerificationLevel.high)
+        except discord.HTTPException:
+            await ctx.send('\N{WARNING SIGN} Could not set verification level.')
+
+        query = """INSERT INTO guild_mod_config (id, raid_mode, broadcast_channel)
+                   VALUES ($1, $2, $3) ON CONFLICT (id)
+                   DO UPDATE SET
+                        raid_mode = EXCLUDED.raid_mode,
+                        broadcast_channel = EXCLUDED.broadcast_channel;
+                """
+
+        await ctx.db.execute(query, ctx.guild.id, RaidMode.on.value, channel.id)
+        self.get_guild_config.invalidate(self, ctx.guild.id)
+        await ctx.send(f'Raid mode enabled. Broadcasting join messages to {channel.mention}.')
+
+    @raid.command(name='off', aliases=['disable', 'disabled'])
+    @checks.is_mod()
+    async def raid_off(self, ctx):
+        """Disables raid mode on the server.
+
+        When disabled, the server verification levels are set
+        back to Low levels and the bot will stop broadcasting
+        join messages.
+        """
+
+        try:
+            await ctx.guild.edit(verification_level=discord.VerificationLevel.low)
+        except discord.HTTPException:
+            await ctx.send('\N{WARNING SIGN} Could not set verification level.')
+
+        query = """INSERT INTO guild_mod_config (id, raid_mode, broadcast_channel)
+                   VALUES ($1, $2, NULL) ON CONFLICT (id)
+                   DO UPDATE SET
+                        raid_mode = EXCLUDED.raid_mode,
+                        broadcast_channel = NULL;
+                """
+
+        await ctx.db.execute(query, ctx.guild.id, RaidMode.off.value)
+        self._recently_kicked.pop(ctx.guild.id, None)
+        self.get_guild_config.invalidate(self, ctx.guild.id)
+        await ctx.send('Raid mode disabled. No longer broadcasting join messages.')
+
+    @raid.command(name='strict')
+    @checks.is_mod()
+    async def raid_strict(self, ctx, *, channel: discord.TextChannel = None):
+        """Enables strict raid mode on the server.
+
+        Strict mode is similar to regular enabled raid mode, with the added
+        benefit of auto-kicking members that meet the following requirements:
+
+        - Account creation date and join date are at most 30 minutes apart.
+        - First message recorded on the server is 30 minutes apart from join date.
+        - Joining a voice channel within 30 minutes of joining.
+
+        Members who meet these requirements will get a private message saying that the
+        server is currently in lock down.
+
+        If this is considered too strict, it is recommended to fall back to regular
+        raid mode.
+        """
+        channel = channel or ctx.channel
+
+        if not ctx.me.guild_permissions.kick_members:
+            return await ctx.send('\N{NO ENTRY SIGN} I do not have permissions to kick members.')
+
+        try:
+            await ctx.guild.edit(verification_level=discord.VerificationLevel.high)
+        except discord.HTTPException:
+            await ctx.send('\N{WARNING SIGN} Could not set verification level.')
+
+        query = """INSERT INTO guild_mod_config (id, raid_mode, broadcast_channel)
+                   VALUES ($1, $2, $3) ON CONFLICT (id)
+                   DO UPDATE SET
+                        raid_mode = EXCLUDED.raid_mode,
+                        broadcast_channel = EXCLUDED.broadcast_channel;
+                """
+
+        await ctx.db.execute(query, ctx.guild.id, RaidMode.strict.value, ctx.channel.id)
+        self.get_guild_config.invalidate(self, ctx.guild.id)
+        await ctx.send(f'Raid mode enabled strictly. Broadcasting join messages to {channel.mention}.')
+
+    async def _basic_cleanup_strategy(self, ctx, search):
+        count = 0
+        async for msg in ctx.history(limit=search, before=ctx.message):
+            if msg.author == ctx.me:
+                await msg.delete()
+                count += 1
+        return { 'Bot': count }
+
+    async def _complex_cleanup_strategy(self, ctx, search):
+        prefixes = tuple(self.bot.get_guild_prefixes(ctx.guild)) # thanks startswith
+
+        def check(m):
+            return m.author == ctx.me or m.content.startswith(prefixes)
+
+        deleted = await ctx.channel.purge(limit=search, check=check, before=ctx.message)
+        return Counter(m.author.display_name for m in deleted)
+
+    @commands.command()
+    @checks.has_permissions(manage_messages=True)
+    async def cleanup(self, ctx, search=100):
+        """Cleans up the bot's messages from the channel.
+
+        If a search number is specified, it searches that many messages to delete.
+        If the bot has Manage Messages permissions then it will try to delete
+        messages that look like they invoked the bot as well.
+
+        After the cleanup is completed, the bot will send you a message with
+        which people got their messages deleted and their count. This is useful
+        to see which users are spammers.
+
+        You must have Manage Messages permission to use this.
+        """
+
+        strategy = self._basic_cleanup_strategy
+        if ctx.me.permissions_in(ctx.channel).manage_messages:
+            strategy = self._complex_cleanup_strategy
+
+        spammers = await strategy(ctx, search)
+        deleted = sum(spammers.values())
+        messages = [f'{deleted} message{" was" if deleted == 1 else "s were"} removed.']
+        if deleted:
+            messages.append('')
+            spammers = sorted(spammers.items(), key=lambda t: t[1], reverse=True)
+            messages.extend(f'- **{author}**: {count}' for author, count in spammers)
+
+        await ctx.send('\n'.join(messages), delete_after=10)
+
+    @commands.command()
+    @commands.guild_only()
+    @checks.has_permissions(kick_members=True)
+    async def kick(self, ctx, member: discord.Member, *, reason: ActionReason = None):
+        """Kicks a member from the server.
+
+        In order for this to work, the bot must have Kick Member permissions.
+
+        To use this command you must have Kick Members permission.
+        """
+
+        if reason is None:
+            reason = f'Action done by {ctx.author} (ID: {ctx.author.id})'
+
+        await member.kick(reason=reason)
+        await ctx.send('\N{OK HAND SIGN}')
+
+    @commands.command()
+    @commands.guild_only()
+    @checks.has_permissions(ban_members=True)
+    async def ban(self, ctx, member: MemberID, *, reason: ActionReason = None):
+        """Bans a member from the server.
+
+        You can also ban from ID to ban regardless whether they're
+        in the server or not.
+
+        In order for this to work, the bot must have Ban Member permissions.
+
+        To use this command you must have Ban Members permission.
+        """
+
+        if reason is None:
+            reason = f'Action done by {ctx.author} (ID: {ctx.author.id})'
+
+        await ctx.guild.ban(discord.Object(id=member), reason=reason)
+        await ctx.send('\N{OK HAND SIGN}')
+
+    @commands.command()
+    @commands.guild_only()
+    @checks.has_permissions(ban_members=True)
+    async def massban(self, ctx, reason: ActionReason, *members: MemberID):
+        """Mass bans multiple members from the server.
+
+        You can also ban from ID to ban regardless whether they're
+        in the server or not.
+
+        Note that unlike the ban command, the reason comes first
+        and is not optional.
+
+        In order for this to work, the bot must have Ban Member permissions.
+
+        To use this command you must have Ban Members permission.
+        """
+
+        for member_id in members:
+            await ctx.guild.ban(discord.Object(id=member_id), reason=reason)
+
+        await ctx.send('\N{OK HAND SIGN}')
+
+    @commands.command()
+    @commands.guild_only()
+    @checks.has_permissions(kick_members=True)
+    async def softban(self, ctx, member: MemberID, *, reason: ActionReason = None):
+        """Soft bans a member from the server.
+
+        A softban is basically banning the member from the server but
+        then unbanning the member as well. This allows you to essentially
+        kick the member while removing their messages.
+
+        In order for this to work, the bot must have Ban Member permissions.
+
+        To use this command you must have Kick Members permissions.
+        """
+
+        if reason is None:
+            reason = f'Action done by {ctx.author} (ID: {ctx.author.id})'
+
+        obj = discord.Object(id=member)
+        await ctx.guild.ban(obj, reason=reason)
+        await ctx.guild.unban(obj, reason=reason)
+        await ctx.send('\N{OK HAND SIGN}')
+
+    @commands.command()
+    @commands.guild_only()
+    @checks.has_permissions(ban_members=True)
+    async def unban(self, ctx, member: BannedMember, *, reason: ActionReason = None):
+        """Unbans a member from the server.
+
+        You can pass either the ID of the banned member or the Name#Discrim
+        combination of the member. Typically the ID is easiest to use.
+
+        In order for this to work, the bot must have Ban Member permissions.
+
+        To use this command you must have Ban Members permissions.
+        """
+
+        if reason is None:
+            reason = f'Action done by {ctx.author} (ID: {ctx.author.id})'
+
+        await ctx.guild.unban(member.user, reason=reason)
+        if member.reason:
+            await ctx.send(f'Unbanned {member.user} (ID: {member.user.id}), previously banned for {member.reason}.')
+        else:
+            await ctx.send(f'Unbanned {member.user} (ID: {member.user.id}).')
+
+    @commands.command()
+    @commands.guild_only()
+    @checks.has_permissions(ban_members=True)
+    async def tempban(self, ctx, duration: time.FutureTime, member: MemberID, *, reason: ActionReason = None):
+        """Temporarily bans a member for the specified duration.
+
+        The duration can be a a short time form, e.g. 30d or a more human
+        duration such as "until thursday at 3PM" or a more concrete time
+        such as "2017-12-31".
+
+        Note that times are in UTC.
+
+        You can also ban from ID to ban regardless whether they're
+        in the server or not.
+
+        In order for this to work, the bot must have Ban Member permissions.
+
+        To use this command you must have Ban Members permission.
+        """
+
+        if reason is None:
+            reason = f'Action done by {ctx.author} (ID: {ctx.author.id})'
+
+        reminder = self.bot.get_cog('Reminder')
+        if reminder is None:
+            return await ctx.send('Sorry, this functionality is currently unavailable. Try again later?')
+
+        await ctx.guild.ban(discord.Object(id=member), reason=reason)
+        timer = await reminder.create_timer(duration.dt, 'tempban', ctx.guild.id, ctx.author.id, member, connection=ctx.db)
+        await ctx.send(f'Banned ID {member} for {time.human_timedelta(duration.dt)}.')
+
+    async def on_tempban_timer_complete(self, timer):
+        guild_id, mod_id, member_id = timer.args
+
+        guild = self.bot.get_guild(guild_id)
+        if guild is None:
+            # RIP
+            return
+
+        moderator = guild.get_member(mod_id)
+        if moderator is None:
+            try:
+                moderator = await self.bot.get_user_info(mod_id)
+            except:
+                # request failed somehow
+                moderator = f'Mod ID {mod_id}'
+            else:
+                moderator = f'{moderator} (ID: {mod_id})'
+        else:
+            moderator = f'{moderator} (ID: {mod_id})'
+
+        reason = f'Automatic unban from timer made on {timer.created_at} by {moderator}.'
+        await guild.unban(discord.Object(id=member_id), reason=reason)
+
+    @commands.group(invoke_without_command=True)
+    @commands.guild_only()
+    @checks.has_permissions(ban_members=True)
+    async def mentionspam(self, ctx, count: int=None):
+        """Enables auto-banning accounts that spam mentions.
+
+        If a message contains `count` or more mentions then the
+        bot will automatically attempt to auto-ban the member.
+        The `count` must be greater than 3. If the `count` is 0
+        then this is disabled.
+
+        This only applies for user mentions. Everyone or Role
+        mentions are not included.
+
+        To use this command you must have the Ban Members permission.
+        """
+
+        if count is None:
+            query = """SELECT mention_count, COALESCE(safe_mention_channel_ids, '{}') AS channel_ids
+                       FROM guild_mod_config
+                       WHERE id=$1;
+                    """
+
+            row = await ctx.db.fetchrow(query, ctx.guild.id)
+            if row is None or not row['mention_count']:
+                return await ctx.send('This server has not set up mention spam banning.')
+
+            ignores = ', '.join(f'<#{e}>' for e in row['channel_ids']) or 'None'
+            return await ctx.send(f'- Threshold: {row["mention_count"]} mentions\n- Ignored Channels: {ignores}')
+
+        if count == 0:
+            query = """UPDATE guild_mod_config SET mention_count = NULL WHERE id=$1;"""
+            await ctx.db.execute(query, ctx.guild.id)
+            self.get_guild_config.invalidate(self, ctx.guild.id)
+            return await ctx.send('Auto-banning members has been disabled.')
+
+        if count <= 3:
+            await ctx.send('\N{NO ENTRY SIGN} Auto-ban threshold must be greater than three.')
+            return
+
+        query = """INSERT INTO guild_mod_config (id, mention_count, safe_mention_channel_ids)
+                   VALUES ($1, $2, '{}')
+                   ON CONFLICT (id) DO UPDATE SET
+                       mention_count = $2;
+                """
+        await ctx.db.execute(query, ctx.guild.id, count)
+        self.get_guild_config.invalidate(self, ctx.guild.id)
+        await ctx.send(f'Now auto-banning members that mention more than {count} users.')
+
+    @mentionspam.command(name='ignore', aliases=['bypass'])
+    @commands.guild_only()
+    @checks.has_permissions(ban_members=True)
+    async def mentionspam_ignore(self, ctx, *channels: discord.TextChannel):
+        """Specifies what channels ignore mentionspam auto-bans.
+
+        If a channel is given then that channel will no longer be protected
+        by auto-banning from mention spammers.
+
+        To use this command you must have the Ban Members permission.
+        """
+
+        query = """UPDATE guild_mod_config
+                   SET safe_mention_channel_ids =
+                       ARRAY(SELECT DISTINCT * FROM unnest(COALESCE(safe_mention_channel_ids, '{}') || $2::bigint[]))
+                   WHERE id = $1;
+                """
+
+        if len(channels) == 0:
+            return await ctx.send('Missing channels to ignore.')
+
+        channel_ids = [c.id for c in channels]
+        await ctx.db.execute(query, ctx.guild.id, channel_ids)
+        self.get_guild_config.invalidate(self, ctx.guild.id)
+        await ctx.send(f'Mentions are now ignored on {", ".join(c.mention for c in channels)}.')
+
+    @mentionspam.command(name='unignore', aliases=['protect'])
+    @commands.guild_only()
+    @checks.has_permissions(ban_members=True)
+    async def mentionspam_unignore(self, ctx, *channels: discord.TextChannel):
+        """Specifies what channels to take off the ignore list.
+
+        To use this command you must have the Ban Members permission.
+        """
+
+        if len(channels) == 0:
+            return await ctx.send('Missing channels to protect.')
+
+        query = """UPDATE guild_mod_config
+                   SET safe_mention_channel_ids =
+                       ARRAY(SELECT element FROM unnest(safe_mention_channel_ids) AS element
+                             WHERE NOT(element = ANY($2::bigint[])))
+                   WHERE id = $1;
+                """
+
+        await ctx.db.execute(query, ctx.guild.id, [c.id for c in channels])
+        self.get_guild_config.invalidate(self, ctx.guild.id)
+        await ctx.send('Updated mentionspam ignore list.')
+
+    @commands.group(aliases=['purge'])
+    @commands.guild_only()
+    @checks.has_permissions(manage_messages=True)
+    async def remove(self, ctx):
+        """Removes messages that meet a criteria.
+
+        In order to use this command, you must have Manage Messages permissions.
+        Note that the bot needs Manage Messages as well. These commands cannot
+        be used in a private message.
+
+        When the command is done doing its work, you will get a message
+        detailing which users got removed and how many messages got removed.
+        """
+
+        if ctx.invoked_subcommand is None:
+            help_cmd = self.bot.get_command('help')
+            await ctx.invoke(help_cmd, command='remove')
+
+    async def do_removal(self, ctx, limit, predicate, *, before=None, after=None):
+        if limit > 2000:
+            return await ctx.send(f'Too many messages to search given ({limit}/2000)')
+
+        if before is None:
+            before = ctx.message
+        else:
+            before = discord.Object(id=before)
+
+        if after is not None:
+            after = discord.Object(id=after)
+
+        try:
+            deleted = await ctx.channel.purge(limit=limit, before=before, after=after, check=predicate)
+        except discord.Forbidden as e:
+            return await ctx.send('I do not have permissions to delete messages.')
+        except discord.HTTPException as e:
+            return await ctx.send(f'Error: {e} (try a smaller search?)')
+
+        spammers = Counter(m.author.display_name for m in deleted)
+        deleted = len(deleted)
+        messages = [f'{deleted} message{" was" if deleted == 1 else "s were"} removed.']
+        if deleted:
+            messages.append('')
+            spammers = sorted(spammers.items(), key=lambda t: t[1], reverse=True)
+            messages.extend(f'**{name}**: {count}' for name, count in spammers)
+
+        to_send = '\n'.join(messages)
+
+        if len(to_send) > 2000:
+            await ctx.send(f'Successfully removed {deleted} messages.', delete_after=10)
+        else:
+            await ctx.send(to_send, delete_after=10)
+
+    @remove.command()
+    async def embeds(self, ctx, search=100):
+        """Removes messages that have embeds in them."""
+        await self.do_removal(ctx, search, lambda e: len(e.embeds))
+
+    @remove.command()
+    async def files(self, ctx, search=100):
+        """Removes messages that have attachments in them."""
+        await self.do_removal(ctx, search, lambda e: len(e.attachments))
+
+    @remove.command()
+    async def images(self, ctx, search=100):
+        """Removes messages that have embeds or attachments."""
+        await self.do_removal(ctx, search, lambda e: len(e.embeds) or len(e.attachments))
+
+    @remove.command(name='all')
+    async def _remove_all(self, ctx, search=100):
+        """Removes all messages."""
+        await self.do_removal(ctx, search, lambda e: True)
+
+    @remove.command()
+    async def user(self, ctx, member: discord.Member, search=100):
+        """Removes all messages by the member."""
+        await self.do_removal(ctx, search, lambda e: e.author == member)
+
+    @remove.command()
+    async def contains(self, ctx, *, substr: str):
+        """Removes all messages containing a substring.
+
+        The substring must be at least 3 characters long.
+        """
+        if len(substr) < 3:
+            await ctx.send('The substring length must be at least 3 characters.')
+        else:
+            await self.do_removal(ctx, 100, lambda e: substr in e.content)
+
+    @remove.command(name='bot')
+    async def _bot(self, ctx, prefix=None, search=100):
+        """Removes a bot user's messages and messages with their optional prefix."""
+
+        def predicate(m):
+            return (m.webhook_id is None and m.author.bot) or (prefix and m.content.startswith(prefix))
+
+        await self.do_removal(ctx, search, predicate)
+
+    @remove.command(name='emoji')
+    async def _emoji(self, ctx, search=100):
+        """Removes all messages containing custom emoji."""
+        custom_emoji = re.compile(r'<:(\w+):(\d+)>')
+        def predicate(m):
+            return custom_emoji.search(m.content)
+
+        await self.do_removal(ctx, search, predicate)
+
+    @remove.command(name='reactions')
+    async def _reactions(self, ctx, search=100):
+        """Removes all reactions from messages that have them."""
+
+        if search > 2000:
+            return await ctx.send(f'Too many messages to search for ({search}/2000)')
+
+        total_reactions = 0
+        async for message in ctx.history(limit=search, before=ctx.message):
+            if len(message.reactions):
+                total_reactions += sum(r.count for r in message.reactions)
+                await message.clear_reactions()
+
+        await ctx.send(f'Successfully removed {total_reactions} reactions.')
+
+    @remove.command()
+    async def custom(self, ctx, *, args: str):
+        """A more advanced purge command.
+
+        This command uses a powerful "command line" syntax.
+        Most options support multiple values to indicate 'any' match.
+        If the value has spaces it must be quoted.
+
+        The messages are only deleted if all options are met unless
+        the `--or` flag is passed, in which case only if any is met.
+
+        The following options are valid.
+
+        `--user`: A mention or name of the user to remove.
+        `--contains`: A substring to search for in the message.
+        `--starts`: A substring to search if the message starts with.
+        `--ends`: A substring to search if the message ends with.
+        `--search`: How many messages to search. Default 100. Max 2000.
+        `--after`: Messages must come after this message ID.
+        `--before`: Messages must come before this message ID.
+
+        Flag options (no arguments):
+
+        `--bot`: Check if it's a bot user.
+        `--embeds`: Check if the message has embeds.
+        `--files`: Check if the message has attachments.
+        `--emoji`: Check if the message has custom emoji.
+        `--reactions`: Check if the message has reactions
+        `--or`: Use logical OR for all options.
+        `--not`: Use logical NOT for all options.
+        """
+        parser = Arguments(add_help=False, allow_abbrev=False)
+        parser.add_argument('--user', nargs='+')
+        parser.add_argument('--contains', nargs='+')
+        parser.add_argument('--starts', nargs='+')
+        parser.add_argument('--ends', nargs='+')
+        parser.add_argument('--or', action='store_true', dest='_or')
+        parser.add_argument('--not', action='store_true', dest='_not')
+        parser.add_argument('--emoji', action='store_true')
+        parser.add_argument('--bot', action='store_const', const=lambda m: m.author.bot)
+        parser.add_argument('--embeds', action='store_const', const=lambda m: len(m.embeds))
+        parser.add_argument('--files', action='store_const', const=lambda m: len(m.attachments))
+        parser.add_argument('--reactions', action='store_const', const=lambda m: len(m.reactions))
+        parser.add_argument('--search', type=int, default=100)
+        parser.add_argument('--after', type=int)
+        parser.add_argument('--before', type=int)
+
+        try:
+            args = parser.parse_args(shlex.split(args))
+        except Exception as e:
+            await ctx.send(str(e))
+            return
+
+        predicates = []
+        if args.bot:
+            predicates.append(args.bot)
+
+        if args.embeds:
+            predicates.append(args.embeds)
+
+        if args.files:
+            predicates.append(args.files)
+
+        if args.reactions:
+            predicates.append(args.reactions)
+
+        if args.emoji:
+            custom_emoji = re.compile(r'<:(\w+):(\d+)>')
+            predicates.append(lambda m: custom_emoji.search(m.content))
+
+        if args.user:
+            users = []
+            converter = commands.MemberConverter()
+            for u in args.user:
+                try:
+                    user = await converter.convert(ctx, u)
+                    users.append(user)
+                except Exception as e:
+                    await ctx.send(str(e))
+                    return
+
+            predicates.append(lambda m: m.author in users)
+
+        if args.contains:
+            predicates.append(lambda m: any(sub in m.content for sub in args.contains))
+
+        if args.starts:
+            predicates.append(lambda m: any(m.content.startswith(s) for s in args.starts))
+
+        if args.ends:
+            predicates.append(lambda m: any(m.content.endswith(s) for s in args.ends))
+
+        op = all if not args._or else any
+
+        def predicate(m):
+            r = op(p(m) for p in predicates)
+            if args._not:
+                return not r
+            return r
+
+        args.search = max(0, min(2000, args.search)) # clamp from 0-2000
+        await self.do_removal(ctx, args.search, predicate, before=args.before, after=args.after)
 
 
 def setup(bot):
-	bot.add_cog(Mod(bot))
\ No newline at end of file
+    bot.add_cog(Mod(bot))
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/music.py b/All-In-One-bot/cogs/music.py
new file mode 100644
index 0000000000000000000000000000000000000000..90fc6d63db84aa995279192cac184b807adb9317
--- /dev/null
+++ b/All-In-One-bot/cogs/music.py
@@ -0,0 +1,444 @@
+"""
+Please understand Music bots are complex, and that even this basic example can be daunting to a beginner.
+For this reason it's highly advised you familiarize yourself with discord.py, python and asyncio, BEFORE
+you attempt to write a music bot.
+This example makes use of: Python 3.6
+For a more basic voice example please read:
+    https://github.com/Rapptz/discord.py/blob/rewrite/examples/basic_voice.py
+This is a very basic playlist example, which allows per guild playback of unique queues.
+The commands implement very basic logic for basic usage. But allow for expansion. It would be advisable to implement
+your own permissions and usage logic for commands.
+e.g You might like to implement a vote before skipping the song or only allow admins to stop the player.
+Music bots require lots of work, and tuning. Goodluck.
+If you find any bugs feel free to ping me on discord. @Eviee#0666
+"""
+import asyncio
+import itertools
+import sys
+import traceback
+from functools import partial
+
+import discord
+import youtube_dl
+from async_timeout import timeout
+from discord.ext import commands
+
+from .utils import checks
+
+ytdlopts = {
+	'format': 'bestaudio/best',
+	'outtmpl': 'downloads/%(extractor)s-%(id)s-%(title)s.%(ext)s',
+	'restrictfilenames': True,
+	'noplaylist': True,
+	'nocheckcertificate': True,
+	'ignoreerrors': False,
+	'logtostderr': False,
+	'quiet': True,
+	'no_warnings': True,
+	'default_search': 'auto',
+	'source_address': '0.0.0.0'  # ipv6 addresses cause issues sometimes
+}
+
+ffmpegopts = {
+	'before_options': '-nostdin',
+	'options': '-vn'
+}
+
+ytdl = youtube_dl.YoutubeDL(ytdlopts)
+
+
+class VoiceConnectionError(commands.CommandError):
+	"""Custom Exception class for connection errors."""
+
+
+class InvalidVoiceChannel(VoiceConnectionError):
+	"""Exception for cases of invalid Voice Channels."""
+
+
+class YTDLSource(discord.PCMVolumeTransformer):
+	def __init__(self, source, *, data, requester):
+		super().__init__(source)
+		self.requester = requester
+
+		self.title = data.get('title')
+		self.web_url = data.get('webpage_url')
+
+		# YTDL info dicts (data) have other useful information you might want
+		# https://github.com/rg3/youtube-dl/blob/master/README.md
+
+	def __getitem__(self, item: str):
+		"""Allows us to access attributes similar to a dict.
+		This is only useful when you are NOT downloading.
+		"""
+		return self.__getattribute__(item)
+
+	@classmethod
+	async def create_source(cls, ctx, search: str, *, loop, download=False):
+		loop = loop or asyncio.get_event_loop()
+
+		to_run = partial(ytdl.extract_info, url=search, download=download)
+		data = await loop.run_in_executor(None, to_run)
+
+		if 'entries' in data:
+			# take first item from a playlist
+			data = data['entries'][0]
+
+		await ctx.send(f'```ini\n[Added {data["title"]} to the Queue.]\n```', delete_after=15)
+
+		if download:
+			source = ytdl.prepare_filename(data)
+		else:
+			return {'webpage_url': data['webpage_url'], 'requester': ctx.author, 'title': data['title']}
+
+		return cls(discord.FFmpegPCMAudio(source), data=data, requester=ctx.author)
+
+	@classmethod
+	async def regather_stream(cls, data, *, loop):
+		"""Used for preparing a stream, instead of downloading.
+		Since Youtube Streaming links expire."""
+		loop = loop or asyncio.get_event_loop()
+		requester = data['requester']
+
+		to_run = partial(ytdl.extract_info, url=data['webpage_url'], download=False)
+		data = await loop.run_in_executor(None, to_run)
+
+		return cls(discord.FFmpegPCMAudio(data['url']), data=data, requester=requester)
+
+
+class MusicPlayer:
+	"""A class which is assigned to each guild using the bot for Music.
+	This class implements a queue and loop, which allows for different guilds to listen to different playlists
+	simultaneously.
+	When the bot disconnects from the Voice it's instance will be destroyed.
+	"""
+
+	__slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume')
+
+	def __init__(self, ctx):
+		self.bot = ctx.bot
+		self._guild = ctx.guild
+		self._channel = ctx.channel
+		self._cog = ctx.cog
+
+		self.queue = asyncio.Queue()
+		self.next = asyncio.Event()
+
+		self.np = None  # Now playing message
+		self.volume = 0.1
+		self.current = None
+
+		ctx.bot.loop.create_task(self.player_loop())
+
+	async def player_loop(self):
+		"""Our main player loop."""
+		await self.bot.wait_until_ready()
+
+		while not self.bot.is_closed():
+			self.next.clear()
+
+			try:
+				# Wait for the next song. If we timeout cancel the player and disconnect...
+				async with timeout(300):  # 5 minutes...
+					source = await self.queue.get()
+			except asyncio.TimeoutError:
+				return self.destroy(self._guild)
+
+			if not isinstance(source, YTDLSource):
+				# Source was probably a stream (not downloaded)
+				# So we should regather to prevent stream expiration
+				try:
+					source = await YTDLSource.regather_stream(source, loop=self.bot.loop)
+				except Exception as e:
+					await self._channel.send(f'There was an error processing your song.\n'
+											 f'```css\n[{e}]\n```')
+					continue
+
+			source.volume = self.volume
+			self.current = source
+
+			self._guild.voice_client.play(source, after=lambda _: self.bot.loop.call_soon_threadsafe(self.next.set))
+			self.np = await self._channel.send(f'**Now Playing:** `{source.title}` requested by '
+											   f'`{source.requester}`.')
+			await self.next.wait()
+
+			# Make sure the FFmpeg process is cleaned up.
+			source.cleanup()
+			self.current = None
+
+			try:
+				# We are no longer playing this song...
+				await self.np.delete()
+			except discord.HTTPException:
+				pass
+
+	def destroy(self, guild):
+		"""Disconnect and cleanup the player."""
+		return self.bot.loop.create_task(self._cog.cleanup(guild))
+
+
+class Music:
+	"""Music related commands."""
+
+	__slots__ = ('bot', 'players')
+
+	def __init__(self, bot):
+		self.bot = bot
+		self.players = {}
+
+	async def cleanup(self, guild):
+		try:
+			await guild.voice_client.disconnect()
+		except AttributeError:
+			pass
+
+		try:
+			del self.players[guild.id]
+		except KeyError:
+			pass
+
+	async def __local_check(self, ctx):
+		"""A local check which applies to all commands in this cog."""
+		if not ctx.guild:
+			raise commands.NoPrivateMessage
+		return True
+
+	async def __error(self, ctx, error):
+		"""A local error handler for all errors arising from commands in this cog."""
+		if isinstance(error, commands.NoPrivateMessage):
+			try:
+				return await ctx.send('This command can not be used in Private Messages.')
+			except discord.HTTPException:
+				pass
+		elif isinstance(error, InvalidVoiceChannel):
+			await ctx.send('Error connecting to Voice Channel. '
+						   'Please make sure you are in a valid channel or provide me with one')
+
+		print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
+		traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
+
+	def get_player(self, ctx):
+		"""Retrieve the guild player, or generate one."""
+		try:
+			player = self.players[ctx.guild.id]
+		except KeyError:
+			player = MusicPlayer(ctx)
+			self.players[ctx.guild.id] = player
+
+		return player
+
+	@commands.command(name='connect', aliases=['join', 'summon'])
+	async def connect_(self, ctx, *, channel: discord.VoiceChannel = None):
+		"""Connect to voice.
+		Parameters
+		------------
+		channel: discord.VoiceChannel [Optional]
+			The channel to connect to. If a channel is not specified, an attempt to join the voice channel you are in
+			will be made.
+		This command also handles moving the bot to different channels.
+		"""
+		if not channel:
+			try:
+				channel = ctx.author.voice.channel
+			except AttributeError:
+				raise InvalidVoiceChannel('No channel to join. Please either specify a valid channel or join one.')
+
+		vc = ctx.voice_client
+
+		if vc:
+			if vc.channel.id == channel.id:
+				return
+			try:
+				await vc.move_to(channel)
+			except asyncio.TimeoutError:
+				raise VoiceConnectionError(f'Moving to channel: <{channel}> timed out.')
+		else:
+			try:
+				await channel.connect()
+			except asyncio.TimeoutError:
+				raise VoiceConnectionError(f'Connecting to channel: <{channel}> timed out.')
+
+		await ctx.send(f'Connected to: **{channel}**', delete_after=20)
+
+	@commands.command(name='play', aliases=['sing'])
+	async def play_(self, ctx, *, search: str):
+		"""Request a song and add it to the queue.
+		This command attempts to join a valid voice channel if the bot is not already in one.
+		Uses YTDL to automatically search and retrieve a song.
+		Parameters
+		------------
+		search: str [Required]
+			The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.
+		"""
+		await ctx.trigger_typing()
+
+		vc = ctx.voice_client
+
+		if not vc:
+			await ctx.invoke(self.connect_)
+
+		player = self.get_player(ctx)
+
+		# If download is False, source will be a dict which will be used later to regather the stream.
+		# If download is True, source will be a discord.FFmpegPCMAudio with a VolumeTransformer.
+		source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop, download=False)
+
+		await player.queue.put(source)
+
+	@commands.command(name='pause')
+	async def pause_(self, ctx):
+		"""Pause the currently playing song."""
+		vc = ctx.voice_client
+
+		if not vc or not vc.is_playing():
+			return await ctx.send('I am not currently playing anything!', delete_after=20)
+		elif vc.is_paused():
+			return
+
+		vc.pause()
+		await ctx.send(f'**`{ctx.author}`**: Paused the song!')
+
+	@commands.command(name='resume')
+	async def resume_(self, ctx):
+		"""Resume the currently paused song."""
+		vc = ctx.voice_client
+
+		if not vc or not vc.is_connected():
+			return await ctx.send('I am not currently playing anything!', delete_after=20)
+		elif not vc.is_paused():
+			return
+
+		vc.resume()
+		await ctx.send(f'**`{ctx.author}`**: Resumed the song!')
+
+	@commands.command(name='vote_skip', enabled=False)
+	async def vote_skip_(self, ctx):
+		"""Skips the song if the required votes have been acquired"""
+		vc = ctx.voice_client
+
+		votes = []
+		votes_needed = int(3)
+		vote_count = int(0)
+
+		if not vc or not vc.is_connected():
+			return await ctx.send('I am not currently playing anything!', delete_after=20)
+
+		if vc.is_paused():
+			pass
+		elif not vc.is_playing():
+			return
+
+		if not {ctx.author} in votes:
+			votes.append(f'{ctx.author}')
+			await ctx.send(f"There is now {len(votes)} votes!")
+			await ctx.send(votes)
+		elif {ctx.author} in votes:
+			await ctx.send(f"**`{ctx.author}`**: You have already voted")
+
+		if len(votes) == votes_needed or len(votes) > votes_needed:
+			await ctx.send("We have enough votes to skip, skipping.")
+			vc.stop()
+
+	@commands.command(name='skip')
+	@checks.song_requester_or_dj()
+	async def skip_(self, ctx):
+		"""Skip the song."""
+		vc = ctx.voice_client
+
+		if not vc or not vc.is_connected():
+			return await ctx.send('I am not currently playing anything!', delete_after=20)
+
+		if vc.is_paused():
+			pass
+		elif not vc.is_playing():
+			return
+
+		if ctx.message.author.name == vc.source.requester.name:
+			vc.stop()
+			await ctx.send(f'**`{ctx.author}`**: has skipped the song!')
+		elif discord.utils.get(ctx.message.author.roles, name="DJ"):
+			vc.stop()
+			await ctx.send(f'**`{ctx.author}`**: has skipped the song, with the help of the DJ role!')
+
+	@commands.command(name='queue', aliases=['q', 'playlist'])
+	async def queue_info(self, ctx):
+		"""Retrieve a basic queue of upcoming songs."""
+		vc = ctx.voice_client
+
+		if not vc or not vc.is_connected():
+			return await ctx.send('I am not currently connected to voice!', delete_after=20)
+
+		player = self.get_player(ctx)
+		if player.queue.empty():
+			return await ctx.send('There are currently no more queued songs.')
+
+		# Grab up to 5 entries from the queue...
+		upcoming = list(itertools.islice(player.queue._queue, 0, 5))
+
+		fmt = '\n'.join(f'**`{_["title"]}`**' for _ in upcoming)
+		embed = discord.Embed(title=f'Upcoming - Next {len(upcoming)}', description=fmt)
+
+		await ctx.send(embed=embed)
+
+	@commands.command(name='now_playing', aliases=['np', 'current', 'currentsong', 'playing'])
+	async def now_playing_(self, ctx):
+		"""Display information about the currently playing song."""
+		vc = ctx.voice_client
+
+		if not vc or not vc.is_connected():
+			return await ctx.send('I am not currently connected to voice!', delete_after=20)
+
+		player = self.get_player(ctx)
+		if not player.current:
+			return await ctx.send('I am not currently playing anything!')
+
+		try:
+			# Remove our previous now_playing message.
+			await player.np.delete()
+		except discord.HTTPException:
+			pass
+
+		player.np = await ctx.send(f'**Now Playing:** `{vc.source.title}` '
+								   f'requested by `{vc.source.requester}`.')
+
+	@commands.command(name='volume', aliases=['vol'])
+	async def change_volume(self, ctx, *, vol: float):
+		"""Change the player volume.
+		Parameters
+		------------
+		volume: float or int [Required]
+			The volume to set the player to in percentage. This must be between 1 and 100.
+		"""
+		vc = ctx.voice_client
+
+		if not vc or not vc.is_connected():
+			return await ctx.send('I am not currently connected to voice!', delete_after=20)
+
+		if not 0 < vol < 101:
+			return await ctx.send('Please enter a value between 1 and 100.')
+
+		player = self.get_player(ctx)
+
+		if vc.source:
+			vc.source.volume = vol / 100
+
+		player.volume = vol / 100
+		await ctx.send(f'**`{ctx.author}`**: Set the volume to **{vol}%**')
+
+	@commands.command(name='stop')
+	@checks.music_stop_check()
+	async def stop_(self, ctx):
+		"""Stop the currently playing song and destroy the player.
+		!Warning!
+			This will destroy the player assigned to your guild, also deleting any queued songs and settings.
+		"""
+		vc = ctx.voice_client
+
+		if not vc or not vc.is_connected():
+			return await ctx.send('I am not currently playing anything!', delete_after=20)
+
+		await self.cleanup(ctx.guild)
+
+
+def setup(bot):
+	bot.add_cog(Music(bot))
+	print("Music cog loaded.")
diff --git a/All-In-One-bot/cogs/ombi.py b/All-In-One-bot/cogs/ombi.py
index 5f9147b14df366c2f1fd9a0055d795a2ed6ca1f5..cb5d637090bec91d9b0b4dfe9418af332df410f4 100644
--- a/All-In-One-bot/cogs/ombi.py
+++ b/All-In-One-bot/cogs/ombi.py
@@ -13,15 +13,15 @@ class Pmrs:
 		self.bot = bot
 
 	@commands.group(name='pmrs-search', description="Search from Plex Media Request System", brief="Search pmrs", pass_context=True)
-	async def search_ombi(self, context):
-		if context.invoked_subcommand is None:
+	async def search_ombi(self, ctx):
+		if ctx.invoked_subcommand is None:
 			embed = discord.Embed(title="Plex Media System - Search", color=0xdc1112)
 			embed.add_field(name="How to use search with it?", value="If you wish to search for movies, you have to specify it after this command `movie <replaceme-with-title>`.\nIf you wish to search for tv shows, you have to specify it after this command `tv <replaceme-with-series>`.", inline=False)
 			embed.add_field(name="How to lookup most popular content?", value="If you wish to lookup popular movies, execute `popularmovie` after this command.\nIf you wish to lookup popular TV Shows, execute `populartv` after this command.", inline=False)
-			await self.bot.say(embed=embed)
+			await ctx.say(embed=embed)
 
 	@search_ombi.command()
-	async def movie(self, input):
+	async def movie(self, ctx, input):
 		config = parser.ConfigParser()
 		config.read('./config.ini')
 		pmrs_url = config.get('urls', 'PMRS_BASE_URL')
@@ -48,10 +48,10 @@ class Pmrs:
 					embed.add_field(name="Description", value="".join(desc), inline=False)
 					embed.set_thumbnail(url=generated_url_for_thumb)
 					embed.set_footer(text="This data is gathered from PMRS's API.")
-					await self.bot.say(embed=embed)
+					await ctx.say(embed=embed)
 
 	@search_ombi.command()
-	async def tv(self, input):
+	async def tv(self, ctx, input):
 		config = parser.ConfigParser()
 		config.read('./config.ini')
 		pmrs_url = config.get('urls', 'PMRS_BASE_URL')
@@ -78,10 +78,10 @@ class Pmrs:
 					embed.add_field(name="Description", value="".join(desc), inline=False)
 					embed.set_thumbnail(url=banner)
 					embed.set_footer(text="This data is gathered from PMRS's API.")
-					await self.bot.say(embed=embed)
+					await ctx.say(embed=embed)
 
 	@search_ombi.command()
-	async def populartv(self):
+	async def populartv(self, ctx):
 		config = parser.ConfigParser()
 		config.read('./config.ini')
 		pmrs_url = config.get('urls', 'PMRS_BASE_URL')
@@ -100,18 +100,18 @@ class Pmrs:
 					aired_from_string = datetime.datetime.strptime(first_aired, '%Y-%m-%dT%H:%M:%S')
 					formatted_aired = aired_from_string.strftime('%B %d, %Y')
 					desc = entry['overview']
-					imdbId = entry['imdbId']
-					imdb_url_full = imdb_url_base + imdbId
+					imdbid = entry['imdbId']
+					imdb_url_full = imdb_url_base + imdbid
 					embed = discord.Embed(title="Currently popular TV shows", description="", color=0x00ff00)
 					embed.add_field(name="Title", value="".join(title), inline=False)
 					embed.add_field(name="First Aired", value="".join(formatted_aired), inline=False)
 					embed.add_field(name="IMDb", value="".join(imdb_url_full), inline=False)
 					embed.add_field(name="Description", value="".join(desc), inline=False)
 					embed.set_footer(text="This data is gathered from PMRS's API.")
-					await self.bot.say(embed=embed)
+					await ctx.say(embed=embed)
 
 	@search_ombi.command()
-	async def popularmovie(self):
+	async def popularmovie(self, ctx):
 		config = parser.ConfigParser()
 		config.read('./config.ini')
 		pmrs_url = config.get('urls', 'PMRS_BASE_URL')
@@ -127,14 +127,14 @@ class Pmrs:
 				a = await response.json()
 				for entry in a:
 					title = entry['title']
-					releaseDate = entry['releaseDate']
-					released_from_string = datetime.datetime.strptime(releaseDate, '%Y-%m-%dT%H:%M:%S')
+					releasedate = entry['releaseDate']
+					released_from_string = datetime.datetime.strptime(releasedate, '%Y-%m-%dT%H:%M:%S')
 					formatted_released = released_from_string.strftime('%B %d, %Y')
 					desc = entry['overview']
 					picture_url = entry['posterPath']
-					moviedbID = entry['theMovieDbId']
+					moviedbid = entry['theMovieDbId']
 					generated_picture_url = tmdb_image_not_full + picture_url
-					generated_tmdb_url = tmdb_base_url + moviedbID
+					generated_tmdb_url = tmdb_base_url + moviedbid
 					embed = discord.Embed(title="Currently popular movies", description="", color=0x00ff00)
 					embed.add_field(name="Title", value="".join(title), inline=False)
 					embed.add_field(name="Release date", value="".join(formatted_released), inline=False)
@@ -142,7 +142,7 @@ class Pmrs:
 					embed.add_field(name="Description", value="".join(desc), inline=False)
 					embed.set_thumbnail(url=generated_picture_url)
 					embed.set_footer(text="This data is gathered from PMRS's API.")
-					await self.bot.say(embed=embed)
+					await ctx.say(embed=embed)
 
 
 def setup(bot):
diff --git a/All-In-One-bot/cogs/plex.py b/All-In-One-bot/cogs/plex.py
index 91ad02b89e7d7c4a8faf6881c0262ff538a5b325..e2230237538a913ec54581a23ab509d0db660fce 100644
--- a/All-In-One-bot/cogs/plex.py
+++ b/All-In-One-bot/cogs/plex.py
@@ -1,15 +1,10 @@
-## Author: samip5
+# Author: samip5
 
-import random
-import time
-import json
 import aiohttp
-import requests
 import discord
-
-from plexapi.server import PlexServer
-from plexapi.library import Library
+import requests
 from discord.ext import commands
+from plexapi.server import PlexServer
 
 PLEX_TOKEN = ""
 PLEX_SRV = ""
@@ -25,36 +20,35 @@ class Plex:
 
     @commands.command(name='streams', description="Returns currently playing streams on Plex.",
                       brief="Display current streams")
-    async def query_plex_streams(self):
+    async def query_plex_streams(self, ctx):
         sess = requests.Session()
         sess.verify = False
-        plex = PlexServer(baseurl=PLEX_SRV, token=PLEX_TOKEN, session=sess)
-        session_check = plex.sessions() == []
-        session_check_not_empty = plex.sessions() != []
-        for session in plex.sessions():
+        server = PlexServer(baseurl=PLEX_SRV, token=PLEX_TOKEN, session=sess)
+        session_check = server.sessions() == []
+        for session in server.sessions():
             state = session.players[0].state
+            player = session.players[0].platform
             duration = session.duration
             duration_millis = session.duration
-            duration_seconds = (duration_millis / 1000) % 60
-            duration_seconds = int(duration_seconds)
-            duration_minutes = (duration_millis / (1000 * 60)) % 60
-            duration_minutes = int(duration_minutes)
+            # duration_seconds = int(duration_millis / 1000) % 60
+            duration_minutes = int(duration_millis / (1000 * 60)) % 60
             duration_hours = hours = (duration_millis / (1000 * 60 * 60)) % 24
             if duration_hours >= 1:
-                total_duration = ("%d Hours and %d Minutes" % (duration_hours, duration_minutes))
+                # total_duration = ("%d Hours and %d Minutes" % (duration_hours, duration_minutes))
+                total_duration = f'{duration_hours} Hours and {duration_minutes} Minutes'
             else:
-                total_duration = ("%d Minutes" % duration_minutes)
+                # total_duration = ("%d Minutes" % duration_minutes)
+                total_duration = f'{duration_minutes} Minutes'
             view_offset = session.viewOffset
             view_offset_millis = session.viewOffset
-            view_offset_seconds = (view_offset_millis / 1000) % 60
-            view_offset_seconds = int(view_offset_seconds)
-            view_offset_minutes = (view_offset_millis / (1000 * 60)) % 60
-            view_offset_minutes = int(view_offset_minutes)
-            view_offset_hours = hours = (view_offset_millis / (1000 * 60 * 60)) % 24
+            view_offset_minutes = int(view_offset_millis / (1000 * 60)) % 60
+            view_offset_hours = int(view_offset_millis / (1000 * 60 * 60)) % 24
             if view_offset_hours >= 1:
-                offset = ("%d Hours and %d Minutes" % (view_offset_hours, view_offset_minutes))
+                # offset = ("%d Hours and %d Minutes" % (view_offset_hours, view_offset_minutes))
+                offset = f'{view_offset_hours} Hours and {view_offset_minutes} Minutes'
             else:
-                offset = ("%d Minutes" % view_offset_minutes)
+                # offset = ("%d Minutes" % view_offset_minutes)
+                offset = f'{view_offset_minutes} Minutes'
             # print("ms: %d" % (view_offset))
             # print("Minutes: %d" % (view_offset_minutes))
             # print("Hours: %d" % (view_offset_hours))
@@ -64,17 +58,15 @@ class Plex:
                 episode_number = int(session.index)
                 season = int(session.parentIndex)
                 season_and_ep_formatted = ("(s%d:e%d)" % (season, episode_number))
-                current_tv_token = '?checkFiles=1&X-Plex-Token=' + PLEX_TOKEN
+                current_tv_token = f"?checkFiles=1&X-Plex-Token={PLEX_TOKEN}"
                 current_tv_thumb = PLEX_URL + session.thumb + current_tv_token
                 title = session.grandparentTitle + ' - ' + session.title + ' ' + season_and_ep_formatted
             if session.type == 'movie':
                 year = ("(%d)" % session.year)
-                current_movie_token = '?checkFiles=1&X-Plex-Token=' + PLEX_TOKEN
+                current_movie_token = f"?checkFiles=1&X-Plex-Token={PLEX_TOKEN}"
                 current_movie_thumb = PLEX_URL + session.thumb + current_movie_token
                 # print(current_movie_thumb)
                 title = session.title + ' ' + year
-            state = session.players[0].state
-            player = session.players[0].platform
             embed = discord.Embed(title="Currently streaming", description="", color=0x00ff00)
             embed.add_field(name="Username", value="{}".format(username))
             embed.add_field(name="Player", value="".join(player), inline=False)
@@ -88,12 +80,12 @@ class Plex:
             else:
                 embed.set_thumbnail(url=current_movie_thumb)
             embed.set_footer(text="Powered by plexapi.")
-            await self.bot.say(embed=embed)
+            await ctx.say(embed=embed)
         if session_check:
-            await self.bot.say("Nothing is currently streaming.")
+            await ctx.say("Nothing is currently streaming.")
 
     @commands.command(name='plex-search', description="Search from PLex", brief="Search Plex")
-    async def search_plex_though_tautulli(self, input):
+    async def search_plex_though_tautulli(self, ctx, input):
         tautulli_url = "{tautulli_base_url}api/v2?apikey={api_key}&cmd=search&query={string}".format(
             tautulli_base_url=TAUTULLI_BASE_URL, api_key=TAUTULLI_API_KEY, string=input)
         # tautulli_query = "{string}"
@@ -102,19 +94,13 @@ class Plex:
             async with ses.get(tautulli_url) as resp:
                 a = await resp.json()
                 for entry in range(a['response']['data']['results_count']):
-                    a = await resp.json()
-                    b = await resp.json()
                     is_TvShow = (a['response']['data']['results_list']['show'])
-                    is_movie = (b['response']['data']['results_list']['movie'])
+                    is_movie = (a['response']['data']['results_list']['movie'])
                     if is_TvShow:
-                        c = await resp.json()
-                        d = await resp.json()
-                        e = await resp.json()
-                        f = await resp.json()
-                        title = (c['response']['data']['results_list']['show'][entry]['title'])
-                        year = (d['response']['data']['results_list']['show'][entry]['year'])
-                        desc = (e['response']['data']['results_list']['show'][entry]['summary'])
-                        thumbail_not_full = (f['response']['data']['results_list']['show'][entry]['thumb'])
+                        title = (a['response']['data']['results_list']['show'][entry]['title'])
+                        year = (a['response']['data']['results_list']['show'][entry]['year'])
+                        desc = (a['response']['data']['results_list']['show'][entry]['summary'])
+                        thumbail_not_full = (a['response']['data']['results_list']['show'][entry]['thumb'])
                         tv_plex_token = '?checkFiles=1&X-Plex-Token=' + PLEX_TOKEN
                         tv_generated_url = PLEX_URL + thumbail_not_full + tv_plex_token
                         tv_embed = discord.Embed(title="Search results for TV", description="", color=0x00ff00)
@@ -122,18 +108,13 @@ class Plex:
                         tv_embed.add_field(name="Title", value=title)
                         tv_embed.add_field(name="Year", value=year)
                         tv_embed.add_field(name="Summary", value=desc)
-                        await self.bot.say(embed=tv_embed)
+                        await ctx.say(embed=tv_embed)
                     elif is_movie:
-                        h = await resp.json()
-                        i = await resp.json()
-                        j = await resp.json()
-                        k = await resp.json()
-                        l = await resp.json()
-                        movie_title = (h['response']['data']['results_list']['movie'][entry]['title'])
-                        movie_genre = (i['response']['data']['results_list']['movie'][entry]['genres'])
-                        movie_year = (j['response']['data']['results_list']['movie'][entry]['year'])
-                        movie_desc = (k['response']['data']['results_list']['movie'][entry]['summary'])
-                        movie_thumb_not_full = (l['response']['data']['results_list']['movie'][entry]['thumb'])
+                        movie_title = (a['response']['data']['results_list']['movie'][entry]['title'])
+                        movie_genre = (a['response']['data']['results_list']['movie'][entry]['genres'])
+                        movie_year = (a['response']['data']['results_list']['movie'][entry]['year'])
+                        movie_desc = (a['response']['data']['results_list']['movie'][entry]['summary'])
+                        movie_thumb_not_full = (a['response']['data']['results_list']['movie'][entry]['thumb'])
                         movie_plex_token = '?checkFiles=1&X-Plex-Token=' + PLEX_TOKEN
                         movie_generated_url = PLEX_URL + movie_thumb_not_full + movie_plex_token
                         movie_embed = discord.Embed(title="Search results for Movies", description="", color=0x00ff00)
@@ -142,9 +123,9 @@ class Plex:
                         movie_embed.add_field(name="Genre", value=" ,\n".join(movie_genre))
                         movie_embed.add_field(name="Year", value=movie_year)
                         movie_embed.add_field(name="Summary", value=movie_desc)
-                        await self.bot.say(embed=movie_embed)
+                        await ctx.say(embed=movie_embed)
                     else:
-                        await self.bot.say("Unable to find anything.")
+                        await ctx.say("Unable to find anything.")
                         break
 
 
diff --git a/All-In-One-bot/cogs/polls.py b/All-In-One-bot/cogs/polls.py
new file mode 100644
index 0000000000000000000000000000000000000000..c7afcd17359e61bfed22c50c42aef5e94ffd3d28
--- /dev/null
+++ b/All-In-One-bot/cogs/polls.py
@@ -0,0 +1,93 @@
+from discord.ext import commands
+import asyncio
+
+
+def to_emoji(c):
+    base = 0x1f1e6
+    return chr(base + c)
+
+
+class Polls:
+    """Poll voting system."""
+
+    def __init__(self, bot):
+        self.bot = bot
+
+    @commands.command()
+    @commands.guild_only()
+    async def poll(self, ctx, *, question):
+        """Interactively creates a poll with the following question.
+
+        To vote, use reactions!
+        """
+
+        # a list of messages to delete when we're all done
+        messages = [ctx.message]
+        answers = []
+
+        def check(m):
+            return m.author == ctx.author and m.channel == ctx.channel and len(m.content) <= 100
+
+        for i in range(20):
+            messages.append(await ctx.send(f'Say poll option or {ctx.prefix}cancel to publish poll.'))
+
+            try:
+                entry = await self.bot.wait_for('message', check=check, timeout=60.0)
+            except asyncio.TimeoutError:
+                break
+
+            messages.append(entry)
+
+            if entry.clean_content.startswith(f'{ctx.prefix}cancel'):
+                break
+
+            answers.append((to_emoji(i), entry.clean_content))
+
+        try:
+            await ctx.channel.delete_messages(messages)
+        except:
+            pass # oh well
+
+        answer = '\n'.join(f'{keycap}: {content}' for keycap, content in answers)
+        actual_poll = await ctx.send(f'{ctx.author} asks: {question}\n\n{answer}')
+        for emoji, _ in answers:
+            await actual_poll.add_reaction(emoji)
+
+    @poll.error
+    async def poll_error(self, ctx, error):
+        if isinstance(error, commands.MissingRequiredArgument):
+            return await ctx.send('Missing the question.')
+
+    @commands.command()
+    @commands.guild_only()
+    async def quickpoll(self, ctx, *questions_and_choices: str):
+        """Makes a poll quickly.
+
+        The first argument is the question and the rest are the choices.
+        """
+
+        if len(questions_and_choices) < 3:
+            return await ctx.send('Need at least 1 question with 2 choices.')
+        elif len(questions_and_choices) > 21:
+            return await ctx.send('You can only have up to 20 choices.')
+
+        perms = ctx.channel.permissions_for(ctx.me)
+        if not (perms.read_message_history or perms.add_reactions):
+            return await ctx.send('Need Read Message History and Add Reactions permissions.')
+
+        question = questions_and_choices[0]
+        choices = [(to_emoji(e), v) for e, v in enumerate(questions_and_choices[1:])]
+
+        try:
+            await ctx.message.delete()
+        except:
+            pass
+
+        body = "\n".join(f"{key}: {c}" for key, c in choices)
+        poll = await ctx.send(f'{ctx.author} asks: {question}\n\n{body}')
+        for emoji, _ in choices:
+            await poll.add_reaction(emoji)
+
+
+def setup(bot):
+    bot.add_cog(Polls(bot))
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/utils/__init__.py b/All-In-One-bot/cogs/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/All-In-One-bot/cogs/utils/cache.py b/All-In-One-bot/cogs/utils/cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..694dcd506018424ed430230b8fa2588c334f26de
--- /dev/null
+++ b/All-In-One-bot/cogs/utils/cache.py
@@ -0,0 +1,92 @@
+import inspect
+import asyncio
+import enum
+
+from functools import wraps
+
+from lru import LRU
+
+
+def _wrap_and_store_coroutine(cache, key, coro):
+    async def func():
+        value = await coro
+        cache[key] = value
+        return value
+    return func()
+
+
+def _wrap_new_coroutine(value):
+    async def new_coroutine():
+        return value
+    return new_coroutine()
+
+
+class Strategy(enum.Enum):
+    lru = 1
+    raw = 2
+
+
+def cache(maxsize=128, strategy=Strategy.lru):
+    def decorator(func):
+        if strategy is Strategy.lru:
+            _internal_cache = LRU(maxsize)
+            _stats = _internal_cache.get_stats
+        elif strategy is Strategy.raw:
+            _internal_cache = {}
+            _stats = lambda: (0, 0)
+
+        def _make_key(args, kwargs):
+            # this is a bit of a cluster fuck
+            # we do care what 'self' parameter is when we __repr__ it
+            def _true_repr(o):
+                if o.__class__.__repr__ is object.__repr__:
+                    return f'<{o.__class__.__module__}.{o.__class__.__name__}>'
+                return repr(o)
+
+            key = [ f'{func.__module__}.{func.__name__}' ]
+            key.extend(_true_repr(o) for o in args)
+            for k, v in kwargs.items():
+                # note: this only really works for this use case in particular
+                # I want to pass asyncpg.Connection objects to the parameters
+                # however, they use default __repr__ and I do not care what
+                # connection is passed in, so I needed a bypass.
+                if k == 'connection':
+                    continue
+
+                key.append(_true_repr(k))
+                key.append(_true_repr(v))
+
+            return ''.join(key)
+
+        @wraps(func)
+        def wrapper(*args, **kwargs):
+            key = _make_key(args, kwargs)
+            try:
+                value = _internal_cache[key]
+            except KeyError:
+                value = func(*args, **kwargs)
+
+                if inspect.isawaitable(value):
+                    return _wrap_and_store_coroutine(_internal_cache, key, value)
+
+                _internal_cache[key] = value
+                return value
+            else:
+                if asyncio.iscoroutinefunction(func):
+                    return _wrap_new_coroutine(value)
+                return value
+
+        def _invalidate(*args, **kwargs):
+            try:
+                del _internal_cache[_make_key(args, kwargs)]
+            except KeyError:
+                return False
+            else:
+                return True
+
+        wrapper.cache = _internal_cache
+        wrapper.get_key = lambda *args, **kwargs: _make_key(args, kwargs)
+        wrapper.invalidate = _invalidate
+        wrapper.get_stats = _stats
+        return wrapper
+    return decorator
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/utils/checks.py b/All-In-One-bot/cogs/utils/checks.py
index 081400a5b133c5d203f8bd86dedc8036de791f73..556b8ba844fa837df7fd9be3faae277b1a955ebc 100644
--- a/All-In-One-bot/cogs/utils/checks.py
+++ b/All-In-One-bot/cogs/utils/checks.py
@@ -1,50 +1,154 @@
-# Borrowed from https://github.com/Rapptz/RoboDanny/tree/async
-
+import discord
 from discord.ext import commands
-import discord.utils
 
+# The permission system of the bot is based on a "just works" basis
+# You have permissions and the bot has permissions. If you meet the permissions
+# required to execute the command (and the bot does as well) then it goes through
+# and you can execute the command.
+# Certain permissions signify if the person is a moderator (Manage Server) or an
+# admin (Administrator). Having these signify certain bypasses.
+# Of course, the owner will always be able to execute commands.
 
-def is_owner_check(message):
-	return message.author.id == '157970669261422592'
 
+async def check_permissions(ctx, perms, *, check=all):
+    is_owner = await ctx.bot.is_owner(ctx.author)
+    if is_owner:
+        return True
 
-def is_owner():
-	return commands.check(lambda ctx: is_owner_check(ctx.message))
+    resolved = ctx.channel.permissions_for(ctx.author)
+    return check(getattr(resolved, name, None) == value for name, value in perms.items())
 
 
-def check_permissions(ctx, perms):
-	msg = ctx.message
-	if is_owner_check(msg):
-		return True
+def has_permissions(*, check=all, **perms):
+    async def pred(ctx):
+        return await check_permissions(ctx, perms, check=check)
+    return commands.check(pred)
 
-	ch = msg.channel
-	author = msg.author
-	resolved = ch.permissions_for(author)
-	return all(getattr(resolved, name, None) == value for name, value in perms.items())
 
+async def check_guild_permissions(ctx, perms, *, check=all):
+    is_owner = await ctx.bot.is_owner(ctx.author)
+    if is_owner:
+        return True
 
-def role_or_permissions(ctx, check, **perms):
-	if check_permissions(ctx, perms):
-		return True
+    if ctx.guild is None:
+        return False
 
-	ch = ctx.message.channel
-	author = ctx.message.author
-	if ch.is_private:
-		return False  # can't have roles in PMs
+    resolved = ctx.author.guild_permissions
+    return check(getattr(resolved, name, None) == value for name, value in perms.items())
 
-	role = discord.utils.find(check, author.roles)
-	return role is not None
 
+def has_guild_permissions(*, check=all, **perms):
+    async def pred(ctx):
+        return await check_guild_permissions(ctx, perms, check=check)
+    return commands.check(pred)
+
+# These do not take channel overrides into account
+
+
+def is_mod():
+    async def pred(ctx):
+        return await check_guild_permissions(ctx, {'manage_guild': True})
+    return commands.check(pred)
 
-def mod_or_permissions(**perms):
-	def predicate(ctx):
-		return role_or_permissions(ctx, lambda r: r.name in ('Bot Mod', 'Bot Admin'), **perms)
 
-	return commands.check(predicate)
+def is_admin():
+    async def pred(ctx):
+        return await check_guild_permissions(ctx, {'administrator': True})
+    return commands.check(pred)
+
+
+def am_i_owner():
+    async def predicate(ctx):
+        is_owner = await ctx.bot.is_owner(ctx.author)
+        if is_owner:
+            return True
+        else:
+            return False
+    return commands.check(predicate)
+
+
+def mod_or_permissions(**perms):
+    perms['manage_guild'] = True
+    async def predicate(ctx):
+        return await check_guild_permissions(ctx, perms, check=any)
+    return commands.check(predicate)
 
 
 def admin_or_permissions(**perms):
-	def predicate(ctx):
-		return role_or_permissions(ctx, lambda r: r.name == 'Bot Admin', **perms)
+    perms['administrator'] = True
+    async def predicate(ctx):
+        return await check_guild_permissions(ctx, perms, check=any)
+    return commands.check(predicate)
+
+
+def is_in_guilds(*guild_ids):
+    def predicate(ctx):
+        guild = ctx.guild
+        if guild is None:
+            return False
+        return guild.id in guild_ids
+    return commands.check(predicate)
+
+
+def is_lounge_cpp():
+    return is_in_guilds(145079846832308224)
+
+
+def song_requester_or_dj():
+    async def predicate(ctx):
+        vc = ctx.voice_client
+        try:
+            if ctx.message.author.name == vc.source.requester.name:
+                return True
+            elif discord.utils.get(ctx.message.author.roles, name="DJ"):
+                return True
+            elif ctx.message.author != vc.source.requester or not discord.utils.get(ctx.message.author.roles, name="DJ"):
+                await ctx.send(f"The bot owner has been naughty and has failed to add a voting system, so sorry but your "
+                               f"permissions are not enough to use this command.")
+                return False
+        except AttributeError:
+            pass
+
+    return commands.check(predicate)
+
+
+def song_requester_or_owner_or_dj():
+    async def predicate(ctx):
+        is_owner = await ctx.bot.is_owner(ctx.author)
+        try:
+            vc = ctx.voice_client
+
+            if ctx.message.author.name == vc.source.requester.name:
+                return True
+            elif is_owner:
+                return True
+            elif discord.utils.get(ctx.message.author.roles, name="DJ"):
+                return True
+            elif ctx.message.author != vc.source.requester or not discord.utils.get(ctx.message.author.roles, name="DJ"):
+                await ctx.send(f"The bot owner has been naughty and has failed to add a voting system, so sorry but your "
+                           f"permissions are not enough to use this command.")
+                return False
+        except AttributeError:
+            pass
+    return commands.check(predicate)
+
+
+def music_stop_check():
+    async def predicate(ctx):
+        is_owner = await ctx.bot.is_owner(ctx.author)
+        try:
+            vc = ctx.voice_client
+
+            if ctx.message.author.name == vc.source.requester.name:
+                return True
+            elif is_owner:
+                return True
+            elif discord.utils.get(ctx.message.author.roles, name="DJ"):
+                return True
+            elif ctx.message.author != vc.source.requester or not discord.utils.get(ctx.message.author.roles, name="DJ"):
+                await ctx.send(f"You lack the permissions to use this command, sorry.")
+                return False
+        except AttributeError:
+            pass
+    return commands.check(predicate)
 
-	return commands.check(predicate)
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/utils/config.py b/All-In-One-bot/cogs/utils/config.py
index 84e73a823f09ee27ad72734a6013424478922b6d..6498b50cecb3687b832e39eb05ca5fbd97bf4603 100644
--- a/All-In-One-bot/cogs/utils/config.py
+++ b/All-In-One-bot/cogs/utils/config.py
@@ -1,58 +1,86 @@
-# This is purely from https://github.com/Rapptz/RoboDanny/blob/async/cogs/utils/config.py
-
 import json
+import os
+import uuid
 import asyncio
 
 
+def _create_encoder(cls):
+    def _default(self, o):
+        if isinstance(o, cls):
+            return o.to_json()
+        return super().default(o)
+
+    return type('_Encoder', (json.JSONEncoder,), { 'default': _default })
+
+
 class Config:
-	"""The "database" object. Internally based on ``json``."""
-
-	def __init__(self, name, **options):
-		self.name = name
-		self.object_hook = options.pop('object_hook', None)
-		self.encoder = options.pop('encoder', None)
-		self.loop = options.pop('loop', asyncio.get_event_loop())
-		if options.pop('load_later', False):
-			self.loop.create_task(self.load())
-		else:
-			self.load_from_file()
-
-	def load_from_file(self):
-		try:
-			with open(self.name, 'r') as f:
-				self._db = json.load(f, object_hook=self.object_hook)
-		except FileNotFoundError:
-			self._db = {}
-
-	async def load(self):
-		await self.loop.run_in_executor(None, self.load_from_file)
-
-	def _dump(self):
-		with open(self.name, 'w') as f:
-			json.dump(self._db, f, ensure_ascii=True, cls=self.encoder)
-
-	async def save(self):
-		await self.loop.run_in_executor(None, self._dump)
-
-	def get(self, key, *args):
-		"""Retrieves a config entry."""
-		return self._db.get(key, *args)
-
-	async def put(self, key, value, *args):
-		"""Edits a config entry."""
-		self._db[key] = value
-		await self.save()
-
-	async def remove(self, key):
-		"""Removes a config entry."""
-		del self._db[key]
-		await self.save()
-
-	def __contains__(self, item):
-		return self._db.__contains__(item)
-
-	def __len__(self):
-		return self._db.__len__()
-
-	def all(self):
-		return self._db
+    """The "database" object. Internally based on ``json``."""
+
+    def __init__(self, name, **options):
+        self.name = name
+        self.object_hook = options.pop('object_hook', None)
+        self.encoder = options.pop('encoder', None)
+
+        try:
+            hook = options.pop('hook')
+        except KeyError:
+            pass
+        else:
+            self.object_hook = hook.from_json
+            self.encoder = _create_encoder(hook)
+
+        self.loop = options.pop('loop', asyncio.get_event_loop())
+        self.lock = asyncio.Lock()
+        if options.pop('load_later', False):
+            self.loop.create_task(self.load())
+        else:
+            self.load_from_file()
+
+    def load_from_file(self):
+        try:
+            with open(self.name, 'r') as f:
+                self._db = json.load(f, object_hook=self.object_hook)
+        except FileNotFoundError:
+            self._db = {}
+
+    async def load(self):
+        with await self.lock:
+            await self.loop.run_in_executor(None, self.load_from_file)
+
+    def _dump(self):
+        temp = '%s-%s.tmp' % (uuid.uuid4(), self.name)
+        with open(temp, 'w', encoding='utf-8') as tmp:
+            json.dump(self._db.copy(), tmp, ensure_ascii=True, cls=self.encoder, separators=(',', ':'))
+
+        # atomically move the file
+        os.replace(temp, self.name)
+
+    async def save(self):
+        with await self.lock:
+            await self.loop.run_in_executor(None, self._dump)
+
+    def get(self, key, *args):
+        """Retrieves a config entry."""
+        return self._db.get(str(key), *args)
+
+    async def put(self, key, value, *args):
+        """Edits a config entry."""
+        self._db[str(key)] = value
+        await self.save()
+
+    async def remove(self, key):
+        """Removes a config entry."""
+        del self._db[str(key)]
+        await self.save()
+
+    def __contains__(self, item):
+        return str(item) in self._db
+
+    def __getitem__(self, item):
+        return self._db[str(item)]
+
+    def __len__(self):
+        return len(self._db)
+
+    def all(self):
+        return self._db
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/utils/context.py b/All-In-One-bot/cogs/utils/context.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff039fbb96088e732394e8c85773179ac8e65098
--- /dev/null
+++ b/All-In-One-bot/cogs/utils/context.py
@@ -0,0 +1,207 @@
+from discord.ext import commands
+import asyncio
+
+class _ContextDBAcquire:
+    __slots__ = ('ctx', 'timeout')
+
+    def __init__(self, ctx, timeout):
+        self.ctx = ctx
+        self.timeout = timeout
+
+    def __await__(self):
+        return self.ctx._acquire(self.timeout).__await__()
+
+    async def __aenter__(self):
+        await self.ctx._acquire(self.timeout)
+        return self.ctx.db
+
+    async def __aexit__(self, *args):
+        await self.ctx.release()
+
+class Context(commands.Context):
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.pool = self.bot.pool
+        self.db = None
+
+    async def entry_to_code(self, entries):
+        width = max(len(a) for a, b in entries)
+        output = ['```']
+        for name, entry in entries:
+            output.append(f'{name:<{width}}: {entry}')
+        output.append('```')
+        await self.send('\n'.join(output))
+
+    async def indented_entry_to_code(self, entries):
+        width = max(len(a) for a, b in entries)
+        output = ['```']
+        for name, entry in entries:
+            output.append(f'\u200b{name:>{width}}: {entry}')
+        output.append('```')
+        await self.send('\n'.join(output))
+
+    def __repr__(self):
+        # we need this for our cache key strategy
+        return '<Context>'
+
+    @property
+    def session(self):
+        return self.bot.session
+
+    async def disambiguate(self, matches, entry):
+        if len(matches) == 0:
+            raise ValueError('No results found.')
+
+        if len(matches) == 1:
+            return matches[0]
+
+        await self.send('There are too many matches... Which one did you mean? **Only say the number**.')
+        await self.send('\n'.join(f'{index}: {entry(item)}' for index, item in enumerate(matches, 1)))
+
+        def check(m):
+            return m.content.isdigit() and m.author.id == self.author.id and m.channel.id == self.channel.id
+
+        await self.release()
+
+        # only give them 3 tries.
+        try:
+            for i in range(3):
+                try:
+                    message = await self.bot.wait_for('message', check=check, timeout=30.0)
+                except asyncio.TimeoutError:
+                    raise ValueError('Took too long. Goodbye.')
+
+                index = int(message.content)
+                try:
+                    return matches[index - 1]
+                except:
+                    await self.send(f'Please give me a valid number. {2 - i} tries remaining...')
+
+            raise ValueError('Too many tries. Goodbye.')
+        finally:
+            await self.acquire()
+
+    async def prompt(self, message, *, timeout=60.0, delete_after=True, reacquire=True, author_id=None):
+        """An interactive reaction confirmation dialog.
+
+        Parameters
+        -----------
+        message: str
+            The message to show along with the prompt.
+        timeout: float
+            How long to wait before returning.
+        delete_after: bool
+            Whether to delete the confirmation message after we're done.
+        reacquire: bool
+            Whether to release the database connection and then acquire it
+            again when we're done.
+        author_id: Optional[int]
+            The member who should respond to the prompt. Defaults to the author of the
+            Context's message.
+
+        Returns
+        --------
+        Optional[bool]
+            ``True`` if explicit confirm,
+            ``False`` if explicit deny,
+            ``None`` if deny due to timeout
+        """
+
+        if not self.channel.permissions_for(self.me).add_reactions:
+            raise RuntimeError('Bot does not have Add Reactions permission.')
+
+        fmt = f'{message}\n\nReact with \N{WHITE HEAVY CHECK MARK} to confirm or \N{CROSS MARK} to deny.'
+
+        author_id = author_id or self.author.id
+        msg = await self.send(fmt)
+
+        confirm = None
+
+        def check(payload):
+            nonlocal confirm
+
+            if payload.message_id != msg.id or payload.user_id != author_id:
+                return False
+
+            codepoint = str(payload.emoji)
+
+            if codepoint == '\N{WHITE HEAVY CHECK MARK}':
+                confirm = True
+                return True
+            elif codepoint == '\N{CROSS MARK}':
+                confirm = False
+                return True
+
+            return False
+
+        for emoji in ('\N{WHITE HEAVY CHECK MARK}', '\N{CROSS MARK}'):
+            await msg.add_reaction(emoji)
+
+        if reacquire:
+            await self.release()
+
+        try:
+            await self.bot.wait_for('raw_reaction_add', check=check, timeout=timeout)
+        except asyncio.TimeoutError:
+            confirm = None
+
+        try:
+            if reacquire:
+                await self.acquire()
+
+            if delete_after:
+                await msg.delete()
+        finally:
+            return confirm
+
+    def tick(self, opt, label=None):
+        emoji = '<:greenTick:330090705336664065>' if opt else '<:redTick:330090723011592193>'
+        if label is not None:
+            return f'{emoji}: {label}'
+        return emoji
+
+    async def _acquire(self, timeout):
+        if self.db is None:
+            self.db = await self.pool.acquire(timeout=timeout)
+        return self.db
+
+    def acquire(self, *, timeout=None):
+        """Acquires a database connection from the pool. e.g. ::
+
+            async with ctx.acquire():
+                await ctx.db.execute(...)
+
+        or: ::
+
+            await ctx.acquire()
+            try:
+                await ctx.db.execute(...)
+            finally:
+                await ctx.release()
+        """
+        return _ContextDBAcquire(self, timeout)
+
+    async def release(self):
+        """Releases the database connection from the pool.
+
+        Useful if needed for "long" interactive commands where
+        we want to release the connection and re-acquire later.
+
+        Otherwise, this is called automatically by the bot.
+        """
+        # from source digging asyncpg source, releasing an already
+        # released connection does nothing
+
+        if self.db is not None:
+            await self.bot.pool.release(self.db)
+            self.db = None
+
+    async def show_help(self, command=None):
+        """Shows the help command for the specified command if given.
+
+        If no command is given, then it'll show help for the current
+        command.
+        """
+        cmd = self.bot.get_command('help')
+        command = command or self.command.qualified_name
+        await self.invoke(cmd, command=command)
diff --git a/All-In-One-bot/cogs/utils/db.py b/All-In-One-bot/cogs/utils/db.py
new file mode 100644
index 0000000000000000000000000000000000000000..d8a4c898be148d152c073002ea0f8625ba1c2289
--- /dev/null
+++ b/All-In-One-bot/cogs/utils/db.py
@@ -0,0 +1,1011 @@
+# -*- coding: utf-8 -*-
+
+"""
+The MIT License (MIT)
+
+Copyright (c) 2017 Rapptz
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+"""
+
+# These are just things that allow me to make tables for PostgreSQL easier
+# This isn't exactly good. It's just good enough for my uses.
+# Also shoddy migration support.
+
+from collections import OrderedDict
+from pathlib import Path
+import json
+import os
+import pydoc
+import uuid
+import datetime
+import inspect
+import decimal
+import asyncpg
+import logging
+import asyncio
+
+log = logging.getLogger(__name__)
+
+
+class SchemaError(Exception):
+    pass
+
+
+class SQLType:
+    python = None
+
+    def to_dict(self):
+        o = self.__dict__.copy()
+        cls = self.__class__
+        o['__meta__'] = cls.__module__ + '.' + cls.__qualname__
+        return o
+
+    @classmethod
+    def from_dict(cls, data):
+        meta = data.pop('__meta__')
+        given = cls.__module__ + '.' + cls.__qualname__
+        if given != meta:
+            cls = pydoc.locate(meta)
+            if cls is None:
+                raise RuntimeError('Could not locate "%s".' % meta)
+
+        self = cls.__new__(cls)
+        self.__dict__.update(data)
+        return self
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def to_sql(self):
+        raise NotImplementedError()
+
+    def is_real_type(self):
+        return True
+
+class Binary(SQLType):
+    python = bytes
+
+    def to_sql(self):
+        return 'BYTEA'
+
+class Boolean(SQLType):
+    python = bool
+
+    def to_sql(self):
+        return 'BOOLEAN'
+
+class Date(SQLType):
+    python = datetime.date
+
+    def to_sql(self):
+        return 'DATE'
+
+class Datetime(SQLType):
+    python = datetime.datetime
+
+    def __init__(self, *, timezone=False):
+        self.timezone = timezone
+
+    def to_sql(self):
+        if self.timezone:
+            return 'TIMESTAMP WITH TIMEZONE'
+        return 'TIMESTAMP'
+
+class Double(SQLType):
+    python = float
+
+    def to_sql(self):
+        return 'REAL'
+
+class Float(SQLType):
+    python = float
+
+    def to_sql(self):
+        return 'FLOAT'
+
+class Integer(SQLType):
+    python = int
+
+    def __init__(self, *, big=False, small=False, auto_increment=False):
+        self.big = big
+        self.small = small
+        self.auto_increment = auto_increment
+
+        if big and small:
+            raise SchemaError('Integer column type cannot be both big and small.')
+
+    def to_sql(self):
+        if self.auto_increment:
+            if self.big:
+                return 'BIGSERIAL'
+            if self.small:
+                return 'SMALLSERIAL'
+            return 'SERIAL'
+        if self.big:
+            return 'BIGINT'
+        if self.small:
+            return 'SMALLINT'
+        return 'INTEGER'
+
+    def is_real_type(self):
+        return not self.auto_increment
+
+class Interval(SQLType):
+    python = datetime.timedelta
+
+    def __init__(self, field=None):
+        if field:
+            field = field.upper()
+            if field not in ('YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND',
+                             'YEAR TO MONTH', 'DAY TO HOUR', 'DAY TO MINUTE', 'DAY TO SECOND',
+                             'HOUR TO MINUTE', 'HOUR TO SECOND', 'MINUTE TO SECOND'):
+                raise SchemaError('invalid interval specified')
+            self.field = field
+        else:
+            self.field = None
+
+    def to_sql(self):
+        if self.field:
+            return 'INTERVAL ' + self.field
+        return 'INTERVAL'
+
+class Numeric(SQLType):
+    python = decimal.Decimal
+
+    def __init__(self, *, precision=None, scale=None):
+        if precision is not None:
+            if precision < 0 or precision > 1000:
+                raise SchemaError('precision must be greater than 0 and below 1000')
+            if scale is None:
+                scale = 0
+
+        self.precision = precision
+        self.scale = scale
+
+    def to_sql(self):
+        if self.precision is not None:
+            return 'NUMERIC({0.precision}, {0.scale})'.format(self)
+        return 'NUMERIC'
+
+class String(SQLType):
+    python = str
+
+    def __init__(self, *, length=None, fixed=False):
+        self.length = length
+        self.fixed = fixed
+
+        if fixed and length is None:
+            raise SchemaError('Cannot have fixed string with no length')
+
+    def to_sql(self):
+        if self.length is None:
+            return 'TEXT'
+        if self.fixed:
+            return 'CHAR({0.length})'.format(self)
+        return 'VARCHAR({0.length})'.format(self)
+
+class Time(SQLType):
+    python = datetime.time
+
+    def __init__(self, *, timezone=False):
+        self.timezone = timezone
+
+    def to_sql(self):
+        if self.timezone:
+            return 'TIME WITH TIME ZONE'
+        return 'TIME'
+
+class JSON(SQLType):
+    python = None
+
+    def to_sql(self):
+        return 'JSONB'
+
+class ForeignKey(SQLType):
+    def __init__(self, table, column, *, sql_type=None, on_delete='CASCADE', on_update='NO ACTION'):
+        if not table or not isinstance(table, str):
+            raise SchemaError('missing table to reference (must be string)')
+
+        valid_actions = (
+            'NO ACTION',
+            'RESTRICT',
+            'CASCADE',
+            'SET NULL',
+            'SET DEFAULT',
+        )
+
+        on_delete = on_delete.upper()
+        on_update = on_update.upper()
+
+        if on_delete not in valid_actions:
+            raise TypeError('on_delete must be one of %s.' % valid_actions)
+
+        if on_update not in valid_actions:
+            raise TypeError('on_update must be one of %s.' % valid_actions)
+
+
+        self.table = table
+        self.column = column
+        self.on_update = on_update
+        self.on_delete = on_delete
+
+        if sql_type is None:
+            sql_type = Integer
+
+        if inspect.isclass(sql_type):
+            sql_type = sql_type()
+
+        if not isinstance(sql_type, SQLType):
+            raise TypeError('Cannot have non-SQLType derived sql_type')
+
+        if not sql_type.is_real_type():
+            raise SchemaError('sql_type must be a "real" type')
+
+        self.sql_type = sql_type.to_sql()
+
+    def is_real_type(self):
+        return False
+
+    def to_sql(self):
+        fmt = '{0.sql_type} REFERENCES {0.table} ({0.column})' \
+              ' ON DELETE {0.on_delete} ON UPDATE {0.on_update}'
+        return fmt.format(self)
+
+class Array(SQLType):
+    python = list
+
+    def __init__(self, sql_type):
+        if inspect.isclass(sql_type):
+            sql_type = sql_type()
+
+        if not isinstance(sql_type, SQLType):
+            raise TypeError('Cannot have non-SQLType derived sql_type')
+
+        if not sql_type.is_real_type():
+            raise SchemaError('sql_type must be a "real" type')
+
+        self.sql_type = sql_type.to_sql()
+
+    def to_sql(self):
+        return '{0.sql_type} ARRAY'.format(self)
+
+    def is_real_type(self):
+        # technically, it is a real type
+        # however, it doesn't play very well with migrations
+        # so we're going to pretend that it isn't
+        return False
+
+class Column:
+    __slots__ = ( 'column_type', 'index', 'primary_key', 'nullable',
+                  'default', 'unique', 'name', 'index_name' )
+    def __init__(self, column_type, *, index=False, primary_key=False,
+                 nullable=True, unique=False, default=None, name=None):
+
+        if inspect.isclass(column_type):
+            column_type = column_type()
+
+        if not isinstance(column_type, SQLType):
+            raise TypeError('Cannot have a non-SQLType derived column_type')
+
+        self.column_type = column_type
+        self.index = index
+        self.unique = unique
+        self.primary_key = primary_key
+        self.nullable = nullable
+        self.default = default
+        self.name = name
+        self.index_name = None # to be filled later
+
+        if sum(map(bool, (unique, primary_key, default is not None))) > 1:
+            raise SchemaError("'unique', 'primary_key', and 'default' are mutually exclusive.")
+
+    @classmethod
+    def from_dict(cls, data):
+        index_name = data.pop('index_name', None)
+        column_type = data.pop('column_type')
+        column_type = SQLType.from_dict(column_type)
+        self = cls(column_type=column_type, **data)
+        self.index_name = index_name
+        return self
+
+    @property
+    def _comparable_id(self):
+        return '-'.join('%s:%s' % (attr, getattr(self, attr)) for attr in self.__slots__)
+
+    def _to_dict(self):
+        d = {
+            attr: getattr(self, attr)
+            for attr in self.__slots__
+        }
+        d['column_type'] = self.column_type.to_dict()
+        return d
+
+    def _qualifiers_dict(self):
+        return { attr: getattr(self, attr) for attr in ('nullable', 'default')}
+
+    def _is_rename(self, other):
+        if self.name == other.name:
+            return False
+
+        return self.unique == other.unique and self.primary_key == other.primary_key
+
+    def _create_table(self):
+        builder = []
+        builder.append(self.name)
+        builder.append(self.column_type.to_sql())
+
+        default = self.default
+        if default is not None:
+            builder.append('DEFAULT')
+            if isinstance(default, str) and isinstance(self.column_type, String):
+                builder.append("'%s'" % default)
+            elif isinstance(default, bool):
+                builder.append(str(default).upper())
+            else:
+                builder.append("(%s)" % default)
+        elif self.unique:
+            builder.append('UNIQUE')
+        elif self.primary_key:
+            builder.append('PRIMARY KEY')
+
+        if not self.nullable:
+            builder.append('NOT NULL')
+
+        return ' '.join(builder)
+
+class PrimaryKeyColumn(Column):
+    """Shortcut for a SERIAL PRIMARY KEY column."""
+
+    def __init__(self):
+        super().__init__(Integer(auto_increment=True), primary_key=True)
+
+class SchemaDiff:
+    __slots__ = ('table', 'upgrade', 'downgrade')
+
+    def __init__(self, table, upgrade, downgrade):
+        self.table = table
+        self.upgrade = upgrade
+        self.downgrade = downgrade
+
+    def to_dict(self):
+        return { 'upgrade': self.upgrade, 'downgrade': self.downgrade }
+
+    def is_empty(self):
+        return len(self.upgrade) == 0 and len(self.downgrade) == 0
+
+    def to_sql(self, *, downgrade=False):
+        statements = []
+        base = 'ALTER TABLE %s ' % self.table.__tablename__
+        path = self.upgrade if not downgrade else self.downgrade
+
+        for rename in path.get('rename_columns', []):
+            fmt = '{0}RENAME COLUMN {1[before]} TO {1[after]};'.format(base, rename)
+            statements.append(fmt)
+
+        sub_statements = []
+        for dropped in path.get('remove_columns', []):
+            fmt = 'DROP COLUMN {0[name]} RESTRICT'.format(dropped)
+            sub_statements.append(fmt)
+
+        for changed_types in path.get('changed_column_types', []):
+            fmt = 'ALTER COLUMN {0[name]} SET DATA TYPE {0[type]}'.format(changed_types)
+
+            using = changed_types.get('using')
+            if using is not None:
+                fmt = '%s USING %s' % (fmt, using)
+
+            sub_statements.append(fmt)
+
+        for constraints in path.get('changed_constraints', []):
+            before, after = constraints['before'], constraints['after']
+
+            before_default, after_default = before.get('default'), after.get('default')
+            if before_default is None and after_default is not None:
+                fmt = 'ALTER COLUMN {0[name]} SET DEFAULT {1[default]}'.format(constraints, after)
+                sub_statements.append(fmt)
+            elif before_default is not None and after_default is None:
+                fmt = 'ALTER COLUMN {0[name]} DROP DEFAULT'.format(constraints)
+                sub_statements.append(fmt)
+
+            before_nullable, after_nullable = before.get('nullable'), after.get('nullable')
+            if not before_nullable and after_nullable:
+                fmt = 'ALTER COLUMN {0[name]} DROP NOT NULL'.format(constraints)
+                sub_statements.append(fmt)
+            elif before_nullable and not after_nullable:
+                fmt = 'ALTER COLUMN {0[name]} SET NOT NULL'.format(constraints)
+                sub_statements.append(fmt)
+
+        for added in path.get('add_columns', []):
+            column = Column.from_dict(added)
+            sub_statements.append('ADD COLUMN ' + column._create_table())
+
+        if sub_statements:
+            statements.append(base + ', '.join(sub_statements) + ';')
+
+        # handle the index creation bits
+        for dropped in path.get('drop_index', []):
+            statements.append('DROP INDEX IF EXISTS {0[index]};'.format(dropped))
+
+        for added in path.get('add_index', []):
+            fmt = 'CREATE INDEX IF NOT EXISTS {0[index]} ON {1.__tablename__} ({0[name]});'
+            statements.append(fmt.format(added, self.table))
+
+        return '\n'.join(statements)
+
+class MaybeAcquire:
+    def __init__(self, connection, *, pool):
+        self.connection = connection
+        self.pool = pool
+        self._cleanup = False
+
+    async def __aenter__(self):
+        if self.connection is None:
+            self._cleanup = True
+            self._connection = c = await self.pool.acquire()
+            return c
+        return self.connection
+
+    async def __aexit__(self, *args):
+        if self._cleanup:
+            await self.pool.release(self._connection)
+
+class TableMeta(type):
+    @classmethod
+    def __prepare__(cls, name, bases, **kwargs):
+        return OrderedDict()
+
+    def __new__(cls, name, parents, dct, **kwargs):
+        columns = []
+
+        try:
+            table_name = kwargs['table_name']
+        except KeyError:
+            table_name = name.lower()
+
+        dct['__tablename__'] = table_name
+
+        for elem, value in dct.items():
+            if isinstance(value, Column):
+                if value.name is None:
+                    value.name = elem
+
+                if value.index:
+                    value.index_name = '%s_%s_idx' % (table_name, value.name)
+
+                columns.append(value)
+
+        dct['columns'] = columns
+        return super().__new__(cls, name, parents, dct)
+
+    def __init__(self, name, parents, dct, **kwargs):
+        super().__init__(name, parents, dct)
+
+class Table(metaclass=TableMeta):
+    @classmethod
+    async def create_pool(cls, uri, **kwargs):
+        """Sets up and returns the PostgreSQL connection pool that is used.
+
+        .. note::
+
+            This must be called at least once before doing anything with the tables.
+            And must be called on the ``Table`` class.
+
+        Parameters
+        -----------
+        uri: str
+            The PostgreSQL URI to connect to.
+        \*\*kwargs
+            The arguments to forward to asyncpg.create_pool.
+        """
+
+        def _encode_jsonb(value):
+            return json.dumps(value)
+
+        def _decode_jsonb(value):
+            return json.loads(value)
+
+        old_init = kwargs.pop('init', None)
+
+        async def init(con):
+            await con.set_type_codec('jsonb', schema='pg_catalog', encoder=_encode_jsonb, decoder=_decode_jsonb, format='text')
+            if old_init is not None:
+                await old_init(con)
+
+        cls._pool = pool = await asyncpg.create_pool(uri, init=init, **kwargs)
+        return pool
+
+    @classmethod
+    def acquire_connection(cls, connection):
+        return MaybeAcquire(connection, pool=cls._pool)
+
+    @classmethod
+    def write_migration(cls, *, directory='migrations'):
+        """Writes the migration diff into the data file.
+
+        Note
+        ------
+        This doesn't actually commit/do the migration.
+        To do so, use :meth:`migrate`.
+
+        Returns
+        --------
+        bool
+            ``True`` if a migration was written, ``False`` otherwise.
+
+        Raises
+        -------
+        RuntimeError
+            Could not find the migration data necessary.
+        """
+
+        directory = Path(directory) / cls.__tablename__
+        p = directory.with_suffix('.json')
+
+        if not p.exists():
+            raise RuntimeError('Could not find migration file.')
+
+        current = directory.with_name('current-' + p.name)
+
+        if not current.exists():
+            raise RuntimeError('Could not find current data file.')
+
+        with current.open() as fp:
+            current_table = cls.from_dict(json.load(fp))
+
+        diff = cls().diff(current_table)
+
+        # the most common case, no difference
+        if diff.is_empty():
+            return None
+
+        # load the migration data
+        with p.open('r', encoding='utf-8') as fp:
+            data = json.load(fp)
+            migrations = data['migrations']
+
+        # check if we should add it
+        our_migrations = diff.to_dict()
+        if len(migrations) == 0 or migrations[-1] != our_migrations:
+            # we have a new migration, so add it
+            migrations.append(our_migrations)
+            temp_file = p.with_name('%s-%s.tmp' % (uuid.uuid4(), p.name))
+            with temp_file.open('w', encoding='utf-8') as tmp:
+                json.dump(data, tmp, ensure_ascii=True, indent=4)
+
+            temp_file.replace(p)
+            return True
+        return False
+
+    @classmethod
+    async def migrate(cls, *, directory='migrations', index=-1, downgrade=False, verbose=False, connection=None):
+        """Actually run the latest migration pointed by the data file.
+
+        Parameters
+        -----------
+        directory: str
+            The directory of where the migration data file resides.
+        index: int
+            The index of the migration array to use.
+        downgrade: bool
+            Whether to run an upgrade or a downgrade.
+        verbose: bool
+            Whether to output some information to stdout.
+        connection: Optional[asyncpg.Connection]
+            The connection to use, if not provided will acquire one from
+            the internal pool.
+        """
+
+        directory = Path(directory) / cls.__tablename__
+        p = directory.with_suffix('.json')
+        if not p.exists():
+            raise RuntimeError('Could not find migration file.')
+
+        with p.open('r', encoding='utf-8') as fp:
+            data = json.load(fp)
+            migrations = data['migrations']
+
+        try:
+            migration = migrations[index]
+        except IndexError:
+            return False
+
+        diff = SchemaDiff(cls, migration['upgrade'], migration['downgrade'])
+        if diff.is_empty():
+            return False
+
+        async with MaybeAcquire(connection, pool=cls._pool) as con:
+            sql = diff.to_sql(downgrade=downgrade)
+            if verbose:
+                print(sql)
+            await con.execute(sql)
+
+        current = directory.with_name('current-' + p.name)
+        with current.open('w', encoding='utf-8') as fp:
+            json.dump(cls.to_dict(), fp, indent=4, ensure_ascii=True)
+
+    @classmethod
+    async def create(cls, *, directory='migrations', verbose=False, connection=None, run_migrations=True):
+        """Creates the database and manages migrations, if any.
+
+        Parameters
+        -----------
+        directory: str
+            The migrations directory.
+        verbose: bool
+            Whether to output some information to stdout.
+        connection: Optional[asyncpg.Connection]
+            The connection to use, if not provided will acquire one from
+            the internal pool.
+        run_migrations: bool
+            Whether to run migrations at all.
+
+        Returns
+        --------
+        Optional[bool]
+            ``True`` if the table was successfully created or
+            ``False`` if the table was successfully migrated or
+            ``None`` if no migration took place.
+        """
+        directory = Path(directory) / cls.__tablename__
+        p = directory.with_suffix('.json')
+        current = directory.with_name('current-' + p.name)
+
+        table_data = cls.to_dict()
+
+        if not p.exists():
+            p.parent.mkdir(parents=True, exist_ok=True)
+
+            # we're creating this table for the first time,
+            # it's an uncommon case so let's get it out of the way
+            # first, try to actually create the table
+            async with MaybeAcquire(connection, pool=cls._pool) as con:
+                sql = cls.create_table(exists_ok=True)
+                if verbose:
+                    print(sql)
+                await con.execute(sql)
+
+            # since that step passed, let's go ahead and make the migration
+            with p.open('w', encoding='utf-8') as fp:
+                data = { 'table': table_data, 'migrations': [] }
+                json.dump(data, fp, indent=4, ensure_ascii=True)
+
+            with current.open('w', encoding='utf-8') as fp:
+                json.dump(table_data, fp, indent=4, ensure_ascii=True)
+
+            return True
+
+        if not run_migrations:
+            return None
+
+        with current.open() as fp:
+            current_table = cls.from_dict(json.load(fp))
+
+        diff = cls().diff(current_table)
+
+        # the most common case, no difference
+        if diff.is_empty():
+            return None
+
+        # execute the upgrade SQL
+        async with MaybeAcquire(connection, pool=cls._pool) as con:
+            sql = diff.to_sql()
+            if verbose:
+                print(sql)
+            await con.execute(sql)
+
+        # load the migration data
+        with p.open('r', encoding='utf-8') as fp:
+            data = json.load(fp)
+            migrations = data['migrations']
+
+        # check if we should add it
+        our_migrations = diff.to_dict()
+        if len(migrations) == 0 or migrations[-1] != our_migrations:
+            # we have a new migration, so add it
+            migrations.append(our_migrations)
+            temp_file = p.with_name('%s-%s.tmp' % (uuid.uuid4(), p.name))
+            with temp_file.open('w', encoding='utf-8') as tmp:
+                json.dump(data, tmp, ensure_ascii=True, indent=4)
+
+            temp_file.replace(p)
+
+        # update our "current" data in the filesystem
+        with current.open('w', encoding='utf-8') as fp:
+            json.dump(table_data, fp, indent=4, ensure_ascii=True)
+
+        return False
+
+    @classmethod
+    async def drop(cls, *, directory='migrations', verbose=False, connection=None):
+        """Drops the database and migrations, if any.
+
+        Parameters
+        -----------
+        directory: str
+            The migrations directory.
+        verbose: bool
+            Whether to output some information to stdout.
+        connection: Optional[asyncpg.Connection]
+            The connection to use, if not provided will acquire one from
+            the internal pool.
+        """
+
+        directory = Path(directory) / cls.__tablename__
+        p = directory.with_suffix('.json')
+        current = directory.with_name('current-' + p.name)
+
+        if not p.exists() or not current.exists():
+            raise RuntimeError('Could not find the appropriate data files.')
+
+        try:
+            p.unlink()
+        except:
+            raise RuntimeError('Could not delete migration file')
+
+        try:
+            current.unlink()
+        except:
+            raise RuntimeError('Could not delete current migration file')
+
+        async with MaybeAcquire(connection, pool=cls._pool) as con:
+            sql = 'DROP TABLE {0} CASCADE;'.format(cls.__tablename__)
+            if verbose:
+                print(sql)
+            await con.execute(sql)
+
+    @classmethod
+    def create_table(cls, *, exists_ok=True):
+        """Generates the CREATE TABLE stub."""
+        statements = []
+        builder = ['CREATE TABLE']
+
+        if exists_ok:
+            builder.append('IF NOT EXISTS')
+
+        builder.append(cls.__tablename__)
+        builder.append('(%s)' % ', '.join(c._create_table() for c in cls.columns))
+        statements.append(' '.join(builder) + ';')
+
+        # handle the index creations
+        for column in cls.columns:
+            if column.index:
+                fmt = 'CREATE INDEX IF NOT EXISTS {1.index_name} ON {0} ({1.name});'.format(cls.__tablename__, column)
+                statements.append(fmt)
+
+        return '\n'.join(statements)
+
+    @classmethod
+    async def insert(cls, connection=None, **kwargs):
+        """Inserts an element to the table."""
+
+        # verify column names:
+        verified = {}
+        for column in cls.columns:
+            try:
+                value = kwargs[column.name]
+            except KeyError:
+                continue
+
+            check = column.column_type.python
+            if value is None and not column.nullable:
+                raise TypeError('Cannot pass None to non-nullable column %s.' % column.name)
+            elif not check or not isinstance(value, check):
+                fmt = 'column {0.name} expected {1.__name__}, received {2.__class__.__name__}'
+                raise TypeError(fmt.format(column, check, value))
+
+            verified[column.name] = value
+
+        sql = 'INSERT INTO {0} ({1}) VALUES ({2});'.format(cls.__tablename__, ', '.join(verified),
+                                                           ', '.join('$' + str(i) for i, _ in enumerate(verified, 1)))
+
+        async with MaybeAcquire(connection, pool=cls._pool) as con:
+            await con.execute(sql, *verified.values())
+
+    @classmethod
+    def to_dict(cls):
+        x = {}
+        x['name'] = cls.__tablename__
+        x['__meta__'] = cls.__module__ + '.' + cls.__qualname__
+
+        # nb: columns is ordered due to the ordered dict usage
+        #     this is used to help detect renames
+        x['columns'] = [a._to_dict() for a in cls.columns]
+        return x
+
+    @classmethod
+    def from_dict(cls, data):
+        meta = data['__meta__']
+        given = cls.__module__ + '.' + cls.__qualname__
+        if given != meta:
+            cls = pydoc.locate(meta)
+            if cls is None:
+                raise RuntimeError('Could not locate "%s".' % meta)
+
+        self = cls()
+        self.__tablename__ = data['name']
+        self.columns = [Column.from_dict(a) for a in data['columns']]
+        return self
+
+    @classmethod
+    def all_tables(cls):
+        return cls.__subclasses__()
+
+    def diff(self, before):
+        """Outputs the upgrade and downgrade path in JSON.
+
+        This isn't necessarily good, but it outputs it in a format
+        that allows the user to manually make edits if something is wrong.
+
+        The following JSON schema is used:
+
+        Note that every major key takes a list of objects as noted below.
+
+        Note that add_column and drop_column automatically create and drop
+        indices as necessary.
+
+        changed_column_types:
+            name: str [The column name]
+            type: str [The new column type]
+            using: Optional[str] [The USING expression to use, if applicable]
+        add_columns:
+            column: object
+        remove_columns:
+            column: object
+        rename_columns:
+            before: str [The previous column name]
+            after:  str [The new column name]
+        drop_index:
+            name: str [The column name]
+            index: str [The index name]
+        add_index:
+            name: str [The column name]
+            index: str [The index name]
+        changed_constraints:
+            name: str [The column name]
+            before:
+                nullable: Optional[bool]
+                default: Optional[str]
+            after:
+                nullable: Optional[bool]
+                default: Optional[str]
+        """
+        upgrade = {}
+        downgrade = {}
+
+        def check_index_diff(a, b):
+            if a.index != b.index:
+                # Let's assume we have {name: thing, index: True}
+                # and we're going to { name: foo, index: False }
+                # This is a 'dropped' column when we upgrade with a rename
+                # care must be taken to use the old name when dropping
+
+                # check if we're dropping the index
+                if not a.index:
+                    # we could also be renaming so make sure to use the old index name
+                    upgrade.setdefault('drop_index', []).append({ 'name': a.name, 'index': b.index_name })
+                    # if we want to roll back, we need to re-add the old index to the old column name
+                    downgrade.setdefault('add_index', []).append({ 'name': b.name, 'index': b.index_name })
+                else:
+                    # we're not dropping an index, instead we're adding one
+                    upgrade.setdefault('add_index', []).append({ 'name': a.name, 'index': a.index_name })
+                    downgrade.setdefault('drop_index', []).append({ 'name': a.name, 'index': a.index_name })
+
+        def insert_column_diff(a, b):
+            if a.column_type != b.column_type:
+                if a.name == b.name and a.column_type.is_real_type() and b.column_type.is_real_type():
+                    upgrade.setdefault('changed_column_types', []).append({ 'name': a.name, 'type': a.column_type.to_sql() })
+                    downgrade.setdefault('changed_column_types', []).append({ 'name': a.name, 'type': b.column_type.to_sql() })
+                else:
+                    a_dict, b_dict = a._to_dict(), b._to_dict()
+                    upgrade.setdefault('add_columns', []).append(a_dict)
+                    upgrade.setdefault('remove_columns', []).append(b_dict)
+                    downgrade.setdefault('remove_columns', []).append(a_dict)
+                    downgrade.setdefault('add_columns', []).append(b_dict)
+                    check_index_diff(a, b)
+                    return
+
+            elif a._is_rename(b):
+                upgrade.setdefault('rename_columns', []).append({ 'before': b.name, 'after': a.name })
+                downgrade.setdefault('rename_columns', []).append({ 'before': a.name, 'after': b.name })
+
+            # technically, adding UNIQUE or PRIMARY KEY is rather simple and straight forward
+            # however, since the inverse is a little bit more complicated (you have to remove
+            # the index it maintains and you can't easily know what it is), it's not exactly
+            # worth supporting any sort of change to the uniqueness/primary_key as it stands.
+            # So.. just drop/add the column and call it a day.
+            if a.unique != b.unique or a.primary_key != b.primary_key:
+                a_dict, b_dict = a._to_dict(), b._to_dict()
+                upgrade.setdefault('add_columns', []).append(a_dict)
+                upgrade.setdefault('remove_columns', []).append(b_dict)
+                downgrade.setdefault('remove_columns', []).append(a_dict)
+                downgrade.setdefault('add_columns', []).append(b_dict)
+                check_index_diff(a, b)
+                return
+
+            check_index_diff(a, b)
+
+            b_qual, a_qual = b._qualifiers_dict(), a._qualifiers_dict()
+            if a_qual != b_qual:
+                upgrade.setdefault('changed_constraints', []).append({ 'name': a.name, 'before': b_qual, 'after': a_qual })
+                downgrade.setdefault('changed_constraints', []).append({ 'name': a.name, 'before': a_qual, 'after': b_qual })
+
+        if len(self.columns) == len(before.columns):
+            # check if we have any changes at all
+            for a, b in zip(self.columns, before.columns):
+                if a._comparable_id == b._comparable_id:
+                    # no change
+                    continue
+                insert_column_diff(a, b)
+
+        elif len(self.columns) > len(before.columns):
+            # check if we have more columns
+            # typically when we add columns we add them at the end of
+            # the table, this assumption makes this particularly bit easier.
+            # Breaking this assumption will probably break this portion and thus
+            # will require manual handling, sorry.
+
+            for a, b in zip(self.columns, before.columns):
+                if a._comparable_id == b._comparable_id:
+                    # no change
+                    continue
+                insert_column_diff(a, b)
+
+            new_columns = self.columns[len(before.columns):]
+            added = [c._to_dict() for c in new_columns]
+            upgrade.setdefault('add_columns', []).extend(added)
+            downgrade.setdefault('remove_columns', []).extend(added)
+        elif len(self.columns) < len(before.columns):
+            # check if we have fewer columns
+            # this one is a little bit more complicated
+
+            # first we sort the columns by comparable IDs.
+            sorted_before = sorted(before.columns, key=lambda c: c._comparable_id)
+            sorted_after  = sorted(self.columns, key=lambda c: c._comparable_id)
+
+            # handle the column diffs:
+            for a, b in zip(sorted_after, sorted_before):
+                if a._comparable_id == b._comparable_id:
+                    continue
+                insert_column_diff(a, b)
+
+            # check which columns are 'left over' and remove them
+            removed = [c._to_dict() for c in sorted_before[len(sorted_after):]]
+            upgrade.setdefault('remove_columns', []).extend(removed)
+            downgrade.setdefault('add_columns', []).extend(removed)
+
+        return SchemaDiff(self, upgrade, downgrade)
+
+
+async def _table_creator(tables, *, verbose=True):
+    for table in tables:
+        try:
+            await table.create(verbose=verbose)
+        except:
+            log.error('Failed to create table %s.', table.__tablename__)
+
+
+def create_tables(*tables, verbose=True, loop=None):
+    if loop is None:
+        loop = asyncio.get_event_loop()
+
+    loop.create_task(_table_creator(tables, verbose=verbose))
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/utils/formats.py b/All-In-One-bot/cogs/utils/formats.py
index 759aa2e03ae7f4b1ecd474f31b052c1d19ecc5af..f438d8094c21b409f00038a5cf1c642e9a3129cc 100644
--- a/All-In-One-bot/cogs/utils/formats.py
+++ b/All-In-One-bot/cogs/utils/formats.py
@@ -1,27 +1,77 @@
-# Borrowed from https://github.com/Rapptz/RoboDanny/tree/async
-
-
-async def entry_to_code(bot, entries):
-    width = max(map(lambda t: len(t[0]), entries))
-    output = ['```']
-    fmt = '{0:<{width}}: {1}'
-    for name, entry in entries:
-        output.append(fmt.format(name, entry, width=width))
-    output.append('```')
-    await bot.say('\n'.join(output))
-
-
-async def too_many_matches(bot, msg, matches, entry):
-    check = lambda m: m.content.isdigit()
-    await bot.say('There are too many matches... Which one did you mean?')
-    await bot.say('\n'.join(map(entry, enumerate(matches, 1))))
-
-    # only give them 3 tries.
-    for i in range(3):
-        message = await bot.wait_for_message(author=msg.author, channel=msg.channel, check=check)
-        index = int(message.content)
-        try:
-            return matches[index - 1]
-        except:
-            await bot.say('Please give me a valid number. {} tries remaining...'.format(2 - i))
-        raise ValueError('Too many tries. Goodbye.')
\ No newline at end of file
+class Plural:
+    def __init__(self, **attr):
+        iterator = attr.items()
+        self.name, self.value = next(iter(iterator))
+
+    def __str__(self):
+        v = self.value
+        if v == 0 or v > 1:
+            return f'{v} {self.name}s'
+        return f'{v} {self.name}'
+
+
+def human_join(seq, delim=', ', final='or'):
+    size = len(seq)
+    if size == 0:
+        return ''
+
+    if size == 1:
+        return seq[0]
+
+    if size == 2:
+        return f'{seq[0]} {final} {seq[1]}'
+
+    return delim.join(seq[:-1]) + f' {final} {seq[-1]}'
+
+class TabularData:
+    def __init__(self):
+        self._widths = []
+        self._columns = []
+        self._rows = []
+
+    def set_columns(self, columns):
+        self._columns = columns
+        self._widths = [len(c) + 2 for c in columns]
+
+    def add_row(self, row):
+        rows = [str(r) for r in row]
+        self._rows.append(rows)
+        for index, element in enumerate(rows):
+            width = len(element) + 2
+            if width > self._widths[index]:
+                self._widths[index] = width
+
+    def add_rows(self, rows):
+        for row in rows:
+            self.add_row(row)
+
+    def render(self):
+        """Renders a table in rST format.
+
+        Example:
+
+        +-------+-----+
+        | Name  | Age |
+        +-------+-----+
+        | Alice | 24  |
+        |  Bob  | 19  |
+        +-------+-----+
+        """
+
+        sep = '+'.join('-' * w for w in self._widths)
+        sep = f'+{sep}+'
+
+        to_draw = [sep]
+
+        def get_entry(d):
+            elem = '|'.join(f'{e:^{self._widths[i]}}' for i, e in enumerate(d))
+            return f'|{elem}|'
+
+        to_draw.append(get_entry(self._columns))
+        to_draw.append(sep)
+
+        for row in self._rows:
+            to_draw.append(get_entry(row))
+
+        to_draw.append(sep)
+        return '\n'.join(to_draw)
\ No newline at end of file
diff --git a/All-In-One-bot/cogs/utils/paginator.py b/All-In-One-bot/cogs/utils/paginator.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd912211e71c74b4fe6abe7fd469535a36c055b2
--- /dev/null
+++ b/All-In-One-bot/cogs/utils/paginator.py
@@ -0,0 +1,511 @@
+import asyncio
+import discord
+
+
+class CannotPaginate(Exception):
+    pass
+
+
+class Pages:
+    """Implements a paginator that queries the user for the
+    pagination interface.
+
+    Pages are 1-index based, not 0-index based.
+
+    If the user does not reply within 2 minutes then the pagination
+    interface exits automatically.
+
+    Parameters
+    ------------
+    ctx: Context
+        The context of the command.
+    entries: List[str]
+        A list of entries to paginate.
+    per_page: int
+        How many entries show up per page.
+    show_entry_count: bool
+        Whether to show an entry count in the footer.
+
+    Attributes
+    -----------
+    embed: discord.Embed
+        The embed object that is being used to send pagination info.
+        Feel free to modify this externally. Only the description,
+        footer fields, and colour are internally modified.
+    permissions: discord.Permissions
+        Our permissions for the channel.
+    """
+    def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True):
+        self.bot = ctx.bot
+        self.entries = entries
+        self.message = ctx.message
+        self.channel = ctx.channel
+        self.author = ctx.author
+        self.per_page = per_page
+        pages, left_over = divmod(len(self.entries), self.per_page)
+        if left_over:
+            pages += 1
+        self.maximum_pages = pages
+        self.embed = discord.Embed(colour=discord.Colour.blurple())
+        self.paginating = len(entries) > per_page
+        self.show_entry_count = show_entry_count
+        self.reaction_emojis = [
+            ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
+            ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
+            ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
+            ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
+            ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ),
+            ('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
+            ('\N{INFORMATION SOURCE}', self.show_help),
+        ]
+
+        if ctx.guild is not None:
+            self.permissions = self.channel.permissions_for(ctx.guild.me)
+        else:
+            self.permissions = self.channel.permissions_for(ctx.bot.user)
+
+        if not self.permissions.embed_links:
+            raise CannotPaginate('Bot does not have embed links permission.')
+
+        if not self.permissions.send_messages:
+            raise CannotPaginate('Bot cannot send messages.')
+
+        if self.paginating:
+            # verify we can actually use the pagination session
+            if not self.permissions.add_reactions:
+                raise CannotPaginate('Bot does not have add reactions permission.')
+
+            if not self.permissions.read_message_history:
+                raise CannotPaginate('Bot does not have Read Message History permission.')
+
+    def get_page(self, page):
+        base = (page - 1) * self.per_page
+        return self.entries[base:base + self.per_page]
+
+    async def show_page(self, page, *, first=False):
+        self.current_page = page
+        entries = self.get_page(page)
+        p = []
+        for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
+            p.append(f'{index}. {entry}')
+
+        if self.maximum_pages > 1:
+            if self.show_entry_count:
+                text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
+            else:
+                text = f'Page {page}/{self.maximum_pages}'
+
+            self.embed.set_footer(text=text)
+
+        if not self.paginating:
+            self.embed.description = '\n'.join(p)
+            return await self.channel.send(embed=self.embed)
+
+        if not first:
+            self.embed.description = '\n'.join(p)
+            await self.message.edit(embed=self.embed)
+            return
+
+        p.append('')
+        p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
+        self.embed.description = '\n'.join(p)
+        self.message = await self.channel.send(embed=self.embed)
+        for (reaction, _) in self.reaction_emojis:
+            if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
+                # no |<< or >>| buttons if we only have two pages
+                # we can't forbid it if someone ends up using it but remove
+                # it from the default set
+                continue
+
+            await self.message.add_reaction(reaction)
+
+    async def checked_show_page(self, page):
+        if page != 0 and page <= self.maximum_pages:
+            await self.show_page(page)
+
+    async def first_page(self):
+        """goes to the first page"""
+        await self.show_page(1)
+
+    async def last_page(self):
+        """goes to the last page"""
+        await self.show_page(self.maximum_pages)
+
+    async def next_page(self):
+        """goes to the next page"""
+        await self.checked_show_page(self.current_page + 1)
+
+    async def previous_page(self):
+        """goes to the previous page"""
+        await self.checked_show_page(self.current_page - 1)
+
+    async def show_current_page(self):
+        if self.paginating:
+            await self.show_page(self.current_page)
+
+    async def numbered_page(self):
+        """lets you type a page number to go to"""
+        to_delete = []
+        to_delete.append(await self.channel.send('What page do you want to go to?'))
+
+        def message_check(m):
+            return m.author == self.author and \
+                   self.channel == m.channel and \
+                   m.content.isdigit()
+
+        try:
+            msg = await self.bot.wait_for('message', check=message_check, timeout=30.0)
+        except asyncio.TimeoutError:
+            to_delete.append(await self.channel.send('Took too long.'))
+            await asyncio.sleep(5)
+        else:
+            page = int(msg.content)
+            to_delete.append(msg)
+            if page != 0 and page <= self.maximum_pages:
+                await self.show_page(page)
+            else:
+                to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})'))
+                await asyncio.sleep(5)
+
+        try:
+            await self.channel.delete_messages(to_delete)
+        except Exception:
+            pass
+
+    async def show_help(self):
+        """shows this message"""
+        messages = ['Welcome to the interactive paginator!\n']
+        messages.append('This interactively allows you to see pages of text by navigating with ' \
+                        'reactions. They are as follows:\n')
+
+        for (emoji, func) in self.reaction_emojis:
+            messages.append(f'{emoji} {func.__doc__}')
+
+        self.embed.description = '\n'.join(messages)
+        self.embed.clear_fields()
+        self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
+        await self.message.edit(embed=self.embed)
+
+        async def go_back_to_current_page():
+            await asyncio.sleep(60.0)
+            await self.show_current_page()
+
+        self.bot.loop.create_task(go_back_to_current_page())
+
+    async def stop_pages(self):
+        """stops the interactive pagination session"""
+        await self.message.delete()
+        self.paginating = False
+
+    def react_check(self, reaction, user):
+        if user is None or user.id != self.author.id:
+            return False
+
+        if reaction.message.id != self.message.id:
+            return False
+
+        for (emoji, func) in self.reaction_emojis:
+            if reaction.emoji == emoji:
+                self.match = func
+                return True
+        return False
+
+    async def paginate(self):
+        """Actually paginate the entries and run the interactive loop if necessary."""
+        first_page = self.show_page(1, first=True)
+        if not self.paginating:
+            await first_page
+        else:
+            # allow us to react to reactions right away if we're paginating
+            self.bot.loop.create_task(first_page)
+
+        while self.paginating:
+            try:
+                reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0)
+            except asyncio.TimeoutError:
+                self.paginating = False
+                try:
+                    await self.message.clear_reactions()
+                except:
+                    pass
+                finally:
+                    break
+
+            try:
+                await self.message.remove_reaction(reaction, user)
+            except:
+                pass # can't remove it so don't bother doing so
+
+            await self.match()
+
+class FieldPages(Pages):
+    """Similar to Pages except entries should be a list of
+    tuples having (key, value) to show as embed fields instead.
+    """
+    async def show_page(self, page, *, first=False):
+        self.current_page = page
+        entries = self.get_page(page)
+
+        self.embed.clear_fields()
+        self.embed.description = discord.Embed.Empty
+
+        for key, value in entries:
+            self.embed.add_field(name=key, value=value, inline=False)
+
+        if self.maximum_pages > 1:
+            if self.show_entry_count:
+                text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
+            else:
+                text = f'Page {page}/{self.maximum_pages}'
+
+            self.embed.set_footer(text=text)
+
+        if not self.paginating:
+            return await self.channel.send(embed=self.embed)
+
+        if not first:
+            await self.message.edit(embed=self.embed)
+            return
+
+        self.message = await self.channel.send(embed=self.embed)
+        for (reaction, _) in self.reaction_emojis:
+            if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
+                # no |<< or >>| buttons if we only have two pages
+                # we can't forbid it if someone ends up using it but remove
+                # it from the default set
+                continue
+
+            await self.message.add_reaction(reaction)
+
+import itertools
+import inspect
+import re
+
+# ?help
+# ?help Cog
+# ?help command
+#   -> could be a subcommand
+
+_mention = re.compile(r'<@\!?([0-9]{1,19})>')
+
+def cleanup_prefix(bot, prefix):
+    m = _mention.match(prefix)
+    if m:
+        user = bot.get_user(int(m.group(1)))
+        if user:
+            return f'@{user.name} '
+    return prefix
+
+async def _can_run(cmd, ctx):
+    try:
+        return await cmd.can_run(ctx)
+    except:
+        return False
+
+def _command_signature(cmd):
+    # this is modified from discord.py source
+    # which I wrote myself lmao
+
+    result = [cmd.qualified_name]
+    if cmd.usage:
+        result.append(cmd.usage)
+        return ' '.join(result)
+
+    params = cmd.clean_params
+    if not params:
+        return ' '.join(result)
+
+    for name, param in params.items():
+        if param.default is not param.empty:
+            # We don't want None or '' to trigger the [name=value] case and instead it should
+            # do [name] since [name=None] or [name=] are not exactly useful for the user.
+            should_print = param.default if isinstance(param.default, str) else param.default is not None
+            if should_print:
+                result.append(f'[{name}={param.default!r}]')
+            else:
+                result.append(f'[{name}]')
+        elif param.kind == param.VAR_POSITIONAL:
+            result.append(f'[{name}...]')
+        else:
+            result.append(f'<{name}>')
+
+    return ' '.join(result)
+
+class HelpPaginator(Pages):
+    def __init__(self, ctx, entries, *, per_page=4):
+        super().__init__(ctx, entries=entries, per_page=per_page)
+        self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help))
+        self.total = len(entries)
+
+    @classmethod
+    async def from_cog(cls, ctx, cog):
+        cog_name = cog.__class__.__name__
+
+        # get the commands
+        entries = sorted(ctx.bot.get_cog_commands(cog_name), key=lambda c: c.name)
+
+        # remove the ones we can't run
+        entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
+
+        self = cls(ctx, entries)
+        self.title = f'{cog_name} Commands'
+        self.description = inspect.getdoc(cog)
+        self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
+
+        # no longer need the database
+        await ctx.release()
+
+        return self
+
+    @classmethod
+    async def from_command(cls, ctx, command):
+        try:
+            entries = sorted(command.commands, key=lambda c: c.name)
+        except AttributeError:
+            entries = []
+        else:
+            entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
+
+        self = cls(ctx, entries)
+        self.title = command.signature
+
+        if command.description:
+            self.description = f'{command.description}\n\n{command.help}'
+        else:
+            self.description = command.help or 'No help given.'
+
+        self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
+        await ctx.release()
+        return self
+
+    @classmethod
+    async def from_bot(cls, ctx):
+        def key(c):
+            return c.cog_name or '\u200bMisc'
+
+        entries = sorted(ctx.bot.commands, key=key)
+        nested_pages = []
+        per_page = 9
+
+        # 0: (cog, desc, commands) (max len == 9)
+        # 1: (cog, desc, commands) (max len == 9)
+        # ...
+
+        for cog, commands in itertools.groupby(entries, key=key):
+            plausible = [cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden]
+            if len(plausible) == 0:
+                continue
+
+            description = ctx.bot.get_cog(cog)
+            if description is None:
+                description = discord.Embed.Empty
+            else:
+                description = inspect.getdoc(description) or discord.Embed.Empty
+
+            nested_pages.extend((cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page))
+
+        self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
+        self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
+        await ctx.release()
+
+        # swap the get_page implementation with one that supports our style of pagination
+        self.get_page = self.get_bot_page
+        self._is_bot = True
+
+        # replace the actual total
+        self.total = sum(len(o) for _, _, o in nested_pages)
+        return self
+
+    def get_bot_page(self, page):
+        cog, description, commands = self.entries[page - 1]
+        self.title = f'{cog} Commands'
+        self.description = description
+        return commands
+
+    async def show_page(self, page, *, first=False):
+        self.current_page = page
+        entries = self.get_page(page)
+
+        self.embed.clear_fields()
+        self.embed.description = self.description
+        self.embed.title = self.title
+
+        if hasattr(self, '_is_bot'):
+            value ='For more help, join the official bot support server: https://discord.gg/DWEaqMy'
+            self.embed.add_field(name='Support', value=value, inline=False)
+
+        self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.')
+
+        signature = _command_signature
+
+        for entry in entries:
+            self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False)
+
+        if self.maximum_pages:
+            self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)')
+
+        if not self.paginating:
+            return await self.channel.send(embed=self.embed)
+
+        if not first:
+            await self.message.edit(embed=self.embed)
+            return
+
+        self.message = await self.channel.send(embed=self.embed)
+        for (reaction, _) in self.reaction_emojis:
+            if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
+                # no |<< or >>| buttons if we only have two pages
+                # we can't forbid it if someone ends up using it but remove
+                # it from the default set
+                continue
+
+            await self.message.add_reaction(reaction)
+
+    async def show_help(self):
+        """shows this message"""
+
+        self.embed.title = 'Paginator help'
+        self.embed.description = 'Hello! Welcome to the help page.'
+
+        messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis]
+        self.embed.clear_fields()
+        self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False)
+
+        self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
+        await self.message.edit(embed=self.embed)
+
+        async def go_back_to_current_page():
+            await asyncio.sleep(30.0)
+            await self.show_current_page()
+
+        self.bot.loop.create_task(go_back_to_current_page())
+
+    async def show_bot_help(self):
+        """shows how to use the bot"""
+
+        self.embed.title = 'Using the bot'
+        self.embed.description = 'Hello! Welcome to the help page.'
+        self.embed.clear_fields()
+
+        entries = (
+            ('<argument>', 'This means the argument is __**required**__.'),
+            ('[argument]', 'This means the argument is __**optional**__.'),
+            ('[A|B]', 'This means the it can be __**either A or B**__.'),
+            ('[argument...]', 'This means you can have multiple arguments.\n' \
+                              'Now that you know the basics, it should be noted that...\n' \
+                              '__**You do not type in the brackets!**__')
+        )
+
+        self.embed.add_field(name='How do I use this bot?', value='Reading the bot signature is pretty simple.')
+
+        for name, value in entries:
+            self.embed.add_field(name=name, value=value, inline=False)
+
+        self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
+        await self.message.edit(embed=self.embed)
+
+        async def go_back_to_current_page():
+            await asyncio.sleep(30.0)
+            await self.show_current_page()
+
+        self.bot.loop.create_task(go_back_to_current_page())
diff --git a/All-In-One-bot/cogs/utils/time.py b/All-In-One-bot/cogs/utils/time.py
new file mode 100644
index 0000000000000000000000000000000000000000..32207b43411966f155e82dfb7f243a2d75c11b70
--- /dev/null
+++ b/All-In-One-bot/cogs/utils/time.py
@@ -0,0 +1,196 @@
+import datetime
+import parsedatetime as pdt
+from dateutil.relativedelta import relativedelta
+from .formats import Plural
+from discord.ext import commands
+import re
+
+
+class ShortTime:
+    compiled = re.compile("""(?:(?P<years>[0-9])(?:years?|y))?             # e.g. 2y
+                             (?:(?P<months>[0-9]{1,2})(?:months?|mo))?     # e.g. 2months
+                             (?:(?P<weeks>[0-9]{1,4})(?:weeks?|w))?        # e.g. 10w
+                             (?:(?P<days>[0-9]{1,5})(?:days?|d))?          # e.g. 14d
+                             (?:(?P<hours>[0-9]{1,5})(?:hours?|h))?        # e.g. 12h
+                             (?:(?P<minutes>[0-9]{1,5})(?:minutes?|m))?    # e.g. 10m
+                             (?:(?P<seconds>[0-9]{1,5})(?:seconds?|s))?    # e.g. 15s
+                          """, re.VERBOSE)
+
+    def __init__(self, argument):
+        match = self.compiled.fullmatch(argument)
+        if match is None or not match.group(0):
+            raise commands.BadArgument('invalid time provided')
+
+        data = { k: int(v) for k, v in match.groupdict(default=0).items() }
+        now = datetime.datetime.utcnow()
+        self.dt = now + relativedelta(**data)
+
+
+class HumanTime:
+    calendar = pdt.Calendar(version=pdt.VERSION_CONTEXT_STYLE)
+
+    def __init__(self, argument):
+        now = datetime.datetime.utcnow()
+        dt, status = self.calendar.parseDT(argument, sourceTime=now)
+        if not status.hasDateOrTime:
+            raise commands.BadArgument('invalid time provided, try e.g. "tomorrow" or "3 days"')
+
+        if not status.hasTime:
+            # replace it with the current time
+            dt = dt.replace(hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond)
+
+        self.dt = dt
+        self._past = dt < now
+
+
+class Time(HumanTime):
+    def __init__(self, argument):
+        try:
+            o = ShortTime(argument)
+        except Exception as e:
+            super().__init__(argument)
+        else:
+            self.dt = o.dt
+            self._past = False
+
+
+class FutureTime(Time):
+    def __init__(self, argument):
+        super().__init__(argument)
+
+        if self._past:
+            raise commands.BadArgument('this time is in the past')
+
+
+class UserFriendlyTime(commands.Converter):
+    """That way quotes aren't absolutely necessary."""
+    def __init__(self, converter=None, *, default=None):
+        if isinstance(converter, type) and issubclass(converter, commands.Converter):
+            converter = converter()
+
+        if converter is not None and not isinstance(converter, commands.Converter):
+            raise TypeError('commands.Converter subclass necessary.')
+
+        self.converter = converter
+        self.default = default
+
+    async def check_constraints(self, ctx, now, remaining):
+        if self.dt < now:
+            raise commands.BadArgument('This time is in the past.')
+
+        if not remaining:
+            if self.default is None:
+                raise commands.BadArgument('Missing argument after the time.')
+            remaining = self.default
+
+        if self.converter is not None:
+            self.arg = await self.converter.convert(ctx, remaining)
+        else:
+            self.arg = remaining
+        return self
+
+    async def convert(self, ctx, argument):
+        try:
+            calendar = HumanTime.calendar
+            regex = ShortTime.compiled
+            now = datetime.datetime.utcnow()
+
+            match = regex.match(argument)
+            if match is not None and match.group(0):
+                data = { k: int(v) for k, v in match.groupdict(default=0).items() }
+                remaining = argument[match.end():].strip()
+                self.dt = now + relativedelta(**data)
+                return await self.check_constraints(ctx, now, remaining)
+
+
+            # apparently nlp does not like "from now"
+            # it likes "from x" in other cases though so let me handle the 'now' case
+            if argument.endswith('from now'):
+                argument = argument[:-8].strip()
+
+            if argument[0:2] == 'me':
+                # starts with "me to" or "me in"
+                if argument[0:6] in ('me to ', 'me in '):
+                    argument = argument[6:]
+
+            elements = calendar.nlp(argument, sourceTime=now)
+            if elements is None or len(elements) == 0:
+                raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')
+
+            # handle the following cases:
+            # "date time" foo
+            # date time foo
+            # foo date time
+
+            # first the first two cases:
+            dt, status, begin, end, dt_string = elements[0]
+
+            if not status.hasDateOrTime:
+                raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')
+
+            if begin not in (0, 1) and end != len(argument):
+                raise commands.BadArgument('Time is either in an inappropriate location, which ' \
+                                           'must be either at the end or beginning of your input, ' \
+                                           'or I just flat out did not understand what you meant. Sorry.')
+
+            if not status.hasTime:
+                # replace it with the current time
+                dt = dt.replace(hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond)
+
+            self.dt =  dt
+
+            if begin in (0, 1):
+                if begin == 1:
+                    # check if it's quoted:
+                    if argument[0] != '"':
+                        raise commands.BadArgument('Expected quote before time input...')
+
+                    if not (end < len(argument) and argument[end] == '"'):
+                        raise commands.BadArgument('If the time is quoted, you must unquote it.')
+
+                    remaining = argument[end + 1:].lstrip(' ,.!')
+                else:
+                    remaining = argument[end:].lstrip(' ,.!')
+            elif len(argument) == end:
+                remaining = argument[:begin].strip()
+
+            return await self.check_constraints(ctx, now, remaining)
+        except:
+            import traceback
+            traceback.print_exc()
+            raise
+
+
+def human_timedelta(dt, *, source=None):
+    now = source or datetime.datetime.utcnow()
+    if dt > now:
+        delta = relativedelta(dt, now)
+        suffix = ''
+    else:
+        delta = relativedelta(now, dt)
+        suffix = ' ago'
+
+    if delta.microseconds and delta.seconds:
+        delta = delta + relativedelta(seconds=+1)
+
+    attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
+
+    output = []
+    for attr in attrs:
+        elem = getattr(delta, attr)
+        if not elem:
+            continue
+
+        if elem > 1:
+            output.append(f'{elem} {attr}')
+        else:
+            output.append(f'{elem} {attr[:-1]}')
+
+    if len(output) == 0:
+        return 'now'
+    elif len(output) == 1:
+        return output[0] + suffix
+    elif len(output) == 2:
+        return f'{output[0]} and {output[1]}{suffix}'
+    else:
+        return f'{output[0]}, {output[1]} and {output[2]}{suffix}'
\ No newline at end of file
diff --git a/All-In-One-bot/config.py b/All-In-One-bot/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..d31f5790481f9fbc424e47ebc54755872ace6575
--- /dev/null
+++ b/All-In-One-bot/config.py
@@ -0,0 +1,3 @@
+client_id = '440790222347829258'
+token = 'NDQwNzkwMjIyMzQ3ODI5MjU4.DjfwxQ.blSXGcE4xda6jpTas0h8nBJs4OY'
+postgresql = 'postgresql://samip537:VgZeE7BBj9@localhost/samip537'
\ No newline at end of file
diff --git a/All-In-One-bot/launcher.py b/All-In-One-bot/launcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2a0f5ba30963cf2bd20d6ba243db40cbd481b66
--- /dev/null
+++ b/All-In-One-bot/launcher.py
@@ -0,0 +1,329 @@
+import sys
+import click
+import logging
+import asyncio
+import asyncpg
+import discord
+import importlib
+import contextlib
+
+from bot import RoboSamip, initial_extensions
+from cogs.utils.db import Table
+
+from pathlib import Path
+import config
+import traceback
+
+try:
+    import uvloop
+except ImportError:
+    pass
+else:
+    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
+
+
+@contextlib.contextmanager
+def setup_logging():
+    try:
+        # __enter__
+        logging.getLogger('discord').setLevel(logging.INFO)
+        logging.getLogger('discord.http').setLevel(logging.WARNING)
+
+        log = logging.getLogger()
+        log.setLevel(logging.INFO)
+        handler = logging.FileHandler(filename='samipbot.log', encoding='utf-8', mode='w')
+        dt_fmt = '%Y-%m-%d %H:%M:%S'
+        fmt = logging.Formatter('[{asctime}] [{levelname:<7}] {name}: {message}', dt_fmt, style='{')
+        handler.setFormatter(fmt)
+        log.addHandler(handler)
+
+        yield
+    finally:
+        # __exit__
+        handlers = log.handlers[:]
+        for hdlr in handlers:
+            hdlr.close()
+            log.removeHandler(hdlr)
+
+
+def run_bot():
+    loop = asyncio.get_event_loop()
+    log = logging.getLogger()
+
+    try:
+        pool = loop.run_until_complete(Table.create_pool(config.postgresql, command_timeout=60))
+    except Exception as e:
+        click.echo('Could not set up PostgreSQL. Exiting.', file=sys.stderr)
+        log.exception('Could not set up PostgreSQL. Exiting.')
+        return
+
+    bot = RoboSamip()
+    bot.pool = pool
+    bot.run()
+
+
+@click.group(invoke_without_command=True, options_metavar='[options]')
+@click.pass_context
+def main(ctx):
+    """Launches the bot."""
+    if ctx.invoked_subcommand is None:
+        loop = asyncio.get_event_loop()
+        with setup_logging():
+            run_bot()
+
+
+@main.group(short_help='database stuff', options_metavar='[options]')
+def db():
+    pass
+
+
+@db.command(short_help='initialises the databases for the bot', options_metavar='[options]')
+@click.argument('cogs', nargs=-1, metavar='[cogs]')
+@click.option('-q', '--quiet', help='less verbose output', is_flag=True)
+def init(cogs, quiet):
+    """This manages the migrations and database creation system for you."""
+
+    run = asyncio.get_event_loop().run_until_complete
+    try:
+        run(Table.create_pool(config.postgresql))
+    except Exception:
+        click.echo(f'Could not create PostgreSQL connection pool.\n{traceback.format_exc()}', err=True)
+        return
+
+    if not cogs:
+        cogs = initial_extensions
+    else:
+        cogs = [f'cogs.{e}' if not e.startswith('cogs.') else e for e in cogs]
+
+    for ext in cogs:
+        try:
+            importlib.import_module(ext)
+        except Exception:
+            click.echo(f'Could not load {ext}.\n{traceback.format_exc()}', err=True)
+            return
+
+    for table in Table.all_tables():
+        try:
+            created = run(table.create(verbose=not quiet, run_migrations=False))
+        except Exception:
+            click.echo(f'Could not create {table.__tablename__}.\n{traceback.format_exc()}', err=True)
+        else:
+            if created:
+                click.echo(f'[{table.__module__}] Created {table.__tablename__}.')
+            else:
+                click.echo(f'[{table.__module__}] No work needed for {table.__tablename__}.')
+
+
+@db.command(short_help='migrates the databases')
+@click.argument('cog', nargs=1, metavar='[cog]')
+@click.option('-q', '--quiet', help='less verbose output', is_flag=True)
+@click.pass_context
+def migrate(ctx, cog, quiet):
+    """Update the migration file with the newest schema."""
+
+    if not cog.startswith('cogs.'):
+        cog = f'cogs.{cog}'
+
+    try:
+        importlib.import_module(cog)
+    except Exception:
+        click.echo(f'Could not load {ext}.\n{traceback.format_exc()}', err=True)
+        return
+
+    def work(table, *, invoked=False):
+        try:
+            actually_migrated = table.write_migration()
+        except RuntimeError as e:
+            click.echo(f'Could not migrate {table.__tablename__}: {e}', err=True)
+            if not invoked:
+                click.confirm('do you want to create the table?', abort=True)
+                ctx.invoke(init, cogs=[cog], quiet=quiet)
+                work(table, invoked=True)
+            sys.exit(-1)
+        else:
+            if actually_migrated:
+                click.echo(f'Successfully updated migrations for {table.__tablename__}.')
+            else:
+                click.echo(f'Found no changes for {table.__tablename__}.')
+
+    for table in Table.all_tables():
+        work(table)
+
+    click.echo(f'Done migrating {cog}.')
+
+
+async def apply_migration(cog, quiet, index, *, downgrade=False):
+    try:
+        pool = await Table.create_pool(config.postgresql)
+    except Exception:
+        click.echo(f'Could not create PostgreSQL connection pool.\n{traceback.format_exc()}', err=True)
+        return
+
+    if not cog.startswith('cogs.'):
+        cog = f'cogs.{cog}'
+
+    try:
+        importlib.import_module(cog)
+    except Exception:
+        click.echo(f'Could not load {cog}.\n{traceback.format_exc()}', err=True)
+        return
+
+    async with pool.acquire() as con:
+        tr = con.transaction()
+        await tr.start()
+        for table in Table.all_tables():
+            try:
+                await table.migrate(index=index, downgrade=downgrade, verbose=not quiet, connection=con)
+            except RuntimeError as e:
+                click.echo(f'Could not migrate {table.__tablename__}: {e}', err=True)
+                await tr.rollback()
+                break
+        else:
+            await tr.commit()
+
+
+@db.command(short_help='upgrades from a migration')
+@click.argument('cog', nargs=1, metavar='[cog]')
+@click.option('-q', '--quiet', help='less verbose output', is_flag=True)
+@click.option('--index', help='the index to use', default=-1)
+def upgrade(cog, quiet, index):
+    """Runs an upgrade from a migration"""
+    run = asyncio.get_event_loop().run_until_complete
+    run(apply_migration(cog, quiet, index))
+
+
+@db.command(short_help='downgrades from a migration')
+@click.argument('cog', nargs=1, metavar='[cog]')
+@click.option('-q', '--quiet', help='less verbose output', is_flag=True)
+@click.option('--index', help='the index to use', default=-1)
+def downgrade(cog, quiet, index):
+    """Runs an downgrade from a migration"""
+    run = asyncio.get_event_loop().run_until_complete
+    run(apply_migration(cog, quiet, index, downgrade=True))
+
+
+async def remove_databases(pool, cog, quiet):
+    async with pool.acquire() as con:
+        tr = con.transaction()
+        await tr.start()
+        for table in Table.all_tables():
+            try:
+                await table.drop(verbose=not quiet, connection=con)
+            except RuntimeError as e:
+                click.echo(f'Could not drop {table.__tablename__}: {e}', err=True)
+                await tr.rollback()
+                break
+            else:
+                click.echo(f'Dropped {table.__tablename__}.')
+        else:
+            await tr.commit()
+            click.echo(f'successfully removed {cog} tables.')
+
+
+@db.command(short_help="removes a cog's table", options_metavar='[options]')
+@click.argument('cog',  metavar='<cog>')
+@click.option('-q', '--quiet', help='less verbose output', is_flag=True)
+def drop(cog, quiet):
+    """This removes a database and all its migrations.
+
+    You must be pretty sure about this before you do it,
+    as once you do it there's no coming back.
+
+    Also note that the name must be the database name, not
+    the cog name.
+    """
+
+    run = asyncio.get_event_loop().run_until_complete
+    click.confirm('do you really want to do this?', abort=True)
+
+    try:
+        pool = run(Table.create_pool(config.postgresql))
+    except Exception:
+        click.echo(f'Could not create PostgreSQL connection pool.\n{traceback.format_exc()}', err=True)
+        return
+
+    if not cog.startswith('cogs.'):
+        cog = f'cogs.{cog}'
+
+    try:
+        importlib.import_module(cog)
+    except Exception:
+        click.echo(f'Could not load {cog}.\n{traceback.format_exc()}', err=True)
+        return
+
+    run(remove_databases(pool, cog, quiet))
+
+
+@main.command(short_help='migrates from JSON files')
+@click.argument('cogs', nargs=-1)
+@click.pass_context
+def convertjson(ctx, cogs):
+    """This migrates our older JSON files to PostgreSQL
+
+    Note, this deletes all previous entries in the table
+    so you can consider this to be a destructive decision.
+
+    Do not pass in cog names with "cogs." as a prefix.
+
+    This also connects us to Discord itself so we can
+    use the cache for our migrations.
+
+    The point of this is just to do some migration of the
+    data from v3 -> v4 once and call it a day.
+    """
+
+    import data_migrators
+
+    run = asyncio.get_event_loop().run_until_complete
+
+    if not cogs:
+        to_run = [(getattr(data_migrators, attr), attr.replace('migrate_', ''))
+                  for attr in dir(data_migrators) if attr.startswith('migrate_')]
+    else:
+        to_run = []
+        for cog in cogs:
+            try:
+                elem = getattr(data_migrators, 'migrate_' + cog)
+            except AttributeError:
+                click.echo(f'invalid cog name given, {cog}.', err=True)
+                return
+
+            to_run.append((elem, cog))
+
+    async def make_pool():
+        return await asyncpg.create_pool(config.postgresql)
+
+    try:
+        pool = run(make_pool())
+    except Exception:
+        click.echo(f'Could not create PostgreSQL connection pool.\n{traceback.format_exc()}', err=True)
+        return
+
+    client = discord.AutoShardedClient()
+
+    @client.event
+    async def on_ready():
+        click.echo(f'successfully booted up bot {client.user} (ID: {client.user.id})')
+        await client.logout()
+
+    try:
+        run(client.login(config.token))
+        run(client.connect(reconnect=False))
+    except:
+        pass
+
+    extensions = ['cogs.' + name for _, name in to_run]
+    ctx.invoke(init, cogs=extensions)
+
+    for migrator, _ in to_run:
+        try:
+            run(migrator(pool, client))
+        except Exception:
+            click.echo(f'[error] {migrator.__name__} has failed, terminating\n{traceback.format_exc()}', err=True)
+            return
+        else:
+            click.echo(f'[{migrator.__name__}] completed successfully')
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/All-In-One-bot/main.py b/All-In-One-bot/main.py
deleted file mode 100644
index a6cbbd17bf353108aecc4f3fe23512d9ac0375c0..0000000000000000000000000000000000000000
--- a/All-In-One-bot/main.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# Author: samip5
-# Version: 0.5
-
-import configparser as parser
-import datetime
-import logging
-import sys
-import traceback
-import asyncpg
-
-import discord
-from discord import Game
-from discord.ext import commands
-
-config = parser.ConfigParser()
-
-config.read('./config.ini')
-
-TOKEN = config['secrets']['BOT-DEVELOMENT-TOKEN']
-
-logger_info = logging.getLogger('discord')
-# logger_debug = logging.getLogger('discord')
-logger_info.setLevel(logging.INFO)
-# logger_debug.setLevel(logging.DEBUG)
-logFile_info_handler = logging.FileHandler(filename='discord_info.log', encoding='utf-8', mode='w')
-# logFile_debug_handler = logging.FileHandler(filename='discord_debug.log', encoding='utf-8', mode='w')
-logFile_info_handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
-# logFile_debug_handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
-logger_info.addHandler(logFile_info_handler)
-# logger_debug.addHandler(logFile_debug_handler)
-
-
-def get_prefix(bot, message):
-    prefixes = ['!']
-    return commands.when_mentioned_or(*prefixes)(bot, message)
-
-
-initial_extensions = ['cogs.general', 'cogs.plex', 'cogs.ombi', 'cogs.misc', 'cogs.meta', 'cogs.mod']
-
-help_attrs = dict(hidden=True)
-bot = commands.Bot(command_prefix=get_prefix, help_attrs=help_attrs)
-
-
-# @bot.event
-# async def on_command_error(error, ctx):
-#    if isinstance(error, commands.NoPrivateMessage):
-#        await bot.send_message(ctx.message.author, 'This command cannot be used in private messages.')
-#    elif isinstance(error, commands.DisabledCommand):
-#        await bot.send_message(ctx.message.author, 'Sorry. This command is disabled and cannot be used.')
-
-@bot.event
-async def on_ready():
-    bot.uptime = datetime.datetime.utcnow()
-    credentials = {
-        "user": "samip537",
-        "password": "VgZeE7BBj9",
-        "database": "samip537",
-        "host": "psql1.n.kapsi.fi"
-    }
-    try:
-        bot.pool = await asyncpg.create_pool(**credentials)
-    except TimeoutError:
-        pass
-    print (f'\n\nLogged in as: {bot.user.name} - {bot.user.id}\nAPI Version: {discord.__version__}\n')
-    await bot.change_presence(game=Game(name="with code"))
-    print (f'Successfully connected.')
-
-
-@bot.event
-async def on_message(message):
-    mod = bot.get_cog('Mod')
-
-    if mod is not None and not checks.is_owner_check(message):
-        # check if the user is bot banned
-        if message.author.id in mod.config.get('plonks', []):
-            return
-
-        # check if the channel is ignored
-        # but first, resolve their permissions
-
-        perms = message.channel.permissions_for(message.author)
-        bypass_ignore = perms.manage_roles
-
-        # if we don't have manage roles then we should
-        # check if it's the owner of the bot or they have Bot Admin role.
-
-        if not bypass_ignore:
-            if not message.channel.is_private:
-                bypass_ignore = discord.utils.get(message.author.roles, name='Bot Admin') is not None
-
-        # now we can finally realise if we can actually bypass the ignore.
-
-        if not bypass_ignore:
-            if message.channel.id in mod.config.get('ignored', []):
-                return
-
-    # if someone private messages us with something that looks like a URL then
-    # we should try to see if it's an invite to a discord server and join it if so.
-    if message.channel.is_private and message.content.startswith('http'):
-        try:
-            invite = await bot.get_invite(message.content)
-            await bot.accept_invite(invite)
-            await bot.send_message(message.channel, 'Joined the server.')
-        except:
-            # if an error occurs at this point then ignore it and move on.
-            pass
-        finally:
-            return
-    await bot.process_commands(message)
-
-if __name__ == '__main__':
-    if any('debug' in arg.lower() for arg in sys.argv):
-        bot.command_prefix = '$'
-    for extension in initial_extensions:
-        try:
-            bot.load_extension(extension)
-        except:
-            print(f'Failed to load extension {extension}.', file=sys.stderr)
-            traceback.print_exc()
-
-bot.run(TOKEN, bot=True, reconnect=True)
diff --git a/All-In-One-bot/requiments.txt b/All-In-One-bot/requiments.txt
new file mode 100644
index 0000000000000000000000000000000000000000..39cd25759ad6a8f605dafb5b727c57796f06b41b
--- /dev/null
+++ b/All-In-One-bot/requiments.txt
@@ -0,0 +1,10 @@
+git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice]
+lxml
+psutil
+asyncpg>=0.12.0
+click
+parsedatetime
+lru_dict
+python-dateutil
+plexapi
+requests
\ No newline at end of file
diff --git a/Music-bot/cogs/meta.py b/Music-bot/cogs/meta.py
index 9f7de8ab445fb8fd4b4ab38b3585ec2a0a3fcd3e..337b45672650bc2b33a3298f54b146945bd66074 100644
--- a/Music-bot/cogs/meta.py
+++ b/Music-bot/cogs/meta.py
@@ -17,9 +17,9 @@ class Meta:
         minutes, seconds = divmod(remainder, 60)
         days, hours = divmod(hours, 24)
         if days:
-            fmt = '{d} days, {h} hours, {m} minutes, and {s} seconds'
+            fmt = '{d} days, {h} hours and {m} minutes'
         else:
-            fmt = '{h} hours, {m} minutes, and {s} seconds'
+            fmt = '{h} hours and {m} minutes'
         return fmt.format(d=days, h=hours, m=minutes, s=seconds)
 
     def get_system_uptime(self):
@@ -53,8 +53,7 @@ class Meta:
         if len(string) > 0 or hours > 0:
             string += str(hours) + " " + (hours == 1 and "hour" or "hours") + ", "
         if len(string) > 0 or minutes > 0:
-            string += str(minutes) + " " + (minutes == 1 and "minute" or "minutes") + ", "
-        string += str(seconds) + " " + (seconds == 1 and "second" or "seconds")
+            string += str(minutes) + " " + (minutes == 1 and "minute" or "minutes")
 
         return string
 
diff --git a/Music-bot/cogs/mod.py b/Music-bot/cogs/mod.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cb0ba3394cf12d8d110ac7e58d835403125c5a3
--- /dev/null
+++ b/Music-bot/cogs/mod.py
@@ -0,0 +1,132 @@
+import discord
+
+from discord.ext import commands
+from .utils import config, checks
+
+
+class Mod:
+	"""Moderation related commands."""
+
+	def __init__(self, bot):
+		self.bot = bot
+		self.config = config.Config('mod.json', loop=bot.loop)
+
+	def bot_user(self, message):
+		return message.server.me if message.channel.is_private else self.bot.user
+
+	@commands.command(no_pm=True)
+	@checks.admin_or_permissions(manage_server=True)
+	async def plonk(self, ctx,  *, member: discord.Member):
+		"""Bans a user from using the bot.
+		Note that this ban is **global**. So they are banned from
+		all servers that they access the bot with. So use this with
+		caution.
+		There is no way to bypass a plonk regardless of role or permissions.
+		The only person who cannot be plonked is the bot creator. So this
+		must be used with caution.
+		To use this command you must have the Manage Server permission
+		or have a Bot Admin role.
+		"""
+
+		plonks = self.config.get('plonks', [])
+		if member.id in plonks:
+			await ctx.send('That user is already bot banned.')
+			return
+
+		plonks.append(member.id)
+		await self.config.put('plonks', plonks)
+		await ctx.send('{0.name} has been banned from using the bot.'.format(member))
+
+	@commands.command(no_pm=True)
+	@checks.admin_or_permissions(manage_server=True)
+	async def unplonk(self, ctx, *, member: discord.Member):
+		"""Unbans a user from using the bot.
+		To use this command you must have the Manage Server permission
+		or have a Bot Admin role.
+		"""
+
+		plonks = self.config.get('plonks', [])
+
+		try:
+			plonks.remove(member.id)
+		except ValueError:
+			pass
+		else:
+			await self.config.put('plonks', plonks)
+			await ctx.send('{0.name} has been unbanned from using the bot.'.format(member))
+
+	@commands.group(pass_context=True, no_pm=True)
+	@checks.admin_or_permissions(manage_channel=True)
+	async def ignore(self, ctx):
+		"""Handles the bot's ignore lists.
+		To use these commands, you must have the Bot Admin role or have
+		Manage Channel permissions. These commands are not allowed to be used
+		in a private message context.
+		Users with Manage Roles or Bot Admin role can still invoke the bot
+		in ignored channels.
+		"""
+		if ctx.invoked_subcommand is None:
+			await ctx.send('Invalid subcommand passed: {0.subcommand_passed}'.format(ctx))
+
+	@ignore.command(name='channel', pass_context=True)
+	async def channel_cmd(self, ctx, *, channel: discord.guild.TextChannel = None):
+		"""Ignores a specific channel from being processed.
+		If no channel is specified, the current channel is ignored.
+		If a channel is ignored then the bot does not process commands in that
+		channel until it is unignored.
+		"""
+
+		if channel is None:
+			channel = ctx.message.channel
+
+		ignored = self.config.get('ignored', [])
+		if channel.id in ignored:
+			await ctx.send('That channel is already ignored.')
+			return
+
+		ignored.append(channel.id)
+		await self.config.put('ignored', ignored)
+		await ctx.send('\U0001f44c')
+
+	@ignore.command(name='all', pass_context=True)
+	@checks.admin_or_permissions(manage_server=True)
+	async def _all(self, ctx):
+		"""Ignores every channel in the server from being processed.
+		This works by adding every channel that the server currently has into
+		the ignore list. If more channels are added then they will have to be
+		ignored by using the ignore command.
+		To use this command you must have Manage Server permissions along with
+		Manage Channel permissions. You could also have the Bot Admin role.
+		"""
+
+		ignored = self.config.get('ignored', [])
+		channels = ctx.message.server.channels
+		ignored.extend(c.id for c in channels if c.type == discord.ChannelType.text)
+		await self.config.put('ignored', list(set(ignored)))  # make unique
+		await ctx.send('\U0001f44c')
+
+	@commands.command(pass_context=True, no_pm=True)
+	@checks.admin_or_permissions(manage_channel=True)
+	async def unignore(self, ctx, *, channel: discord.guild.TextChannel = None):
+		"""Unignores a specific channel from being processed.
+		If no channel is specified, it unignores the current channel.
+		To use this command you must have the Manage Channel permission or have the
+		Bot Admin role.
+		"""
+
+		if channel is None:
+			channel = ctx.message.channel
+
+		# a set is the proper data type for the ignore list
+		# however, JSON only supports arrays and objects not sets.
+		ignored = self.config.get('ignored', [])
+		try:
+			ignored.remove(channel.id)
+		except ValueError:
+			await ctx.send('Channel was not ignored in the first place.')
+		else:
+			await ctx.send('\U0001f44c')
+
+
+def setup(bot):
+	bot.add_cog(Mod(bot))
diff --git a/Music-bot/cogs/music.py b/Music-bot/cogs/music.py
index 813d3c43504ba52036c440cdd4385efffede6077..90fc6d63db84aa995279192cac184b807adb9317 100644
--- a/Music-bot/cogs/music.py
+++ b/Music-bot/cogs/music.py
@@ -17,7 +17,6 @@ import itertools
 import sys
 import traceback
 from functools import partial
-from typing import List
 
 import discord
 import youtube_dl
diff --git a/Music-bot/cogs/utils/checks.py b/Music-bot/cogs/utils/checks.py
index 1c7230b8b8b6e028e89695ea4e996d322fd7ed14..336149f43ddd3e1b554c9aedb6a69543bfaeed1b 100644
--- a/Music-bot/cogs/utils/checks.py
+++ b/Music-bot/cogs/utils/checks.py
@@ -103,41 +103,39 @@ def song_requester_or_dj():
 def song_requester_or_owner_or_dj():
     async def predicate(ctx):
         is_owner = await ctx.bot.is_owner(ctx.author)
-        vc = ctx.voice_client
-
-        if not vc or not vc.is_connected():
-            await ctx.send('I am not currently connected to voice!')
+        try:
+            vc = ctx.voice_client
 
-        if ctx.message.author.name == vc.source.requester.name:
-            return True
-        elif is_owner:
-            return True
-        elif discord.utils.get(ctx.message.author.roles, name="DJ"):
-            return True
-        elif ctx.message.author != vc.source.requester or not discord.utils.get(ctx.message.author.roles, name="DJ"):
-            await ctx.send(f"The bot owner has been naughty and has failed to add a voting system, so sorry but your "
+            if ctx.message.author.name == vc.source.requester.name:
+                return True
+            elif is_owner:
+                return True
+            elif discord.utils.get(ctx.message.author.roles, name="DJ"):
+                return True
+            elif ctx.message.author != vc.source.requester or not discord.utils.get(ctx.message.author.roles, name="DJ"):
+                await ctx.send(f"The bot owner has been naughty and has failed to add a voting system, so sorry but your "
                            f"permissions are not enough to use this command.")
-            return False
-
+                return False
+        except AttributeError:
+            pass
     return commands.check(predicate)
 
 
 def music_stop_check():
     async def predicate(ctx):
         is_owner = await ctx.bot.is_owner(ctx.author)
-        vc = ctx.voice_client
-
-        if not vc or not vc.is_connected():
-            await ctx.send('I am not currently connected to voice!')
-
-        if ctx.message.author.name == vc.source.requester.name:
-            return True
-        elif is_owner:
-            return True
-        elif discord.utils.get(ctx.message.author.roles, name="DJ"):
-            return True
-        elif ctx.message.author != vc.source.requester or not discord.utils.get(ctx.message.author.roles, name="DJ"):
-            await ctx.send(f"You lack the permissions to use this command, sorry.")
-            return False
+        try:
+            vc = ctx.voice_client
 
+            if ctx.message.author.name == vc.source.requester.name:
+                return True
+            elif is_owner:
+                return True
+            elif discord.utils.get(ctx.message.author.roles, name="DJ"):
+                return True
+            elif ctx.message.author != vc.source.requester or not discord.utils.get(ctx.message.author.roles, name="DJ"):
+                await ctx.send(f"You lack the permissions to use this command, sorry.")
+                return False
+        except AttributeError:
+            pass
     return commands.check(predicate)
\ No newline at end of file