async def start_party(rp: ReactionPayload) -> None:
    """
    Emoji handler that implements the party creation feature.

    If the reacting member is already part of another party, either as member
    or leader, an error message is printed and the emoji is removed.
    """

    await rp.message.remove_reaction(Emojis.TADA, rp.member)
    channel = rp.channel
    if channel.id not in db.party_channels:
        # this happens if the channel got deactivated but
        # the menu wasn't deleted
        delete_message = await channel.send(f"Channel has not been configured "
                                            f"for party matchmaking")
        scheduling.message_delayed_delete(delete_message)
        return
    channel_info = db.party_channels[channel.id]

    if await channel_info.get_party_message_of_user(rp.member) is not None:
        delete_message = await channel.send(f"{rp.member.mention}, you are "
                                            f"already in another party! "
                                            f"Leave that party before trying "
                                            f"to create another one.")
        scheduling.message_delayed_delete(delete_message)
        return

    max_slots = channel_info.max_slots
    party = Party(channel, rp.member, max_slots - 1)
    message = await channel.send(embed=party.to_embed())
    await message.add_reaction(Emojis.WHITE_CHECK_MARK)
    await message.add_reaction(Emojis.FAST_FORWARD)
    await message.add_reaction(Emojis.NO_ENTRY_SIGN)
    channel_info.set_party_message_of_user(rp.member, message)
async def add_member_emoji_handler(rp: ReactionPayload) -> bool:
    """
    Emoji handler that implements the party join feature.

    Will add a member to the party and trigger voice channel creation when the
    party is full.

    If the member that reacted is the party leader, the emoji is simply removed
    and no further action is taken.

    If the member is already part of another party, either as member or leader,
    an error message is printed and the emoji is removed.
    """

    party = await Party.from_party_message(rp.message)
    message = rp.message
    channel = rp.channel
    channel_info = db.party_channels[channel.id]

    if party.slots_left < 1 or rp.member == party.leader:  # leader can't join as member
        return False  # remove reaction
    if await channel_info.get_party_message_of_user(rp.member) is not None:
        delete_message = await channel.send(f"{rp.member.mention}, you are "
                                            f"already in another party! "
                                            f"Leave that party before trying "
                                            f"to join another.")
        scheduling.message_delayed_delete(delete_message)
        return False  # remove reaction
    channel_info.set_party_message_of_user(rp.member, message)
    await party.add_member(rp.member, rp.message)
    if party.slots_left < 1:
        await handle_full_party(party, rp.message)
    return True  # keep reaction
async def close_party(rp: ReactionPayload) -> None:
    """
    Emoji handler that implements the party close feature.

    If the reacting member is not the party leader or a bot admin as specified
    in `config.BOT_ADMIN_ROLES`, the emoji is removed and no further action is
    taken.

    Otherwise, the party message the party affiliations (membership,
    leadership) are deleted and an appropriate message is posted to the party
    matchmaking channel.
    """

    party = await Party.from_party_message(rp.message)
    channel = party.channel
    if party.leader != rp.member and not checks.is_admin(rp.member):
        await rp.message.remove_reaction(Emojis.NO_ENTRY_SIGN, rp.member)
        return
    if rp.member != party.leader:
        message = await channel.send(f"> {rp.member.mention} has just force "
                                     f"closed {party.leader.mention}'s party!")
    else:
        message = await channel.send(f"> {rp.member.mention} has just "
                                     f"disbanded their party!\n")
    await rp.message.delete()
    for m in party.members:
        db.party_channels[channel.id].clear_party_message_of_user(m)
    db.party_channels[channel.id].clear_party_message_of_user(party.leader)
    scheduling.message_delayed_delete(message)
async def deactivate_party(ctx):
    """
    Deactivates the party matchmaking feature for this channel, removing the
    party creation menu.
    """
    del db.party_channels[ctx.channel.id]
    await ctx.message.delete()
    await ctx.channel.purge(limit=100, check=checks.author_is_me)
    message = await ctx.send(f"Party matchmaking disabled for this channel.")
    scheduling.message_delayed_delete(message)
