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)