# -*- coding: utf-8 -*-
import asyncio
import copy
import io
import json
import re
import secrets
import textwrap
import typing
from datetime import datetime, timedelta
from textwrap import shorten as shrt

import discord
import humanize as hz
from discord import utils as _u
from discord.ext import commands
from eekues import Confirm

import config
from cogs.internal.utils import IgnoredChannel, IgnoredRole, checks
from cogs.internal.utils.checks import admin_or_permissions, review_or_permissions
from cogs.internal.utils.classes import ScrollingEmbedPage
from config import Emojis
from models import sql
from models.misc import App, PartialApp
from .internal.utils import commands as custom_commands


def usable_reaction(_reaction: discord.Reaction, _user: discord.User):
    reaction, user = _reaction, _user
    emoji = reaction.emoji
    if isinstance(emoji, str):
        return True  # unicode
    else:
        if not isinstance(emoji, discord.Emoji):
            return False
        else:
            return emoji.is_usable()


def wrapped_usable_reaction(message):
    def wrapped_wrapped_usable_reaction(r, u):
        def wrapped_wrapped_wrapped_usable_reaction():
            return usable_reaction(r, u)

        return wrapped_wrapped_wrapped_usable_reaction() and r.message.id == message.id

    return wrapped_wrapped_usable_reaction


def override(ctx, *other):
    async def internal(mes):
        if mes.guild != ctx.guild:
            return
        member = mes.author
        if not isinstance(mes.author, discord.Member):
            member = await ctx.guild.fetch_member(mes.author.id)

        overridden = mes.author.guild_permissions.manage_roles and mes.content.lower() == "force cancel"
        return () or all((func(mes) for func in other))

    return internal


async def wf_msg_or_r(
    ctx: commands.Context,
    *,
    timeout: float = 120.0,
    checks: typing.List[callable] = None,
    message: discord.Message = None,
    **pairs,
):
    """
    Waits for a message and/or a reaction, whichever comes first.

    :param message:
    :param ctx: You know what this is.
    :param timeout: the timeout to pass to wait_for. Defaults to 120s (2 minutes)
    :param checks: the custom checks to use. If not supplied, will auto-generate based on ctx
    :param pairs: the {content: emoji} pairs.
    :return: the corresponding emoji
    """

    def can_bypass(reaction, user):
        if user.bot:
            return False
        return (
            isinstance(user, discord.Member)
            and user.guild_permissions.administrator
            and str(reaction.emoji) == "\U000023f9"
        )

    def accept(mes):
        if mes.author.bot:
            return False
        if mes.guild != ctx.guild or not mes.guild.chunked:
            return False
        if mes.channel != ctx.channel:
            return False
        if (
            mes.author == ctx.author
            or mes.content.lower() == "force cancel"
            and mes.author.guild_permissions.administrator
        ):
            return True
        return False

    message = message or ctx.message
    if not checks:

        def reaction_check(r: discord.Reaction, u: discord.User):
            if str(r.emoji) in pairs.values():
                if u.id == ctx.author.id or can_bypass(r, u):
                    if r.message.id == message.id:
                        return True
            return False

        def message_check(mess):
            if mess.content:
                if mess.content.lower() in pairs.keys():
                    if mess.author == ctx.author:
                        if mess.channel == ctx.channel:
                            return True
            return accept(mess) and mess.content.lower() in pairs.keys()

        checks = [message_check, reaction_check]

    tasks = [ctx.bot.wait_for("message", check=checks[0]), ctx.bot.wait_for("reaction_add", check=checks[1])]
    done, pending = await asyncio.wait(tasks, timeout=timeout or 120.0, return_when="FIRST_COMPLETED")
    for task in pending:
        task.cancel()
    if not done:
        raise asyncio.TimeoutError()
    result = done.pop()
    if result.exception():
        raise result.exception()

    resolved = result.result()

    if isinstance(resolved, discord.Message):
        return pairs[resolved.content.lower()]
    return resolved[0].emoji


class Mapping(dict):
    def __missing__(self, key):
        return "{%s}" % key