async def handle_react_event_channel(rp: ReactionPayload) -> None:
    """
    Reaction handler for the event voice channel feature.
    """
    translation_tuple = translate_emoji_event_channels(rp.message, rp.emoji)
    if translation_tuple is None:
        return  # unknown emoji, ignore reaction

    game_name, channel_name, position = translation_tuple

    try:
        channel_id = discord.utils.get(rp.guild.voice_channels, name=channel_name).id
    except discord.NotFound:
        message = await rp.channel.send(f"Channel {channel_name} not found.")
        scheduling.message_delayed_delete(message)
        return

    channel, channel_position = await channelinformation.fetch_reference_channel(
        channel_id, rp.guild
    )
    category = rp.guild.get_channel(channel.category_id)

    counter = 1
    for channel in rp.guild.voice_channels:
        if channel.name.startswith(f"{game_name} - #"):
            counter += 1

    # give creator the ability to change permissions
    overwrites = {rp.member: PermissionOverwrite(manage_permissions=True)}

    vc = await rp.guild.create_voice_channel(
        f"{game_name} - #{counter}", category=category, overwrites=overwrites
    )
    db.event_voice_channels.add(vc.id)

    if position:  # if True the channel will be created above channel_position
        await vc.edit(position=channel_position + 0)
    else:  # else (False) it will be created below channel_position
        await vc.edit(position=channel_position + 1)

    prot_delay_hours = config.EVENT_CHANNEL_GRACE_PERIOD_HOURS
    scheduling.channel_start_grace_period(vc, prot_delay_hours * 3600)

    message = await rp.channel.send(
        f"{rp.member.mention} "
        f"Connect to {vc.mention}. "
        f"Your channel will stay open for "
        f"{prot_delay_hours} hours. "
        f"After that, it gets deleted as soon as "
        f"it empties out. "
        f"You can change the channel's permissions as you see fit."
    )
    scheduling.message_delayed_delete(message)

    return  # will always remove emoji reaction
async def deactivate_event_channel(ctx):
    """
    Deactivates the side game voice channel feature for this channel.
    """

    await ctx.message.delete()
    message = await ctx.send(f"Event voice channel creation disabled for "
                             f"this channel.")
    scheduling.message_delayed_delete(message)

    db.event_channels.remove(ctx.channel.id)
async def deactivate_side_games(ctx):
    """
    Deactivates the side game voice channel feature for this channel.
    """

    await ctx.message.delete()
    message = await ctx.send(f"Side game voice channel creation disabled for "
                             f"this channel.")
    scheduling.message_delayed_delete(message)

    del db.games_channels[ctx.channel.id]
async def activate_side_games(ctx, channel_below_id: int):
    """
    Activates the side game voice channel feature for this channel.
    Note that the voice channel creation menu has to be supplied seperately.
    See "Menu Formatting" below.

    Attributes:
        channel_below_id (int): The ID of the voice channel above which the
            voice channels will be created.

    Menu Formatting:
        The bot watches all menu messages in channels for which this feature
        is activated.
        Menu messages are messages that
        - Have been posted by a member that has any of the bot administrator
          roles specified in the bot configuration (config.BOT_ADMIN_ROLES).
        - Contain at least one menu entry (see below).

        Menu entries are lines in a menu message that have the following
        format:
        ``
        > EMOJI SIDE_GAME_NAME
        ``
        `EMOJI` must be either a Unicode emoji or a custom emoji.
        `SIDE_GAME_NAME` must be a sequence of any characters except a
        line-break.
        Note that the quote character (`>`) has to be the first character in
        the line.

        There can be multiple menu entries per menu and there can be multiple
        menus per channel.

        Example:
            ``
            > :map: Strategy Games
            > :Minecraft: Minecraft
            ``

    To deactivate this feature, use the `deactivate_party` command.
    """

    channel_below = ctx.guild.get_channel(channel_below_id)
    if channel_below is None:
        raise commands.errors.BadArgument()

    await ctx.message.delete()
    message = await ctx.send(f"Channel activated for side game voice "
                             f"channel creation.")
    scheduling.message_delayed_delete(message)

    channel_info = GamesChannelInformation(ctx.channel, channel_below)
    db.games_channels[ctx.channel.id] = channel_info