class Admin(commands.Cog):
    """The description for Admin goes here."""

    def __init__(self, bot):
        self.bot = bot

    async def bot_check(self, ctx):
        if not ctx.guild or isinstance(ctx.author, discord.User):
            return True
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        if not guild:
            return True
        if guild.ignored_channels and ctx.channel.id in guild.ignored_channels:
            raise IgnoredChannel(f"The channel {ctx.channel.name} is being ignored by the guild admins.")
        elif any([x.id in guild.ignored_roles for x in ctx.author.roles]):
            raise IgnoredRole(f"You have a role that is being ignored.")
        return True

    @commands.Cog.listener()
    async def on_message(self, message: discord.Message):
        if not message.guild:
            return
        try:
            if not await self.bot_check(message):
                return
        except (commands.CheckFailure, commands.CommandError):
            return

        ctx = await self.bot.get_context(message)
        if ctx.valid:
            return
        if message.content.lower().startswith("yourapps prefix?"):
            guild = self.bot.guilds_cache.get(message.guild.id)
            if not guild:
                return
            e = discord.Embed(
                title="This server's prefixes:", description=str("`" + "`, `".join(guild.prefixes or ["ya?"]) + "`")
            )
            return await message.channel.send(embed=e, delete_after=10)
        if message.clean_content.startswith("@" + ctx.me.display_name):
            return await ctx.send(
                f"Hello {message.author.mention}! You can run `@{ctx.me.display_name} <command>`, or say"
                f" `yourapps prefix?` to get this server's prefixes.",
                delete_after=15,
            )

    @custom_commands.group(name="config", alaises=["settings"], invoke_without_command=True)
    @admin_or_permissions(manage_roles=True)
    @commands.bot_has_permissions(embed_links=True)
    @commands.guild_only()
    async def cfg(self, ctx: commands.Context):
        """Shows the config for the current server.

        You need to have `manage roles` to be able to run this command on its own."""
        guild: sql.Guild = self.bot.guilds_cache.get(ctx.guild.id)
        ns = "None set."

        log = self.bot.get_channel(guild.log_channel)
        log = log.mention if log else ns

        archive = self.bot.get_channel(guild.arc_channel)
        archive = archive.mention if archive else ns

        ignored_channels = map(str, [self.bot.get_channel(n) or str(n) + " (deleted)" for n in guild.ignored_channels])
        ignored_roles = map(str, [ctx.guild.get_role(n) or str(n) + " (deleted)" for n in guild.ignored_roles])

        admin_roles = ", ".join([x.mention for x in filter(lambda x: x, map(ctx.guild.get_role, guild.admin_roles))])
        review_roles = ", ".join([x.mention for x in filter(lambda x: x, map(ctx.guild.get_role, guild.review_roles))])
        black_roles = ", ".join(
            [x.mention for x in filter(lambda x: x, map(ctx.guild.get_role, guild.blacklist_roles))]
        )
        # this is not a racist remark so shut up

        apps = len(guild.apps)
        applied = len(guild.applied)

        prefixes = "`" + "`, `".join(guild.prefixes or ["ya?", "@YourApps "]) + "`"

        e = discord.Embed(
            title="Guild Configuration:",
            description=f"You have been using YourApps since: {hz.naturaldate(ctx.me.joined_at)} "
            f"({hz.naturaltime(ctx.me.joined_at)})",
            color=discord.Color.gold(),
        )
        e.add_field(name="Prefixes", value=prefixes[:1024] or ns)
        e.add_field(name="Log | archive", value=f"{log} | {archive}")

        e.add_field(name="Ignored Channels", value=shrt(", ".join(ignored_channels), 1024) or ns)
        e.add_field(name="Ignored Roles", value=shrt(", ".join(ignored_roles), 1024) or ns)

        e.add_field(name="Admin Role", value=admin_roles or ns)
        e.add_field(name="Reviewer Role", value=review_roles or ns)
        e.add_field(name="Blacklisted Role", value=black_roles or ns)

        e.add_field(name="Applications | submitted", value=f"{apps} | {applied}")

        e.add_field(name="Premium Status:", value=f"Activated" if guild.premium else "Not Premium.")

        e.fields.sort(key=lambda f: f.name)

        e.set_footer(text=f"See '{ctx.prefix}help' config to see how to change these values.")

        return await ctx.send(embed=e)

    @cfg.group(name="prefix", aliases=["prefixes"], invoke_without_command=True)
    @admin_or_permissions(manage_channels=True)
    @commands.guild_only()
    async def config_prefix(self, ctx: commands.Context):
        """Configures your prefix(es)!"""
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        e = discord.Embed(
            title="Your prefixes (on top of @me):", description=str(", ".join(guild.prefixes or ["ya?"]))[:2048]
        )
        if "/" in e.description:
            e.set_footer(
                text="\N{warning sign} The slash (/) prefix will not work by the end of april 2021. "
                "Please change it."
            )
        return await ctx.send(embed=e)

    # @cfg.command(name="location", aliases=['log'])
    # @review_or_permissions(manage_channels=True)
    # @commands.guild_only()
    # @commands.max_concurrency(1, commands.BucketType.guild)
    # async def config_location(self, ctx: commands.Context, *, where: typing.Union[discord.TextChannel, str] = "auto"):
    #     """Sets where users will apply in.
    #
    #     Parameter "where" can be a #text-channel (applications will be filled in in the specified channel),
    #     "DM" (applications will be filled out in DMs, default), or "auto" (currently just defaults to DM)."""
    #     if isinstance(where, discord.TextChannel):
    #         if not await Confirm(
    #             "By default, YourApps conducts applications in direct messages. This is not only for privacy, but to"
    #             " prevent automoderation bots from punishing users for answering questions (e.g. being muted for "
    #             "supplying an invite link to a message).\nYou should only continue if you know what you're doing and"
    #             " are aware of the roadblocks that may come with this."
    #         ).result(ctx):
    #             return
    #         loc = where.id
    #     elif isinstance(where, str):
    #         where = where.lower().strip()
    #         if where == "auto":
    #             loc = None

    @config_prefix.command(name="add")
    @admin_or_permissions(manage_channels=True)
    @commands.guild_only()
    async def config_prefix_add(self, ctx: commands.Context, *prefixes: str):
        """Configures your prefix(es)!"""
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        update = guild.update()
        invalidate = await self.bot.command_prefix(self.bot, ctx.message)
        for prefix in prefixes:
            if prefix.startswith("/") and ctx.message.created_at > datetime(2021, 4, 3):
                return await ctx.send(
                    "\N{warning sign} The slash prefix is unsupported due to discord's slash commands."
                )
            if prefix in invalidate:
                continue
            else:
                guild.prefixes.append(prefix)
                update.update(prefixes=guild.prefixes)
        await update.apply()
        await ctx.message.add_reaction("\N{white heavy check mark}")
        return await self.config_prefix(ctx)

    @config_prefix.command(name="remove")
    @admin_or_permissions(manage_channels=True)
    @commands.guild_only()
    async def config_prefix_remove(self, ctx: commands.Context, *prefixes: str):
        """Configures your prefix(es)!"""
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        update = guild.update()
        for prefix in prefixes:
            if prefix not in guild.prefixes:
                continue
            else:
                guild.prefixes.remove(prefix)
                update.update(prefixes=guild.prefixes)
        await update.apply()
        await ctx.message.add_reaction("\N{white heavy check mark}")
        return await self.config_prefix(ctx)

    @cfg.command(name="log", aliases=["notifs", "logging"])
    @admin_or_permissions(manage_channels=True)
    @commands.guild_only()
    async def config_log(self, ctx: commands.Context, *, new_channel: discord.TextChannel = None):
        """Sets the server's logging channel.

        Events that are logged to the "log" channel are:
        • apps are opened
        • apps are closed
        • apps are deleted
        • apps are edited
        • apps are created
        • a new application is submitted
        • an application is approved
        • an application is denied
        • any configuration changes

        Setting [new_channel] to nothing (not providing a channel) will remove the current one."""
        if new_channel:
            p = new_channel.permissions_for(ctx.me)
            if not all([p.read_messages, p.send_messages, p.attach_files, p.embed_links]):
                return await ctx.send(
                    f"{Emojis.p(ctx, 'x')} I am missing one or more of the following permissions in"
                    f" {new_channel.mention}: **read messages**, **send messages**, **attach files** "
                    f"and/or **embed links**. Please give me those permissions and try again."
                )
            else:
                if not await Confirm(f"Are you sure you want to set your log channel to {new_channel.mention}?").result(
                    ctx
                ):
                    return await ctx.send(f"Cancelled. No changes made.")
                new_channel = new_channel.id
        else:
            new_channel = None
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        async with self.bot.gino_db.transaction() as tx:
            await guild.update(log_channel=new_channel).apply()
        return await ctx.send(
            f"{Emojis.p(ctx, 'y')} Set your log channel to <#{new_channel}>"
            if new_channel
            else f"{Emojis.p(ctx, 'y')} Removed your logging channel."
        )

    @cfg.command(name="arc", aliases=["archive", "arch"])
    @admin_or_permissions(manage_channels=True)
    @commands.guild_only()
    async def config_archive(self, ctx: commands.Context, *, new_channel: discord.TextChannel = None):
        """Sets the server's archive channel.

        the archive channel is where all deleted, denied and approved applications' question(s) (and answers) are sent.

        Setting [new_channel] to nothing (not providing a channel) will remove the current one."""
        if new_channel:
            p = new_channel.permissions_for(ctx.me)
            if not all([p.read_messages, p.send_messages, p.attach_files, p.embed_links]):
                return await ctx.send(
                    f"{Emojis.p(ctx, 'x')} I am missing one or more of the following permissions in"
                    f" {new_channel.mention}: **read messages**, **send messages**, **attach files** "
                    f"and/or **embed links**. Please give me those permissions and try again."
                )
            else:
                if not await Confirm(
                    f"Are you sure you want to set your archive channel to {new_channel.mention}?"
                ).result(ctx):
                    return await ctx.send(f"Cancelled. No changes made.")
                new_channel = new_channel.id
        else:
            new_channel = None
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        async with self.bot.gino_db.transaction() as tx:
            await guild.update(arc_channel=new_channel).apply()
        return await ctx.send(
            f"{Emojis.p(ctx, 'y')} Set your archive channel to <#{new_channel}>."
            if new_channel
            else f"{Emojis.p(ctx, 'y')} Removed your archive channel."
        )

    @cfg.command(name="admin", aliases=["adminrole", "ar", "a"])
    @admin_or_permissions(manage_roles=True)
    @commands.guild_only()
    async def config_admin_role(self, ctx: commands.Context, *, role: discord.Role):
        """Sets the server's bot administrator roles. If you supply a role already an admin role, it'll remove it.

        The admin role can control every aspect of the bot, regardless of permissions.
        """
        if role:
            if role.managed:
                return await ctx.send(
                    f"{Emojis.p(ctx, 'x')} Unable to assign the admin role to a role managed by"
                    f" an integration (discord limitation)."
                )
            else:
                if not await Confirm(
                    f"Are you sure you want to give the role {role.mention} full control of the bot?"
                ).result(ctx):
                    return await ctx.send(f"Cancelled command. No changes have been made.")
                else:
                    role = role.id

        guild = self.bot.guilds_cache.get(ctx.guild.id)
        async with self.bot.gino_db.transaction() as tx:
            if role in guild.admin_roles:
                guild.admin_roles.remove(role)
            else:
                guild.admin_roles.append(role)
            await guild.update(admin_roles=guild.admin_roles).apply()
        role = ctx.guild.get_role(role)
        return await ctx.send(
            f"{Emojis.p(ctx, 'y')} Added {role.name} to your admin roles."
            if role.id in guild.admin_roles
            else f"{Emojis.p(ctx, 'y')} Removed {role.name} from your admin roles."
        )

    @cfg.command(name="reviewer", aliases=["reviewrole", "review", "rr", "r"])
    @admin_or_permissions(manage_roles=True)
    @commands.guild_only()
    async def config_review_role(self, ctx: commands.Context, *, role: discord.Role):
        """Sets the server's bot review role. If you don't pass a role, this will remove it.

        The admin role can only review applications, and see who has applied.
        (unless they have the correct permissions to run a command as well).
        """
        if role:
            if role.managed:
                return await ctx.send(
                    f"{Emojis.p(ctx, 'x')} Unable to assign the review role to a role managed by" f" an integration."
                )
            else:
                role = role.id

        guild = self.bot.guilds_cache.get(ctx.guild.id)
        async with self.bot.gino_db.transaction() as tx:
            if role in guild.review_roles:
                guild.review_roles.remove(role)
            else:
                guild.review_roles.append(role)
            await guild.update(review_roles=guild.review_roles).apply()
        role = ctx.guild.get_role(role)
        return await ctx.send(
            f"{Emojis.p(ctx, 'y')} Added {role.name} to your review roles."
            if role.id in guild.review_roles
            else f"{Emojis.p(ctx, 'y')} Removed {role.name} from your review roles."
        )

    @cfg.command(name="blacklist", aliases=["blacklistrole", "br"])
    @admin_or_permissions(manage_roles=True)
    @commands.guild_only()
    async def config_bl_role(self, ctx: commands.Context, *, role: discord.Role):
        """Sets the server's bot blacklist role. If you don't pass a role, this will remove it.

        the blacklist role, when given to users, they will no-longer be allowed to interact with the bot in this server.
        """
        if role:
            if all([x.bot for x in role.members]):
                return await ctx.send(f"{Emojis.p(ctx, 'x')} Unable to assign the blacklist role to a bot role.")
            elif role.managed:
                return await ctx.send(
                    f"{Emojis.p(ctx, 'x')} Unable to assign the blacklist role to a role managed by" f" an integration."
                )
            else:
                role = role.id

        guild = self.bot.guilds_cache.get(ctx.guild.id)
        async with self.bot.gino_db.transaction() as tx:
            if role in guild.blacklist_roles:
                guild.blacklist_roles.remove(role)
            else:
                guild.blacklist_roles.append(role)
            await guild.update(blacklist_roles=guild.blacklist_roles).apply()
        role = ctx.guild.get_role(role)
        return await ctx.send(
            f"{Emojis.p(ctx, 'y')} Added {role.name} to your blacklisted roles."
            if role.id in guild.blacklist_roles
            else f"{Emojis.p(ctx, 'y')} Removed {role.name} from your blacklisted roles."
        )

    @cfg.command(name="ignore", aliases=["i", "ui", "unignore"])
    @admin_or_permissions(manage_channels=True)
    @commands.guild_only()
    async def config_ignored(
        self, ctx: commands.Context, *items: typing.Union[discord.Role, discord.TextChannel, discord.CategoryChannel, int]
    ):
        """Adds or removes ignored channels/roles.
        You can mention roles, text channels, or a category ID (to ignore all channels in that category)"""
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        items = list(items)

        for category in filter(lambda x: isinstance(x, discord.CategoryChannel), items):
            items += category.text_channels
            items.remove(category)

        ignored_channels = guild.ignored_channels
        ignored_roles = guild.ignored_roles

        for item in items:
            if isinstance(item, discord.TextChannel):
                if item.id in ignored_channels:
                    ignored_channels.remove(item.id)
                else:
                    ignored_channels.append(item.id)
            elif isinstance(item, int) and item in ignored_channels:
                ignored_channels.remove(item)
            else:
                if item.id in ignored_roles:
                    ignored_roles.remove(item.id)
                else:
                    ignored_roles.append(item.id)

        async with self.bot.gino_db.transaction() as tx:
            await guild.update(ignored_roles=ignored_roles, ignored_channels=ignored_channels).apply()
        return await ctx.send(f"Now ignoring {len(guild.ignored_roles)} roles & {len(guild.ignored_channels)} channels.")

    @cfg.command(name="new", aliases=["create", "add"])
    @admin_or_permissions(manage_roles=True, manage_guild=True)
    @commands.guild_only()
    async def config_new_app(self, ctx: commands.Context, *, name: str):
        """Creates an application for people to apply for.

        This command is entirely interactive.

        Once you've created an application, it will show up on `ya?pos`.
        You can add and edit things after, via `ya?config edit <name>`."""
        name = name[:64]
        guild = self.bot.guilds_cache[ctx.guild.id]
        if guild.apps.get(name.lower()):
            return await ctx.send(
                f"{Emojis.p(ctx, 'x')} You already have an app named `{name.lower()}`!\n"
                f"Remember that application names are __not__ case sensitive!"
            )

        max_questions = 30 if not guild.premium else 9_223_372_036_854_775_807

        init = discord.Embed(
            title=f'New Application: "{name}"',
            description=f"__Welcome to the interactive setup of {name}!__\n\n"
            f"To start, just send every question you want __in separate messages__."
            f"\nThere are a maximum of **{max_questions} questions**\n"
            f"Each of those questions have a limit of **253 characters** (discord limit)\n"
            f"Each question will time out __after 10 minutes of no response.__",
            color=discord.Color.gold(),
            timestamp=ctx.message.created_at,
            # url=""
        )
        msg = await ctx.send(embed=init)

        entry = {
            "name": name.lower(),
            "id": hash(ctx.message.created_at),
            "created_at": ctx.message.created_at,
            "questions": {},
            "created_by": ctx.author.id,
            "required_roles": [],
            "reward_roles": [],
            "remove_roles": [],
            "open": True,
            "guild": ctx.guild.id,
        }

        e = discord.Embed(
            title=f"[CREATE] Apply for {name}!",
            description="Reply **`finish`** to prematurely finish adding questions!\nNeed to stop? say `cancel`!",
            color=discord.Color.gold(),
        )

        n = 0
        while len(entry["questions"]) < max_questions:
            details = {}
            while len(e.fields) >= 25 or len(e) >= 5000:
                e.remove_field(-1)
                e.set_footer(text="This page has scrolled. You still have all of your questions.")

            if n >= 1:
                await msg.edit(embed=e)
            n += 1
            r = await self.bot.wait_for(
                "message",
                check=lambda m: m.channel == ctx.channel and m.author == ctx.author and m.content,
                timeout=600,
            )
            await r.delete(delay=0.1)
            question = textwrap.shorten(r.clean_content, 253)
            if question.lower().startswith("finish"):
                break
            elif "cancel" in question:
                return await ctx.send(
                    embed=discord.Embed(
                        title="quit.", description="Kept the above embed in case you wanna get the questions."
                    )
                )
            if question.lower().strip() in entry["questions"]:
                continue
            entry["questions"][question] = {"type": 0, "details": details}  # _type,
            e.add_field(name=question, value="<answer goes here>")

        await msg.edit(
            embed=discord.Embed(
                title="Should the bot give the user any roles if they are accepted?",
                description=f"If so, please provide a list of role names, IDs, or mentions, separated by `/`.\n"
                f"If not, just reply `skip`.",
                color=discord.Colour.blue(),
            )
        )
        try:
            reward = await self.bot.wait_for(
                "message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=300
            )
            await reward.delete(delay=0.1)
        except asyncio.TimeoutError:
            entry["reward_roles"] = []
        else:
            resolved = []
            for role in reward.content.split("/"):
                try:
                    role = await commands.RoleConverter().convert(ctx, role)
                    if role == ctx.guild.default_role:
                        continue
                except commands.BadArgument:
                    continue
                else:
                    resolved.append(role)
            entry["reward_roles"] = list(map(lambda ro: ro.id, resolved))

        await msg.edit(
            embed=discord.Embed(
                title="Should the bot remove any roles if the user is accepted?",
                description=f"If so, please provide a list of role names, IDs, or mentions, separated by `/`.\n"
                f"If not, just reply `skip`."
                f"\nRemoved roles can not be a reward role too.",
                color=discord.Color.blue(),
            )
        )
        try:
            reward = await self.bot.wait_for(
                "message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=300
            )
            await reward.delete(delay=0.1)
        except asyncio.TimeoutError:
            entry["remove_roles"] = []
        else:
            resolved = []
            for role in reward.content.split("/"):
                try:
                    role = await commands.RoleConverter().convert(ctx, role)
                    if role == ctx.guild.default_role:
                        continue
                except commands.BadArgument:
                    continue
                else:
                    if role.id in entry["reward_roles"]:
                        continue
                    resolved.append(role)
            entry["remove_roles"] = list(map(lambda ro: ro.id, resolved))

        await msg.edit(
            embed=discord.Embed(
                title="Are members required to have (a) certain role(s)?",
                description=f"If so, please provide a list of role names, IDs, or mentions, separated by `/`.\n"
                f"If not, just reply `skip`."
                f"\nRequired roles can't be reward roles.",
                color=discord.Color.blue(),
            )
        )
        try:
            reward = await self.bot.wait_for(
                "message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=300
            )
            await reward.delete(delay=0.1)
        except asyncio.TimeoutError:
            entry["required_roles"] = []
        else:
            resolved = []
            for role in reward.content.split("/"):
                try:
                    role = await commands.RoleConverter().convert(ctx, role)
                    if role == ctx.guild.default_role:
                        continue
                except commands.BadArgument:
                    continue
                else:
                    if role.id in entry["reward_roles"]:
                        continue
                    resolved.append(role)
            entry["required_roles"] = list(map(lambda ro: ro.id, resolved))

        async with ctx.bot.gino_db.transaction() as tx:
            await sql.App.create(**entry)
            guild.apps[str(entry["id"])] = entry["name"]
            await guild.update(apps=guild.apps).apply()
        return await msg.edit(
            embed=discord.Embed(
                title=f'Created new application "{name}"!',
                description=f"You can apply for this application via `{ctx.prefix}apply {name.lower()}`.\n\n"
                f"[**Vote for yourapps!**](https://top.gg/bot/{ctx.me.id}/vote)",
                color=discord.Color.dark_green(),
            ),
            delete_after=60,
        )

    @cfg.command(name="edit")
    @admin_or_permissions(manage_roles=True)
    @commands.bot_has_permissions(add_reactions=True, embed_links=True, manage_messages=True)
    async def config_edit_app(self, ctx: commands.Context, *, app: App):
        """Edits an application."""
        things = {
            "0\U0000fe0f\U000020e3": "App Name",
            "1\U0000fe0f\U000020e3": "Reward Message",
            "2\U0000fe0f\U000020e3": "Reward Roles",
            "3\U0000fe0f\U000020e3": "Required Roles",
            "4\U0000fe0f\U000020e3": "Remove Roles",
            "5\U0000fe0f\U000020e3": "Add Question(s)",
            "6\U0000fe0f\U000020e3": "Edit Question(s) (\N{cross mark} Not added yet)",
            "7\U0000fe0f\U000020e3": "Remove Question(s)",
            # "\U0001f4be": "Save Changes",
            "\U000023f9": "Exit Menu",
        }

        def update(**kwargs):
            for key, value in kwargs.items():
                if isinstance(value, map.__class__):
                    kwargs[key] = list(value)
            return app.sql.update(**kwargs)

        async def save(change):
            try:
                async with self.bot.gino_db.transaction() as _tx:
                    await change.apply()
            except Exception as e:
                await ctx.send(f"There was an error saving some changes: {e}")
                raise e
            else:
                return

        changes = False
        emojis = list(things.keys())
        index = discord.Embed(
            title=f"Edit App: {app.name}",
            description="\n".join(f"{x}: {y.title()}" for x, y in things.items()),
            color=discord.Color.gold(),
            timestamp=datetime.utcnow(),
        )
        index.set_footer(text="\N{heavy exclamation mark symbol} This menu is not yet complete!")
        msg = await ctx.send(embed=index)
        try:
            while True:
                for emoji in emojis:
                    self.bot.loop.create_task(msg.add_reaction(emoji))
                await msg.edit(embed=index)
                try:
                    r, u = await self.bot.wait_for(
                        "reaction_add",
                        check=lambda reac, user: user == ctx.author and str(reac.emoji) in things.keys(),
                        timeout=600,
                    )
                    await msg.clear_reactions()
                except asyncio.TimeoutError:
                    return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                else:
                    if r.emoji == emojis[0]:  # Name
                        await msg.edit(
                            embed=discord.Embed(
                                title="Edit Name:",
                                description=f"Current: `{app.name}`\n" f"New: ",
                                color=discord.Color.gold(),
                                timestamp=datetime.utcnow(),
                            )
                        )
                        try:
                            new_name = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content,
                                timeout=600,
                            )
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        else:
                            changes = True
                            await new_name.delete(delay=0.01)
                            new_name = new_name.clean_content[:64].lower()
                            await save(app.sql.update(name=new_name))
                            embed = msg.embeds[0]
                            embed.description += f"`{new_name}`"
                            await msg.edit(embed=embed)
                            await asyncio.sleep(5)
                            continue
                    elif r.emoji == emojis[1]:  # reward message
                        await msg.edit(
                            embed=discord.Embed(
                                title="Edit Reward Message:",
                                description=f"Current: "
                                f"{textwrap.shorten(_u.escape_markdown(app.reward_message), 128)}\n"
                                f"New: ",
                                color=discord.Color.gold(),
                            ).add_field(
                                name="Want to know how to use text-templates?",
                                value="[Read about them here](https://gist.github.com/EEKIM10/0f1acdf797e3d54"
                                "f55f9dafaa5bb74e0)",
                            )
                        )
                        try:
                            new_message = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content,
                                timeout=600,
                            )
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        else:
                            changes = True
                            await new_message.delete(delay=0.01)
                            new_message = new_message.clean_content.capitalize()
                            await save(app.sql.update(reward_message=new_message))
                            embed = msg.embeds[0]
                            embed.description += f"`{new_message}`"
                            await msg.edit(embed=embed)
                            await asyncio.sleep(5)
                            continue
                    elif r.emoji == emojis[2]:  # Reward Roles
                        await msg.edit(
                            embed=discord.Embed(
                                title="Edit reward roles",
                                description=f"Please provide a list of role names, IDs, or mentions, separated by `/`.",
                                color=discord.Colour.gold(),
                            )
                        )
                        try:
                            reward = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content,
                                timeout=300,
                            )
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        else:
                            resolved = []
                            for role in reward.content.split("/"):
                                try:
                                    role = int(role)
                                except ValueError:
                                    role = str(role)
                                try:
                                    role = await commands.RoleConverter().convert(ctx, str(role))
                                    if role == ctx.guild.default_role:
                                        continue
                                except commands.BadArgument:
                                    continue
                                else:
                                    resolved.append(role)
                            if not resolved:
                                if reward.content.lower() == "skip":
                                    continue
                                await msg.edit(
                                    embed=discord.Embed(title="No valid roles found!", color=discord.Color.red())
                                )
                                await asyncio.sleep(3)
                                continue
                            else:
                                await save(app.sql.update(reward_roles=list(map(lambda ro: ro.id, resolved))))
                    elif r.emoji == emojis[3]:  # Reward Roles
                        await msg.edit(
                            embed=discord.Embed(
                                title="Edit required roles",
                                description=f"Please provide a list of role names, IDs, or mentions, separated by `/`.",
                                color=discord.Colour.gold(),
                            )
                        )
                        try:
                            reward = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content,
                                timeout=300,
                            )
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        else:
                            resolved = []
                            for role in reward.content.split("/"):
                                try:
                                    role = int(role)
                                except ValueError:
                                    role = str(role)
                                try:
                                    role = await commands.RoleConverter().convert(ctx, str(role))
                                    if role == ctx.guild.default_role:
                                        continue
                                except commands.BadArgument:
                                    continue
                                else:
                                    resolved.append(role)
                            if not resolved:
                                if reward.content.lower() == "skip":
                                    continue
                                await msg.edit(
                                    embed=discord.Embed(title="No valid roles found!", color=discord.Color.red())
                                )
                                await asyncio.sleep(3)
                                continue
                            else:
                                await save(update(required_roles=list(map(lambda ro: ro.id, resolved))))
                    elif r.emoji == emojis[4]:  # Remove Roles
                        await msg.edit(
                            embed=discord.Embed(
                                title="Edit remove roles",
                                description=f"Please provide a list of role names, IDs, or mentions, separated by `/`.",
                                color=discord.Colour.gold(),
                            )
                        )
                        try:
                            reward = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content,
                                timeout=300,
                            )
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        else:
                            resolved = []
                            for role in reward.content.split("/"):
                                try:
                                    role = int(role)
                                except ValueError:
                                    role = str(role)
                                try:
                                    role = await commands.RoleConverter().convert(ctx, str(role))
                                    if role == ctx.guild.default_role:
                                        continue
                                except commands.BadArgument:
                                    continue
                                else:
                                    resolved.append(role)
                            if not resolved:
                                if reward.content.lower() == "skip":
                                    continue
                                await msg.edit(
                                    embed=discord.Embed(title="No valid roles found!", color=discord.Color.red())
                                )
                                await asyncio.sleep(3)
                                continue
                            else:
                                await save(update(remove_roles=list(map(lambda ro: ro.id, resolved))))
                    elif r.emoji == emojis[5]:
                        if len(app.questions) == 50 and not self.bot.guilds_cache[ctx.guild.id].premium:
                            await msg.edit(
                                embed=discord.Embed(
                                    title="You can't add anymore questions!",
                                    description=f"The limit for non-premium users is 50 questions per app.",
                                )
                            )
                            await asyncio.sleep(3)
                            continue
                        await msg.edit(
                            embed=discord.Embed(
                                title="Where do you want to add your question?",
                                description=f"e.g, to add it to the end, say `{len(app.questions)}`. "
                                f"To add it after question 2, say `2`."
                                f"To make it the first question, say `0`.",
                                color=discord.Color.gold(),
                            )
                        )
                        try:
                            num = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author
                                and m.channel == ctx.channel
                                and m.content.isdigit()
                                and m.content,
                                timeout=300,
                            )
                            await num.delete()
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        try:
                            inx = int(num.content)
                        except ValueError:
                            await msg.edit(
                                embed=discord.Embed(title="You didn't provide only numbers!", color=discord.Color.red())
                            )
                            await asyncio.sleep(3)
                            continue
                        questions = list(app.questions)
                        await msg.edit(
                            embed=discord.Embed(title="What should your new question be?", color=discord.Color.gold())
                        )
                        try:
                            q = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content,
                                timeout=300,
                            )
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        try:
                            questions.insert(inx, q.clean_content[:250])
                        except IndexError:
                            await msg.edit(embed=discord.Embed(title="You didn't provide a valid position!"))
                            await asyncio.sleep(3)
                            continue
                        questions = {x: {"details": {}, "type": 0} for x in questions}
                        await save(update(questions=questions))
                        changes = True
                        continue
                    elif r.emoji == emojis[6]:  # edit
                        pass
                    elif r.emoji == emojis[7]:  # remove
                        if len(app.questions) == 1:
                            await msg.edit(
                                embed=discord.Embed(
                                    title="You only have 1 question!",
                                    description=f"If you tried to remove it, the app'd have no questions!",
                                )
                            )
                            await asyncio.sleep(3)
                            continue
                        await msg.edit(
                            embed=discord.Embed(
                                title="Which question do you want to remove?",
                                description=(
                                    "\n".join(f"**{x}**. {list(y)[0]}" for x, y in enumerate(app.questions.items()))
                                )[:2048],
                                color=discord.Color.gold(),
                            )
                        )
                        try:
                            num = await self.bot.wait_for(
                                "message",
                                check=lambda m: m.author == ctx.author
                                and m.channel == ctx.channel
                                and m.content.isdigit()
                                and m.content,
                                timeout=300,
                            )
                            await num.delete()
                        except asyncio.TimeoutError:
                            return await msg.edit(content=f"{ctx.author.mention} This menu timed out!", embed=None)
                        try:
                            inx = int(num.content)
                        except ValueError:
                            await msg.edit(
                                embed=discord.Embed(title="You didn't provide only numbers!", color=discord.Color.red())
                            )
                            await asyncio.sleep(3)
                            continue
                        try:
                            questions = app.questions
                            del questions[list(questions)[inx]]
                        except (KeyError, IndexError):
                            await msg.edit(embed=discord.Embed(title="Error removing the question."))
                            await asyncio.sleep(3)
                            continue
                        await save(update(questions=questions))
                        changes = True
                        continue
                    # elif r.emoji == emojis[-2]:  # save
                    #     async with self.bot.db.transaction() as tx:
                    #         if changes:
                    #             await update.apply()
                    #             update = app.sql.update()
                    #             changes = False
                    #     await msg.edit(
                    #         embed=discord.Embed(
                    #             title="All changes saved!",
                    #             color=discord.Color.green()
                    #         )
                    #     )
                    #     await asyncio.sleep(5)
                    #     continue
                    elif r.emoji == emojis[-1]:  # exit
                        await ctx.message.delete(delay=0.1)
                        await msg.delete(delay=0.1)
                        return
        except discord.NotFound:
            return

    @cfg.command(name="info", aliases=["about", "desc", "describe"])
    @admin_or_permissions(manage_roles=True, manage_guild=True)
    @commands.guild_only()
    async def config_info(self, ctx: commands.Context, *, app: App):
        """Shows you information on an application.

        App can be the App ID or name."""
        e = discord.Embed(
            title=f"Information on {app.name}:",
            description=f"**Name:** {app.name}\n"
            f"**ID:** `{app.id}`\n"
            f"**Created:** {hz.naturaldate(app.created_at)} ({hz.naturaltime(app.created_at)})\n"
            f"**Questions:** {len(app.questions)}\n"
            f"**Open?** {Emojis.b(ctx, app.open)}\n",
            color=discord.Color.green() if app.open else discord.Color.red(),
        )
        e.add_field(
            name="Required roles:",
            value=textwrap.shorten(", ".join(map(str, app.required_roles)), 1024, placeholder="...") or "\u200b",
        )
        e.add_field(
            name="Reward roles:",
            value=textwrap.shorten(", ".join(map(str, app.reward_roles)), 1024, placeholder="...") or "\u200b",
        )
        e.add_field(
            name="Remove roles:",
            value=textwrap.shorten(", ".join(map(str, app.remove_roles)), 1024, placeholder="...") or "\u200b",
        )
        e.add_field(name="Reward Message:", value=textwrap.shorten(app.reward_message, 1024, placeholder="..."))
        e.add_field(name="Cooldown:", value=f"{hz.naturaldelta(app.sql.cooldown)}")
        return await ctx.send(embed=e)

    @cfg.command(name="reset")
    @admin_or_permissions(administrator=True)
    @commands.guild_only()
    async def config_reset(self, ctx, quick: bool = False):
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        fast = all([quick, guild.premium])
        is_premium = guild.premium
        if not fast:
            for i in range(5):
                really = " ".join(["*really*"] * i) + " "
                warning = [
                    "This will delete __all data__ for this guild!",
                    "That includes applications and submitted apps!",
                    "And all other settings!",
                    "**And can not be undone!!!**",
                ]
                c = Confirm("Are you {}sure you want to do this?\n\n{}".format(really, "\n".join(warning[:i])))
                if not await c.result(ctx):
                    await ctx.message.delete(delay=5)
                    return await ctx.send(f"{Emojis.p(ctx, 'x')} Cancelled reset.", delete_after=5)
                else:
                    continue
        msg = await ctx.send(f"Deleting your data...")
        async with self.bot.gino_db.transaction() as tx:
            guild = self.bot.guilds_cache.get(ctx.guild.id)
            if guild.log_channel:
                ch = self.bot.get_channel(guild.log_channel)
                try:
                    await ch.send(f"**{ctx.author}** (`{ctx.author.id}`) has reset the server's data.")
                except discord.HTTPException:
                    pass
            x = await sql.App.delete.where(sql.App.guild == ctx.guild.id).gino.status()

            await guild.delete()
            await sql.Guild.create(id=ctx.guild.id, premium=is_premium)
        await ctx.message.delete(delay=5)
        return await msg.edit(content=f"{Emojis.p(ctx, 'y')} Deleted your server's data!", delete_after=10)

    @cfg.command(name="clear")
    @review_or_permissions(manage_roles=True)
    @commands.max_concurrency(1, commands.BucketType.guild)
    @commands.guild_only()
    async def config_clear_app(self, ctx: commands.Context, *, app: App):
        """Clears all submitted applications for a position. This is irreversible!"""
        to_dm = set()
        guild: sql.Guild = self.bot.guilds_cache.get(ctx.guild.id)
        for author_id, app_id, _ in guild.applied:
            if app_id != app.id:
                continue
            to_dm.add(author_id)

        if not to_dm:
            return await ctx.send(
                "It does not appear that anyone has applied for that position.\n"
                "If this is incorrect, please contact a developer in my support server "
                "(ya?invite)."
            )
        else:
            message = await ctx.send(
                "What reason should I send to people who's submission will be deleted " "as to why such is happening?"
            )
            try:
                x = await self.bot.wait_for(
                    "message", check=lambda y: y.author == ctx.author and y.channel == ctx.channel
                )
                await x.delete(delay=0.1)
                reason = x.content
            except asyncio.TimeoutError:
                return await message.delete(delay=0.1)
            else:
                await message.edit(
                    content=f"I have successfully cleared {len(to_dm)} submissions for {app.name!r}.\n"
                    f"I am now notifying those submission authors that their application was cleared."
                    f"Until that is complete, you will be unable to re-run this command."
                )
                for user_id in to_dm:
                    user = self.bot.get_user(user_id)
                    if not user:
                        continue
                    try:
                        await user.send(
                            embed=discord.Embed(
                                title=f"Your application for {app.name} in {ctx.guild} has been cleared!",
                                description=f"Reason: {reason}",
                                color=discord.Color.red(),
                            ).set_footer(text="Your application was NOT reviewed.")
                        )
                    except discord.HTTPException:
                        pass
                    finally:
                        await asyncio.sleep(1)

    @cfg.group(name="cooldown", invoke_without_command=True)
    @admin_or_permissions(administrator=True)
    @commands.guild_only()
    async def config_cooldown(self, ctx: commands.Context, app: App, *, cooldown: float):
        """Sets a cooldown for an application.

        While this cooldown is active, the user who applied will not be able to apply again until it runs out.
        The cooldown is per-person, and if they hit a cooldown, there's no way to reset it for them (yet)

        <cooldown> is a number, in minutes. e.g: 60 = 1 hour
        Making cooldown `0` will remove it.

        <app> is the app name or ID. If the name has spaces, wrap them in "double quotes".

        example: ya?config cooldown "staff app" 3 -> cooldown of 3 minutes on staff app.
        """
        if not self.bot.guilds_cache[ctx.guild.id].premium:
            return await ctx.send(
                "\N{cross mark} Application Cooldowns are a premium feature."
                f" Please consider buying premium via `{ctx.prefix}donate`. Its only £3!"
            )
        if cooldown == 0.0:
            await app.sql.update(cooldown=0, cooldowns={}).apply()
            return await ctx.send(f"\N{white heavy check mark} Removed the cooldown on {app.name}.")
        elif cooldown < 0:
            return await ctx.send(f"\N{cross mark} Cooldowns can't be negative.")
        else:
            oc = app.sql.cooldowns
            for uid, ts in oc.items():
                dt = datetime.fromisoformat(ts)
                user = int(uid)
                if (dt - datetime.utcnow()).total_seconds() >= cooldown:
                    oc[uid] = datetime.utcnow() + timedelta(seconds=cooldown)
            await app.sql.update(cooldown=round(60 * cooldown), cooldowns=oc).apply()
            return await ctx.send(
                f"\N{white heavy check mark} People now have to wait {round(cooldown)} minutes"
                f" before applying again for {app.name}."
            )

    @cfg.group(name="premium", invoke_without_command=True)
    async def cfg_premium(self, ctx):
        premium = self.bot.guilds_cache[ctx.guild.id].premium
        return await ctx.send(
            f"This server has premium activated." if premium else f"This server does not have premium activated."
        )

    @cfg_premium.command(name="genkey", aliases=["new", "newkey", "key"])
    @commands.is_owner()
    async def cfg_prem_key_gen(
        self, ctx, *, to: typing.Union[discord.User, discord.Member, discord.TextChannel] = None
    ):
        to = to or ctx.author
        token = "-".join(secrets.token_hex(8) for i in range(5))
        await ctx.message.delete(delay=10)
        # data[token] = {"claimed": False, "claimed by": None, "claimed for": None, "claimed at": None}
        await sql.PremiumKey.create(id=token)
        try:
            await ctx.message.add_reaction("\N{INBOX TRAY}")
        except discord.HTTPException:
            pass
        return await to.send(
            f"**Yourapps Premium** one-time token: `" + token + "`\nUse `ya?config premium redeem <that key>`"
            " to use it."
        )

    @cfg_premium.command(name="getkey")
    @commands.is_owner()
    async def cfg_prem_key_get(self, ctx: commands.Context, *, key: str):
        value = await sql.PremiumKey.get(key)
        if not value:
            return await ctx.send(f":x: No valid tokens.")

        claimed_at = datetime.fromisoformat(value.claimed_at or datetime.min)

        e = discord.Embed(
            title=discord.utils.escape_markdown(
                f"Token: {value.id.split('-')[0][:4] + '*' * 4}-{'*' * 8}-{'*' * 8}-{'*' * 8}"
            ),
            description=f"**Claimed?:** {Emojis.b(ctx, value.claimed)}\n"
            f"**Claimed By:** {self.bot.get_user(value.claimed_by)} (`{value.claimed_by}`)\n"
            f"**Claimed for:** {self.bot.get_guild(value.claimed_for)} (`{value.claimed_for}`)\n"
            f"**Claimed @:** {hz.naturaltime(claimed_at)} ({hz.naturaldate(claimed_at)})",
            timestamp=claimed_at,
            color=discord.Color.blue(),
        )
        return await ctx.send(embed=e)

    @cfg_premium.command(name="set")
    @commands.is_owner()
    async def cfg_prem_key_get(self, ctx: commands.Context, server_id: int, *, value: bool):
        guild = self.bot.guilds_cache[ctx.guild.id]
        await guild.update(premium=value)
        return await ctx.send(f"{guild} {value}")

    @cfg_premium.command(name="list")
    @commands.is_owner()
    async def cfg_prem_lst(self, ctx: commands.Context, *, claimed: bool = True):
        paginator = commands.Paginator()
        for values in await sql.PremiumKey.query.where(sql.PremiumKey.claimed is claimed).gino.all():
            paginator.add_line(values.id)
        for page in paginator.pages:
            await ctx.send(page, delete_after=60)

    @cfg_premium.command(name="redeem", aliases=["claim"])
    @commands.guild_only()
    @commands.bot_has_permissions(manage_messages=True)
    async def cfg_prem_redeem(self, ctx: commands.Context, *, key: str):
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        if guild.premium:
            await ctx.message.delete()
            return await ctx.send(f"{Emojis.p(ctx, 'x')} This server is already premium!")
        value = await sql.PremiumKey.get(key)
        if value is None:
            return await ctx.send(f"{Emojis.p(ctx, 'x')} That is not a valid key!")

        if value.claimed:
            return await ctx.send(f"{Emojis.p(ctx, 'x')} That key has already been redeemed!")
        else:

            async with self.bot.gino_db.transaction():
                if await Confirm(
                    f"Are you sure you want to use this **one-time key** on this server?\n"
                    f"Remember, once it has been redeemed, __it can not be transferred!__"
                ).result(ctx):
                    await guild.update(premium=True).apply()
                    await value.update(
                        claimed=True,
                        claimed_by=ctx.author.id,
                        claimed_for=ctx.guild.id,
                    ).apply()
                else:
                    return await ctx.message.delete()
            return await ctx.send(f"{Emojis.p(ctx, 'y')} Redeemed key, this server is now premium :tada:!")

    @cfg.command(name="open", aliases=["close"])
    @review_or_permissions(manage_roles=True, manage_guild=True)
    @commands.bot_has_permissions(embed_links=True)
    async def config_open(self, ctx: commands.Context, *, app: App = None):
        """Opens or closes an app, depending on whether it is open or closed already.

        Remember that closed apps can't be applied for."""
        app = app or await App.convert(ctx, "")
        open_or_closed = not app.open

        await app.sql.update(open=open_or_closed).apply()
        return await ctx.send(f"{Emojis.p(ctx, 'y')} {'Opened' if open_or_closed else 'Closed'} {app}.")

    @cfg.command(name="delete", aliases=["remove"])
    @admin_or_permissions(manage_roles=True, manage_guild=True)
    @commands.bot_has_permissions(embed_links=True)
    async def config_del(self, ctx: commands.Context, *, app: App = None):
        """Completely removes an application, but keeps the submissions."""
        app = app or await App.convert(ctx, "")
        guild = self.bot.guilds_cache[ctx.guild.id]

        if not await Confirm(
            f"Are you sure you want to delete {app}? **This action is completely irreversible!**"
        ).result(ctx):
            return await ctx.send(f"Ok. I haven't deleted anything.")

        for user_id, position_id, app_data in guild.applied:
            if position_id == app.id:
                x = Confirm(
                    "There are one or more applications waiting for review under this position.\n"
                    "If you delete it, you may not be able to properly review these applications in the future.\n\n"
                    ""
                    "Do you want to delete these applications before deleting the position?"
                )
                if await x.result(ctx):
                    entry = discord.utils.find(
                        lambda y: y[0] == user_id and y[1] == position_id and y[2] == app_data,
                        guild.applied
                    )
                    if entry:
                        guild.applied.remove(entry)

        app_id = app.id
        try:
            del guild.apps[app_id]
        except KeyError:
            pass  # ghost app
        await app.sql.delete()
        await guild.update(apps=guild.apps).apply()
        return await ctx.send(f"\U0001f5d1 {app}.")

    @cfg.command(name="import")
    @commands.bot_has_permissions(manage_messages=True)
    @admin_or_permissions(manage_roles=True, manage_guild=True)
    async def import_data(self, ctx: commands.Context, *, raw_data: str = None):
        """Imports an application from another server.

        This requires a `.yapp` file."""
        if not checks.has_voted(ctx):
            return await ctx.send(f"You need to vote to use this feature! Run `ya?vote`.")
        if raw_data is None:
            if not ctx.message.attachments:
                return await ctx.send(f"You need to upload the attachment I should import!")

            file: discord.Attachment = ctx.message.attachments[0]
            if not file.filename.lower().endswith(".yapp"):
                return await ctx.send(f"That is not a valid application file.")
            else:
                text = (await file.read()).decode("utf-16", "replace")
        else:
            text = raw_data
        try:
            loaded = json.loads(text)
        except json.JSONDecodeError:
            return await ctx.send(f"Invalid application file. It may be damaged or corrupted.")
        if not loaded or not loaded.get("name") or not loaded.get("questions"):
            return await ctx.send(f"Invalid application file. It may be damaged or corrupted.")
        else:
            entry = {
                "name": loaded["name"],
                "id": hash(ctx.message.created_at),
                "created_at": ctx.message.created_at,
                "questions": {x: {"details": {}, "type": 0} for x in loaded["questions"]},
                "created_by": ctx.author.id,
                "required_roles": loaded["required_roles"],
                "reward_roles": loaded["reward_roles"],
                "remove_roles": loaded["remove_roles"],
                "open": True if loaded["open"] == "true" else False,
                "guild": ctx.guild.id,
            }
            if loaded.get("cert_key"):
                tmp = await sql.Template.query.where(sql.Template.cert_key == loaded["cert_key"]).gino.first()
                if not tmp:
                    if not await Confirm(
                        f"\N{warning sign} The application you're trying to import does not "
                        f"match what we have on database. "
                        f"The handshake failed, meaning someone has modified the data that was in the template entry,"
                        f"or tried to make it look like a template official app.\n"
                        f"Do you still want to import it?"
                    ).result(ctx):
                        return await ctx.send(
                            f"Ok. If you think someone is trying to do something malicious with "
                            f"our template/import system, please let the developers know via "
                            f"{ctx.prefix}invite."
                        )
            else:
                if not await Confirm(
                    f"\N{warning sign} You're trying to import a third-party application. "
                    f"Functionality is not guaranteed. "
                    f"Are you sure you want to continue?"
                ).result(ctx):
                    return await ctx.send(
                        f"Ok, cancelled importing. Please remember you can get a list of "
                        f"hand-approved applications from the {ctx.prefix}templates command."
                    )

                async with ctx.bot.gino_db.transaction() as tx:
                    guild = self.bot.guilds_cache[ctx.guild.id]
                    await sql.App.create(**entry)
                    guild.apps[str(entry["id"])] = entry["name"]
                    await guild.update(apps=guild.apps).apply()
                return await ctx.send(f"\N{white heavy check mark} Successfully imported app {entry['name']}.")

    @cfg.command(name="export")
    @commands.bot_has_permissions(manage_messages=True)
    @admin_or_permissions(manage_roles=True, manage_guild=True)
    async def export_data(self, ctx: commands.Context, *, app: App):
        """Exports a certain application into a file so that you can import it elsewhere."""

        def i(r):
            return r.id

        tmp = await sql.Template.query.where(sql.Template.source_id == app.id).gino.first()
        if tmp:
            cert = tmp.cert_key
        else:
            cert = None

        entry = {
            "name": app.name.lower(),
            "id": app.id,
            "created_at": str(app.created_at),
            "questions": app.questions,
            "created_by": app.sql.created_by,
            "required_roles": list(map(i, app.required_roles)),
            "reward_roles": list(map(i, app.reward_roles)),
            "remove_roles": list(map(i, app.remove_roles)),
            "open": app.open,
            "guild": ctx.guild.id,
            "cert_key": cert,
        }
        fmt = json.dumps(entry)

        buf = io.BytesIO(fmt.encode("utf-16"))
        return await ctx.send(file=discord.File(buf, "export.yapp"))

    @cfg.command(name="clearsubs", hidden=True)
    @admin_or_permissions(administrator=True)
    async def clear_submissions(self, ctx: commands.Context, *, app: App):
        """Clears all submissions for an application. Useful for cleanup before deleting an app."""
        guild = self.bot.guilds_cache.get(ctx.guild.id)
        if await Confirm(
            f"Are you sure you want to delete all submissions for {app}? **This can not be undone, and I mean this!**"
        ).result(ctx):
            new = []
            for e in guild.applied:
                if e[1] != app.id:
                    new.append(e)
            async with self.bot.gino_db.transaction():
                await guild.update(applied=new).apply()
                guild.applied = new
                await ctx.message.add_reaction("\N{white heavy check mark}")

    @custom_commands.command(name="review", usage="[member] [app]")
    @review_or_permissions(manage_roles=True, manage_messages=True, add_reactions=True)
    @commands.bot_has_permissions(
        send_messages=True,
        embed_links=True,
        attach_files=True,
        manage_messages=True,
        use_external_emojis=True,
        manage_roles=True,
    )
    @commands.max_concurrency(1, commands.BucketType.channel)
    async def review_user(self, ctx: commands.Context, member: discord.Member = None, *, app: App = None, **kw):
        """Reviews users."""
        if kw.get("recurse", 0) >= 6:
            return await ctx.send(hex(400))
        else:
            if kw.get("recurse") is None:
                kw["recurse"] = 1
            else:
                kw["recurse"] += 1

        def get_emoji(en):
            return (
                discord.utils.get(self.bot.emojis, name=str(en), guild=self.bot.get_guild(706271127542038608))
                or f'<missing emoji "{en}">'
            )

        guild = self.bot.guilds_cache.get(ctx.guild.id)
        if len(guild.applied) == 0:
            return await ctx.send(
                embed=discord.Embed(
                    title=f"{Emojis.p(ctx, 'x')} Nobody has applied in this server!", color=discord.Color.red()
                )
            )
        if not member:
            emojis = [(str(n) + f"\N{variation selector-16}\N{combining enclosing keycap}") for n in range(10)]
            emojis.append(f"\N{keycap ten}")
            emojis += [str(get_emoji(x)) for x in range(11, 21)]
            options = {}
            for n, sub in enumerate(guild.applied[:20]):
                try:
                    name = (await App.from_id(ctx, sub[1])).name.lower()
                except (discord.HTTPException, commands.BadArgument):
                    name = sub[-1].get("cached_name", "deleted app")
                options[n] = f"<@{sub[0]}>:{name}"
            e = discord.Embed(
                title="Who should we review?",
                description=("\n".join(f"{emojis[n]}: {d}" for n, d in options.items()))[:2048],
                color=discord.Color.orange(),
            )
            msg = await ctx.send(embed=e)
            used_emojis = [em for em in emojis if em in e.description]
            used_dict = {str(n): used_emojis[n] for n in range(len(used_emojis))}
            used_dict["cancel"] = "\U000023f9"
            for emoji in used_dict.values():
                self.bot.loop.create_task(msg.add_reaction(emoji))

            try:
                emoji = await wf_msg_or_r(ctx, message=msg, **used_dict)
                if emoji == used_dict["cancel"]:
                    return await msg.delete(delay=0.1)
            except asyncio.TimeoutError:
                return await msg.delete(delay=0.1)
            else:
                await msg.delete()
                e = options[used_emojis.index(str(emoji))]
                user_id = int(re.search(r"<@[!]?(?P<id>[0-9]*)>", e).group("id"))
                try:
                    member = ctx.guild.get_member(user_id) or await ctx.guild.fetch_member(user_id)
                except discord.NotFound:
                    for sub in guild.applied:
                        if sub[0] == user_id:
                            guild.applied.remove(sub)
                    async with self.bot.gino_db.transaction() as tx:
                        await guild.update(applied=guild.applied).apply()
                    return await ctx.send(
                        f"The member that applied for this app has left, so I have deleted their app."
                    )
                name = e.split(":")[-1]
                try:
                    app = await App.from_name(ctx, name, cross_guild=False)
                except commands.BadArgument:
                    for sub in guild.applied:
                        if sub[0] == user_id and sub[2].get("name", "deleted app") == name:
                            data = sub[2]
                            break
                    else:
                        return await ctx.send(
                            f"\N{heavy exclamation mark symbol} The app that this user applied"
                            f" from could not be recovered (since it has been deleted)."
                        )
                    # construct a partial app
                    app = PartialApp(
                        ctx,
                        0,
                        name,
                        data.get("cached_questions", {"unable to get questions": {"message": "fatal error"}}),
                        datetime.utcnow(),
                        [],
                        [],
                        [],
                        f"Your app was approved in {ctx.guild}!",
                    )
                return await self.review_user(ctx, member, app=app, recurse=kw.get("recurse", 1))
        elif not app:
            emojis = [(str(n) + f"\N{variation selector-16}\N{combining enclosing keycap}") for n in range(10)]
            emojis.append(f"\N{keycap ten}")
            emojis += [str(get_emoji(x)) for x in range(11, 21)]
            options = {}
            for n, sub in enumerate(guild.applied[:20]):
                try:
                    options[n] = f"<@{sub[0]}>:{(await App.from_id(ctx, sub[1])).name.lower()}"
                except (discord.HTTPException, commands.BadArgument):
                    continue
            e = discord.Embed(
                title=f"What app from {member} should we review?",
                description="\n".join(f"{emojis[n]}: {d}" for n, d in options.items())[:2048],
                color=discord.Color.orange(),
            )
            msg = await ctx.send(embed=e)
            used_emojis = [em for em in emojis if em in e.description]
            used_dict = {str(n): used_emojis[n] for n in range(len(used_emojis))}
            used_dict["cancel"] = "\U000023f9"
            for emoji in used_dict.values():
                self.bot.loop.create_task(msg.add_reaction(emoji))

            try:
                emoji = await wf_msg_or_r(ctx, message=msg, **used_dict)
                if emoji == used_dict["cancel"]:
                    return await msg.delete(delay=0.1)
            except asyncio.TimeoutError:
                return await msg.delete(delay=0.1)
            else:
                await msg.delete()
                e = options[used_emojis.index(str(emoji))]
                user_id = int(re.search(r"<@[!]?(?P<id>[0-9]*)>", e).group("id"))
                try:
                    member = ctx.guild.get_member(user_id) or await ctx.guild.fetch_member(user_id)
                except discord.NotFound:
                    member = None
                if not member:
                    for sub in guild.applied:
                        if sub[0] == user_id:
                            guild.applied.remove(sub)
                    async with self.bot.gino_db.transaction() as tx:
                        await guild.update(applied=guild.applied).apply()
                    return await ctx.send(
                        f"The member that applied for this app has left, so I have deleted their app."
                    )
                name = e.split(":")[-1]
                try:
                    app = await App.from_name(ctx, name, cross_guild=False)
                except commands.BadArgument:
                    for sub in guild.applied:
                        if sub[0] == user_id and sub[2].get("name", "deleted app") == name:
                            data = sub[2]
                            break
                    else:
                        return await ctx.send(
                            f"\N{heavy exclamation mark symbol} The app that this user applied"
                            f" from could not be recovered (since it has been deleted)."
                        )
                    # construct a partial app
                    app = PartialApp(
                        ctx,
                        0,
                        name,
                        data.get("cached_questions", {"unable to get questions": {"message": "fatal error"}}),
                        datetime.utcnow(),
                        [],
                        [],
                        [],
                        f"Your app was approved in {ctx.guild}!",
                    )
                    await ctx.send(
                        f"[\N{warning sign}] Unable to find source application (deleted). Using"
                        f" cached values from partial app. **Functionality is NOT guaranteed.**",
                        delete_after=10,
                    )
                    await asyncio.sleep(5)
                return await self.review_user(ctx, member, app=app, recurse=kw.get("recurse", 1))

        embeds = []
        e = discord.Embed(title=f"[Review - Page 1] Apply for: {app.name}:", color=discord.Color.orange())
        e.set_author(name=str(member), icon_url=str(member.avatar_url_as(static_format="png")))

        n = 0
        for sub in guild.applied:
            # hi
            # what does this code do
            # I know I wrote it but like
            # I've got a bad habit of never commenting whatever this is
            # -eek
            if sub[0] == member.id and sub[1] == app.id:
                answers = sub[-1]["answers"]
                break
        else:
            raise RuntimeError(
                "Hit `else` block in `for sub in guild.applied` block. this shouldnt happen."
            )
        for question in app.questions.keys():
            question: str
            if len(e.fields) == 25 or len(e) + len(question) >= 5000:
                embeds.append(e)
                e = discord.Embed(
                    title=f"[Review - Page {len(embeds) + 1}] Apply for: {app.name}:", color=discord.Color.orange()
                )
                e.set_author(name=str(member), icon_url=str(member.avatar_url_as(static_format="png")))

            try:
                lines = answers[n].splitlines()
            except IndexError:
                lines = []
            except AttributeError:  # is new format
                lines = [answers[n]["question"], answers[n]["answer"]]
            if not lines:
                Q = f"[FATAL ERROR]"
                A = f"[FATAL ERROR, BUT MORE FATAL]"
            else:
                Q = lines[0].capitalize()
                A = "\n".join(lines[1:])  # eek, what the hell is this mess? Please fix it eventually. <3

            e.add_field(name=Q[:256], value=shrt(A, 1024, placeholder="..."))
            n += 1
        embeds.append(e)

        messages = []
        if len(embeds) >= 10:
            await ctx.channel.trigger_typing()
        for embed in embeds:
            messages.append(await ctx.send(embed=embed))
            # await asyncio.sleep(1)  # commented out because we dont actually need this (often)
        last_message = await ctx.send(
            f"**What to press:**\n"
            f"\U00002705 (or say `approve`): Approve\n"
            f"\U0001f4e5 (or say `reason`): Approve, with reason\n"
            f"\U0000274c (or say `deny`): Deny\n"
            f"\U000023f9\U0000fe0f (or say `stop`/`cancel`): Stop reviewing"
        )

        reactions = ["\U00002705", "\U0001f4e5", "\U0000274c", "\U000023f9"]
        for reaction in reactions:
            self.bot.loop.create_task(last_message.add_reaction(reaction))
        try:
            emoji = await wf_msg_or_r(
                ctx,
                timeout=600,
                message=last_message,
                approve=reactions[0],
                reason=reactions[1],
                deny=reactions[2],
                stop=reactions[3],
                cancel=reactions[3],
            )
            if emoji == reactions[3]:
                for message in messages:
                    await message.delete(delay=0.1)
                await last_message.delete(delay=0.1)
                return
        except asyncio.TimeoutError:
            for message in messages:
                await message.delete(delay=0.1)
            await last_message.delete(delay=0.1)
            # await ctx.channel.delete_messages(messages)
        else:
            try:
                reward = re.sub(
                    r"{([^.}]+(\.)*)+\.(_state|(_)?http)([^}])*}",
                    lambda match: "[Automatically blacklisted filler]",
                    app.reward_message,
                    re.MULTILINE | re.VERBOSE | re.IGNORECASE,
                )
                reward = reward.format_map(
                    Mapping(
                        author=member,
                        app=app,
                        guild=ctx.guild,
                        reviewer=ctx.author,
                        message=ctx.message,
                        server=ctx.guild,
                    )
                )
                reward = reward.replace(
                    self.bot.http.token, "[It seems someone tried to expose my token. They failed.]"
                )
                reward = textwrap.shorten(reward, 2048)
            except Exception as e:
                await ctx.send("\N{warning sign} Unable to format reward message. Defaulting to empty.\n" f"Error: {e}")
                reward = discord.Embed.Empty

            def accept(mes):
                if mes.author.bot:
                    return False
                if mes.guild != ctx.guild:
                    return False
                if mes.channel != ctx.channel:
                    return False
                if mes.author == ctx.author or (
                    mes.content.lower() == "force cancel" and mes.author.guild_permissions.administrator
                ):
                    return True
                return False

            if emoji == reactions[0]:  # Approve (w/o reason)
                try:
                    await member.send(
                        embed=discord.Embed(
                            title=f"Your application for {app.name} in {ctx.guild} has been approved!",
                            description=reward,
                            color=discord.Color.green(),
                        )
                    )
                except discord.HTTPException:
                    pass
                finally:
                    log = self.bot.get_channel(guild.log_channel)
                    arc = self.bot.get_channel(guild.arc_channel)
                    if log:
                        try:
                            await log.send(
                                embed=discord.Embed(
                                    title=f"{ctx.author} approved {member}'s application for {app.name}.",
                                    color=discord.Color.green(),
                                )
                            )
                        except discord.HTTPException:
                            pass
                    if arc:
                        try:
                            for embed in embeds:
                                embed.set_author(name=str(member), icon_url=str(member.avatar_url))
                                embed.color = discord.Color.green()
                                embed.timestamp = ctx.message.created_at
                                embed.set_footer(text=f"Accepted by {ctx.author}")
                                await arc.send(embed=embed)
                        except discord.HTTPException:
                            pass
                    try:
                        await member.add_roles(*app.reward_roles)
                        await member.remove_roles(*app.remove_roles)
                    except discord.HTTPException:
                        await ctx.send(
                            f"There was an error giving the user their reward roles.\nPlease ensure my role"
                            f" is above theirs, and that the reward roles are all below my top role."
                        )
                    finally:
                        for message in messages:
                            await message.delete(delay=0.1)
                        await last_message.delete(delay=0.1)
                        guild.applied.remove(discord.utils.find(lambda _sub: _sub[0] == member.id, guild.applied))
                        async with self.bot.gino_db.transaction() as tx:
                            await guild.update(applied=guild.applied).apply()
                        return await ctx.send(f"Approved {member} for app {app.name}!")
            elif emoji == reactions[1]:  # approve, w/ reason
                reason_msg = await ctx.send(f"Please enter a reason as to why this person is being approved")
                try:
                    m = await self.bot.wait_for("message", check=lambda _ms: accept(_ms), timeout=3600)
                    if m.content.lower() in ["cancel", "stop", "force cancel"]:
                        for message in messages:
                            await message.delete(delay=0.1)
                        await last_message.delete(delay=0.1)
                        return await reason_msg.edit(content="cancelled reviewing.")
                except asyncio.TimeoutError:
                    return await ctx.send(f"Timed out.")
                else:
                    reason = textwrap.shorten(m.clean_content, 1024)
                    await m.delete()
                    await reason_msg.delete()
                try:
                    await member.send(
                        embed=discord.Embed(
                            title=f"Your application for {app.name} in {ctx.guild} has been approved!",
                            description=f"Review Notes: {reason}",
                            color=discord.Color.green(),
                        )
                    )
                except discord.HTTPException:
                    pass
                finally:
                    log = self.bot.get_channel(guild.log_channel)
                    arc = self.bot.get_channel(guild.arc_channel)
                    if log:
                        try:
                            await log.send(
                                embed=discord.Embed(
                                    title=f"{ctx.author} approved {member}'s application for {app.name}.",
                                    description=f"Reason: {reason}",
                                    color=discord.Color.green(),
                                )
                            )
                        except discord.HTTPException:
                            pass
                    if arc:
                        try:
                            for embed in embeds:
                                embed.set_author(name=str(member), icon_url=str(member.avatar_url))
                                embed.timestamp = ctx.message.created_at
                                embed.set_footer(text=f"Accepted by {ctx.author}")
                                embed.color = discord.Color.orange()
                                await arc.send(embed=embed)
                        except discord.HTTPException:
                            pass
                    try:
                        await member.add_roles(*app.reward_roles)
                        await member.remove_roles(*app.remove_roles)
                    except discord.HTTPException:
                        await ctx.send(
                            f"There was an error giving the user their reward roles.\nPlease ensure my role"
                            f" is above theirs, and that the reward roles are all below my top role."
                        )
                    finally:
                        for message in messages:
                            await message.delete(delay=0.1)
                        await last_message.delete(delay=0.1)
                        guild.applied.remove(discord.utils.find(lambda _sub: _sub[0] == member.id, guild.applied))
                        async with self.bot.gino_db.transaction() as tx:
                            await guild.update(applied=guild.applied).apply()
                        return await ctx.send(f"Approved {member} for app {app.name}, with a reason!")
            elif emoji == reactions[2]:  # reject
                reason_msg = await ctx.send(f"Please enter a reason as to why this person is being rejected")
                try:
                    m = await self.bot.wait_for("message", check=lambda _ms: accept(_ms), timeout=3600)
                    if m.content.lower() in ["cancel", "stop", "force cancel"]:
                        for message in messages:
                            await message.delete(delay=0.1)
                        await last_message.delete(delay=0.1)
                        return await reason_msg.edit(content="cancelled reviewing.")
                except asyncio.TimeoutError:
                    return await ctx.send(f"Timed out.")
                else:
                    reason = textwrap.shorten(m.clean_content, 2000)
                    await m.delete()
                    await reason_msg.delete()
                try:
                    await member.send(
                        embed=discord.Embed(
                            title=f"Your application for {app.name} in {ctx.guild} has been Rejected!",
                            description=f"Reason: {reason}",
                            color=discord.Color.red(),
                        )
                    )
                except discord.HTTPException:
                    pass
                finally:
                    log = self.bot.get_channel(guild.log_channel)
                    arc = self.bot.get_channel(guild.arc_channel)
                    if log:
                        try:
                            await log.send(
                                embed=discord.Embed(
                                    title=f"{ctx.author} rejected {member}'s application for {app.name}.",
                                    description=f"Reason: {reason}",
                                    color=discord.Color.red(),
                                )
                            )
                        except discord.HTTPException:
                            pass
                    if arc:
                        try:
                            for embed in embeds:
                                embed.set_author(name=str(member), icon_url=str(member.avatar_url))
                                # noinspection PyUnresolvedReferences,PyDunderSlots
                                embed.color = discord.Color.red()
                                embed.timestamp = ctx.message.created_at
                                embed.set_footer(text=f"Rejected by {ctx.author}")
                                await arc.send(embed=embed)
                        except discord.HTTPException:
                            pass

                    for message in messages:
                        await message.delete(delay=0.1)
                    await last_message.delete(delay=0.1)
                    guild.applied.remove(discord.utils.find(lambda _sub: _sub[0] == member.id, guild.applied))
                    async with self.bot.gino_db.transaction() as tx:
                        await guild.update(applied=guild.applied).apply()
                    return await ctx.send(f"Rejected {member} for app {app.name}!")
            elif emoji == reactions[3]:
                await ctx.message.delete(delay=0.1)
                for message in messages:
                    await message.delete(delay=0.1)
                await last_message.delete(delay=0.1)

    @custom_commands.command(name="apps", aliases=["applied", "submitted"])
    @review_or_permissions(manage_roles=True)
    @commands.max_concurrency(1, commands.BucketType.channel)
    async def apps(self, ctx: commands.Context, mentions: bool = True):
        """Lists who has applied in the server."""

        def get_emoji(en):
            return (
                discord.utils.get(self.bot.emojis, name=str(en), guild=self.bot.get_guild(706271127542038608))
                or f'<missing emoji "{en}">'
            )

        guild = self.bot.guilds_cache.get(ctx.guild.id)

        if len(guild.applied) == 0:
            return await ctx.send(
                embed=discord.Embed(
                    title=f"{Emojis.p(ctx, 'x')} Nobody has applied in this server!", color=discord.Color.red()
                )
            )

        emojis = [(str(n) + f"\N{variation selector-16}\N{combining enclosing keycap}") for n in range(10)]
        emojis.append(f"\N{keycap ten}")
        emojis += [str(get_emoji(x)) for x in range(11, 21)]

        options = {}
        for n, sub in enumerate(guild.applied[:20]):
            try:
                name = (await App.from_id(ctx, sub[1])).name.lower()
            except discord.HTTPException:
                name = sub[-1].get("cached_name", "deleted app")
            if mentions:
                options[n] = f"<@{sub[0]}>:{name}"
            else:
                options[n] = f"{self.bot.get_user(sub[0]) or 'deleted-user#0000'}: {name}"

        e = discord.Embed(
            title="The following people have applied:",
            description=("\n".join(f"{emojis[n]}: {d}" for n, d in options.items()))[:2048],
            color=discord.Color.orange(),
        )
        # e.set_footer(text="React with the corresponding emoji below to start reviewing them!")
        e.set_footer(text=f'Want to review someone? run "{ctx.prefix}review {{@user}}"')
        await ctx.send(embed=e)

    @custom_commands.group(
        name="appbutton", aliases=["reactapply", "ra", "_ab", "appbuttons"], invoke_without_command=True
    )
    @admin_or_permissions(manage_messages=True, manage_roles=True)
    async def ab(self, ctx: commands.Context):
        """Shows you your application buttons. These are like reaction roles,
        except instead of giving people who react a role, applies for an app."""

        def construct_url(app_button):
            return "https://discord.com/channels/{}/{}/{}".format(ctx.guild.id, app_button.channel_id, app_button.id)

        appbuttons = filter(lambda ab: ab.guild_id == ctx.guild.id, await sql.AppButtons.query.gino.all())
        e = discord.Embed(title="Your server's appbuttons:", description="", color=discord.Color.gold())
        for appbutton in appbuttons:
            e.description += f"[{appbutton.id}:]({construct_url(appbutton)}) {len(appbutton.pointers)} buttons\n"
        if len(e.description) >= 2048:
            for appbutton in appbuttons:
                e.description += f"{appbutton.id}: {len(appbutton.pointers)}\n"
            e.set_footer(text="Output has been stripped down because it was too large.")
        if len(e.description) >= 2048:
            for appbutton in appbuttons:
                formatted = f"{appbutton.id}: {len(appbutton.pointers)}\n"
                if len(e.description) >= 2000 or (len(e.description) + len(formatted)) >= 2000:
                    continue
                e.description += formatted
            e.set_footer(text="Output has been REALLY stripped down because it was way too large.")
        if len(e.description) >= 2048:
            return await ctx.send(f"Too large output :c")

        e.description = (
            e.description or f"Your server has no appbuttons! You can create one via `{ctx.prefix}appbutton add`!"
        )
        await ctx.send(embed=e)

    @ab.command(name="add", aliases=["create"])
    @admin_or_permissions(manage_messages=True, manage_roles=True)
    @commands.bot_has_permissions(manage_messages=True)
    async def ab_add(self, ctx: commands.Context):
        """Adds an appbutton to a message.

        This command is interactive and takes no arguments"""
        if len(list(filter(lambda ab: ab.guild_id == ctx.guild.id, await sql.AppButtons.query.gino.all()))) > 10:
            return await ctx.send(
                "\N{cross mark} There's a limit of 10 appbuttons per-server for free users."
                "\nPlease consider donating and getting premium via `ya?donate`"
            )
        kwargs = dict(id=None, channel_id=None, guild_id=ctx.guild.id, pointers={}, remove_reactions=True)
        pre_existing = False
        msg = await ctx.send(f"What is the **message ID** of this appbutton?")
        while True:
            msg_id = await self.bot.wait_for(
                "message",
                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content.isdigit(),
                timeout=600,
            )
            await msg_id.delete(delay=0.1)
            kwargs["id"] = int(msg_id.content)
            ab = await sql.AppButtons.get(int(msg_id.content))
            if ab:
                try:
                    kwargs = ab.__dict__()
                except (ValueError, TypeError, AttributeError):  # i have no idea what this raises
                    kwargs = ab.__dict__.get("__values__", ab.__dict__)
                pre_existing = True
            else:
                appbuttons = filter(lambda _ab: _ab.guild_id == ctx.guild.id, await sql.AppButtons.query.gino.all())
                if len(list(appbuttons)) >= 10 and not self.bot.guilds_cache[ctx.guild.id].premium:
                    return await ctx.send(
                        embed=discord.Embed(
                            title="Looks like you've hit a premium limit!",
                            description=f"As a regular server, you get **20** app-buttons. If you upgrade to premium,"
                            f" you can get unlimited!\n\nFancy it? Join our [support server]({config.server_invite})"
                            f" and query our support team!",
                            color=discord.Color.red(),
                        )
                    )
            try:
                await ctx.channel.fetch_message(int(msg_id.content))
                kwargs["channel_id"] = ctx.channel.id
            except (discord.NotFound, discord.HTTPException):
                pass
            break
        while not kwargs["channel_id"]:
            await msg.edit(content="What **channel ID** is this message in?")
            channel_id = await self.bot.wait_for(
                "message",
                check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content.isdigit(),
                timeout=600,
            )
            await channel_id.delete(delay=0.1)
            channel = self.bot.get_channel(int(channel_id.content))
            try:
                await channel.fetch_message(kwargs["id"])
            except discord.NotFound:
                await msg.edit(
                    content=f"Message \"{kwargs['id']}\" was not found in channel {channel.mention}."
                    f"Please ensure you have the correct channel, and try again. If you think "
                    f"you provided the wrong message ID, please say `cancel`."
                )
                await asyncio.sleep(5)
                continue
            except AttributeError:  # invalid channel:
                await msg.edit(content=f'Channel "{channel_id.content}" was not found. try again.')
                await asyncio.sleep(5)
            else:
                kwargs["channel_id"] = channel.id
        while True:
            await msg.edit(
                content=f"What application do you want this button to point to?\n"
                f"You can see a list of applications via `{ctx.prefix}positions`."
            )
            app_raw = await self.bot.wait_for(
                "message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=600
            )
            try:
                app = await App.convert(ctx, app_raw.clean_content)
            except commands.BadArgument:
                await msg.edit(content=f"An application with that name/ID was not found! Try again.")
                await asyncio.sleep(5)
                continue
            else:
                break
        new_pointer = {}
        while True:
            # noinspection PyUnboundLocalVariable
            await msg.edit(
                content=f"What emoji should people react to in order to trigger this button (and apply for"
                f" {app.name})? __React to this message.__"
                f"\n*Note: Only non-custom and this server's custom emojis are guaranteed"
                f" to work. If I don't continue to the next step, it means you reacted with something"
                f" I can't use.*"
            )
            # linter is throwing a pissyfit over the `app.name` ref in that string.
            # Honestly, it just needs to learn EAFF.
            reaction, _ = await self.bot.wait_for(
                "reaction_add",
                check=lambda r, u: isinstance(u, discord.Member)
                and not u.bot
                and u.guild == ctx.guild
                and usable_reaction(r, u)
                and r.message.id == msg.id,
                timeout=1200,
            )
            new_pointer[str(reaction.emoji)] = app.id

            break
        await msg.edit(content="Continue below...")
        remove_reactions = await Confirm(
            "This means that when someone reacts to apply, it will automatically" " remove their reaction.",
            title="Would you like to auto-remove reactions?",
        ).result(ctx)
        kwargs["remove_reactions"] = remove_reactions
        kwargs["pointers"].update(new_pointer)

        async with self.bot.gino_db.transaction() as tx:
            await msg.edit(content=("Creating" if not pre_existing else "Updating") + " Appbutton...")
            if kwargs.get("_id"):
                kwargs["id"] = kwargs["_id"]
                del kwargs["_id"]
            if pre_existing:
                await ab.update(
                    id=kwargs["id"], pointers=kwargs["pointers"], remove_reactions=kwargs["remove_reactions"]
                ).apply()
            else:
                ab = await sql.AppButtons.create(
                    id=kwargs["id"],
                    channel_id=kwargs["channel_id"],
                    guild_id=kwargs["guild_id"],
                    pointers=kwargs["pointers"],
                    remove_reactions=kwargs["remove_reactions"],
                )
        await msg.edit(content="Appbutton created/updated. Adding the finishing touches...")
        try:
            message = await self.bot.get_channel(ab.channel_id).fetch_message(ab.id)
            if remove_reactions:
                for reaction in message.reactions:
                    if str(reaction.emoji) in ab.pointers.keys():
                        await message.clear_reaction(reaction.emoji)

            for emoji in ab.pointers.keys():
                await message.add_reaction(emoji)
            return await msg.edit(content="All done!", delete_after=10)
        except discord.HTTPException:
            return await msg.delete(delay=10)  # go away errors

    @ab.command(name="remove", aliases=["rem", "del", "delete"])
    @admin_or_permissions(manage_messages=True, manage_roles=True)
    async def ab_rem(self, ctx: commands.Context, message_id: int, *, emoji: str = None):
        """Removes an appbutton.

        If you don't specify an emoji, it will remove __all__ buttons on that message."""
        appbutton = await sql.AppButtons.get(message_id)
        if not appbutton:
            return await ctx.send(f":x: No appbutton with that ID in this server found.")
        elif appbutton.guild_id != ctx.guild.id:
            return await ctx.send(f":x: No appbutton with that ID in this server found.")

        if await Confirm().result(ctx):
            if emoji:
                if not appbutton.pointers.get(emoji):
                    pass
                else:
                    try:
                        message = await self.bot.get_channel(appbutton.channel_id).fetch_message(appbutton.id)
                        await message.clear_reaction(emoji)
                    except discord.HTTPException:
                        pass
                    del appbutton.pointers[emoji]
                    update = await appbutton.update(pointers=appbutton.pointers).apply()
            else:
                await appbutton.delete()
            return await ctx.send(f"\N{white heavy check mark}", delete_after=5)

    @commands.Cog.listener()
    async def on_guild_remove(self, guild: discord.Guild):
        self.bot.chunk_progress -= 1
        g = self.bot.guilds_cache[guild.id]
        if g:
            await g.delete()
        try:
            url = discord.utils.oauth_url(str(self.bot.user.id), guild=guild)
            await guild.owner.send(
                f"It seems like I was removed from your server '{guild}'. If you think this was"
                f" a mistake, you can add me back and your data will be un-deleted. "
                f"\nIf this was intentional, please let us know why at "
                f"<https://forms.gle/ejhTQ1ipCvU4BTqq7>. It only takes ~30 seconds!\n\n"
                f"Want to re-invite me? https://yourapps.cyou/invite or <{url}>"
            )
        except discord.HTTPException:
            pass

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
        if not self.bot.is_ready():
            await self.bot.wait_until_ready()
        appbutton = await sql.AppButtons.get(payload.message_id)
        if not appbutton:
            return

        message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id)
        guild = message.guild
        if not guild.chunked:
            await guild.chunk()
        if message.guild.chunked:
            member = message.guild.get_member(payload.user_id)
        else:
            try:
                member = await message.guild.fetch_member(payload.user_id)
            except discord.NotFound:
                member = None
        if not member:
            return await message.remove_reaction(payload.emoji, discord.Object(payload.user_id))
        if member.bot:
            return
        if str(payload.emoji) not in appbutton.pointers.keys():
            return
        else:
            ctx = copy.copy(await self.bot.get_context(message))
            app_id = appbutton.pointers[str(payload.emoji)]
            try:
                app = await App.from_id(ctx, app_id)
            except commands.BadArgument:
                await ctx.author.send(f"Failed to build appbutton in {message.guild}.")
                return
            ctx.author = message.guild.get_member(payload.user_id)
            ctx.command = self.bot.get_command("apply")
            ctx.kwargs = {"app": app}
            ctx.source = "appbutton"
            try:
                await ctx.command.callback(self.bot.cogs["Member"], ctx, app=app)
            except Exception as e:
                try:
                    await ctx.author.send("There was an error in the application process. Please try again later.")
                except discord.HTTPException:
                    pass
                self.bot.dispatch("command_error", ctx, e)
            finally:
                if appbutton.remove_reactions:
                    try:
                        await message.remove_reaction(payload.emoji, payload.member)
                    except discord.HTTPException:
                        pass
                return


def setup(bot):
    bot.add_cog(Admin(bot))