async def activate_event_channel(ctx):
    """
    Activates the event voice channel feature for this channel.
    Note that the voice channel creation menu has to be supplied seperately.
    See "Menu Formatting" below.

    Menu Formatting:
        The bot watches all menu messages in channels for which this feature
        is activated.
        Menu messages are messages that
        - Have been posted by a member that has any of the bot administrator
          roles specified in the bot configuration (config.BOT_ADMIN_ROLES).
        - Contain at least one menu entry (see below).

        Menu entries are lines in a menu message that have the following
        format:
        ``
        > EMOJI EVENT_NAME [Above "CHANNEL_BELOW_NAME"]
        ``
        `EMOJI` must be either a Unicode emoji or a custom emoji.
        `EVENT_NAME` must be a sequence of any characters except a
        line-break.
        `CHANNEL_BELOW_NAME` must be the name of the voice channel above which the
        voice channel will be created. Note: If the name of the voice channel changes
        it must be changed manually in the menu entry too.
        Note that the quote character (`>`) has to be the first character in
        the line.

        There can be multiple menu entries per menu and there can be multiple
        menus per channel.

        Example:
            ``
            > :map: Strategy Games [Above "Lobby"]
            > :Minecraft: Minecraft [Above "EFT - #1"]
            ``

    To deactivate this feature, use the `deactivate_party` command.
    """

    await ctx.message.delete()
    message = await ctx.send(f"Channel activated for event voice "
                             f"channel creation.")
    scheduling.message_delayed_delete(message)

    db.event_channels.add(ctx.channel.id)
async def handle_react_side_games(rp: ReactionPayload) -> None:
    """
    Reaction handler for the side games voice channel feature.
    """
    game_name = translate_emoji_game_name(rp.message, rp.emoji)
    if game_name is None:
        return  # unknown emoji, ignore reaction

    channel_info = db.games_channels[rp.channel.id]

    # check if user already created a party channel
    vc_id = channel_info.channel_owners.get(rp.member.id)
    if vc_id is not None:
        # make sure it's actually still there
        if rp.guild.get_channel(vc_id) is None:
            print(
                f"VC deletion was not tracked!\n" f"- Owner: {rp.member}\n",
                file=sys.stderr,
            )
            del channel_info.channel_owners[rp.member.id]
        else:
            message = await rp.channel.send(
                f"{rp.member.mention} " f"You already have an open channel."
            )
            scheduling.message_delayed_delete(message)
            return

    if game_name not in channel_info.counters:
        channel_info.counters[game_name] = 0
    channel_info.counters[game_name] += 1
    counter = channel_info.counters[game_name]
    channel_below, channel_below_position = await channel_info.fetch_channel_below(
        rp.guild
    )
    category = rp.guild.get_channel(channel_below.category_id)

    # give creator the ability to change permissions
    overwrites = {rp.member: PermissionOverwrite(manage_permissions=True)}

    vc = await rp.guild.create_voice_channel(
        f"{game_name} - #{counter}", category=category, overwrites=overwrites
    )
    await vc.edit(position=channel_below_position + 0)
    channel_info.channel_owners.update({rp.member.id: vc.id})
    prot_delay_hours = config.GAMES_CHANNEL_GRACE_PERIOD_HOURS
    scheduling.channel_start_grace_period(
        vc,
        prot_delay_hours * 3600,
        delete_callback=side_games_deletion_callback,
        delete_callback_args=[rp.channel.id],
    )

    message = await rp.channel.send(
        f"{rp.member.mention} "
        f"Connect to {vc.mention}. "
        f"Your channel will stay open for "
        f"{prot_delay_hours} hours. "
        f"After that, it gets deleted as soon as "
        f"it empties out. "
        f"You can change the channel's permissions as you see fit."
    )
    scheduling.message_delayed_delete(message)

    return  # will always remove emoji reaction
async def handle_full_party(party: Party,
                            party_message: discord.Message) -> None:
    """
    Called by `Party.add_member` when a party reaches zero open slots.
    Deletes the party message and creates a party voice channel.
    Will inform all party members by posting a message in the party matchmaking
    channel.
    """
    channel = party_message.channel
    guild = party_message.guild
    channel_info = db.party_channels[channel.id]
    division_admin = guild.get_role(
        channel_info.division_admin_id) or guild.get_member(
            channel_info.division_admin_id)
    channel_above, channel_above_position = await channel_info.fetch_channel_above(
        guild)
    category = guild.get_channel(channel_above.category_id)

    overwrites = {
        guild.default_role:
        discord.PermissionOverwrite(read_messages=True,
                                    connect=channel_info.open_parties),
        guild.me:
        discord.PermissionOverwrite(read_messages=True),
        party.leader:
        discord.PermissionOverwrite(read_messages=True, connect=True),
    }
    overwrites.update({
        member: discord.PermissionOverwrite(read_messages=True, connect=True)
        for member in party.members
    })

    # allow bot admins
    for role_id in config.BOT_ADMIN_ROLES:
        role = party_message.guild.get_role(role_id)
        if role is None:
            print(f"[WARN] Bot admin role {role_id} does not exist.")
            continue
        overwrites.update({
            role:
            discord.PermissionOverwrite(read_messages=True, connect=True)
        })

    # allow division admin
    if division_admin is not None:
        overwrites.update({
            division_admin:
            discord.PermissionOverwrite(read_messages=True, connect=True)
        })

    counter = channel_info.voice_channel_counter
    channel_info.voice_channel_counter += 1
    vc = await guild.create_voice_channel(
        f"{channel_info.game_name} "
        f"- Party - #{counter}",
        category=category,
        overwrites=overwrites,
    )
    await vc.edit(position=channel_above_position + 1)
    channel_info.active_voice_channels.add(vc.id)

    # delete original party message
    mentions = f"{party.leader.mention} " + " ".join(
        [m.mention for m in party.members])
    for m in party.members:
        db.party_channels[channel.id].clear_party_message_of_user(m)
    db.party_channels[channel.id].clear_party_message_of_user(party.leader)
    await party_message.delete()

    # send additional message, notifying members
    message = await channel.send(
        f"{mentions}. Matchmaking done. "
        f"Connect to {vc.mention}. "
        f"You have "
        f"{config.PARTY_CHANNEL_GRACE_PERIOD_SECONDS} "
        f"seconds to join. "
        f"After that, the channel gets deleted as soon as it "
        f"empties out.")
    scheduling.channel_start_grace_period(
        vc, config.PARTY_CHANNEL_GRACE_PERIOD_SECONDS)
    scheduling.message_delayed_delete(message)
async def activate_party(
    ctx,
    game_name: str,
    max_slots: int,
    channel_above_id: int,
    open_parties: str,
    division_admin: Optional[Union[discord.Member, discord.Role]],
):
    """
    Activates the party matchmaking feature for this channel, spawning a party
    creation menu.

    Attributes:
        game_name (str): The name of the game displayed in the party creation
            menu.
        max_slots (int): Maximum amount of players per party.
        channel_above_id (int): The ID of the voice channel below which the
            party voice channels will be created.
        open_parties (str): Either OPEN_PARTIES or CLOSED_PARTIES.
            Determines whether non-party members can join the voice chat.
            Note that anyone with the "Move Members" permission can always
            join party voice channels.
        division_admin (Optional, Role or Member): Role or member that will be able to
            join all parties, even if CLOSED_PARTIES is set.

    To deactivate the party matchmaking feature and remove the party creation
    menu, use the `deactivate_party` command.

    To edit the current configuration, simply run this command again.
    """

    if not checks.is_channel_inactive(
            ctx.channel) and not checks.is_party_channel(ctx.channel):
        raise error_handling.ChannelAlreadyActiveError()

    if open_parties == Strings.OPEN_PARTIES:
        open_parties = True
    elif open_parties == Strings.CLOSED_PARTIES:
        open_parties = False
    else:
        raise commands.errors.BadArgument()

    channel_above = ctx.guild.get_channel(channel_above_id)
    if channel_above is None:
        raise commands.errors.BadArgument()

    await ctx.message.delete()

    if checks.is_party_channel(ctx.channel):
        m = await ctx.send(f"Channel configuration updated.")
        scheduling.message_delayed_delete(m)
    else:
        m = await ctx.send(f"This channel has been activated for party "
                           f"matchmaking.")
        scheduling.message_delayed_delete(m)

    channel_info = PartyChannelInformation(game_name, ctx.channel, max_slots,
                                           channel_above, open_parties,
                                           division_admin)

    db.party_channels[ctx.channel.id] = channel_info
    await ctx.channel.purge(limit=100, check=checks.author_is_me)
    embed = discord.Embed.from_dict({
        "title":
        "Game: %s" % game_name,
        "color":
        0x0000FF,
        "description":
        "React with %s to start a party for %s." % (Emojis.TADA, game_name),
    })
    message = await ctx.send("", embed=embed)
    await message.add_reaction(Emojis.TADA)