async def close_draft_emoji_added(self, member, channel, message): announce_message_link = f'https://discord.com/channels/{member.guild.id}/{channel.id}/{message.id}' logger.debug(f'Draft close reaction added by {member.name} to draft announcement {announce_message_link}') grad_role = discord.utils.get(member.guild.roles, name=grad_role_name) novas_role = discord.utils.get(member.guild.roles, name=novas_role_name) try: await message.remove_reaction(self.emoji_draft_close, member) logger.debug(f'Removing {self.emoji_draft_close} reaction placed by {member.name} on message {message.id}') except discord.DiscordException as e: logger.warn(f'Unable to remove reaction in close_draft_emoji_added(): {e}') if not settings.is_mod(member): return draft_config = self.get_draft_config(member.guild.id) if draft_config['draft_open']: new_message = f'~~{message.content}~~\n{self.draft_closed_message}' log_message = f'Draft status closed by <@{member.id}>' draft_config['draft_open'] = False else: new_message = self.draft_open_format_str.format(grad_role.mention, novas_role.mention, draft_config['draft_message']) log_message = f'Draft status opened by <@{member.id}>' draft_config['draft_open'] = True self.save_draft_config(member.guild.id, draft_config) await self.send_to_log_channel(member.guild, log_message) try: await message.edit(content=new_message) except discord.DiscordException as e: return logger.error(f'Could not update message in close_draft_emoji_added: {e}')
async def conclude_draft_emoji_added(self, member, channel, message): announce_message_link = f'https://discord.com/channels/{member.guild.id}/{channel.id}/{message.id}' logger.debug(f'Conclude close reaction added by {member.name} to draft announcement {announce_message_link}') try: await message.remove_reaction(self.emoji_draft_conclude, member) logger.debug(f'Removing {self.emoji_draft_conclude} reaction placed by {member.name} on message {message.id}') except discord.DiscordException as e: logger.warn(f'Unable to remove reaction in conclude_draft_emoji_added(): {e}') if not settings.is_mod(member): return free_agent_role = discord.utils.get(member.guild.roles, name=free_agent_role_name) draftable_role = discord.utils.get(member.guild.roles, name=draftable_role_name) confirm_message = await channel.send(f'<@{member.id}>, react below to confirm the conclusion of the current draft. ' f'{len(free_agent_role.members)} members will lose the **{free_agent_role_name}** role and {len(draftable_role.members)} members with the **{draftable_role_name}** role will lose that role and become the current crop with the **{free_agent_role_name}** role.\n' '*If you do not react within 30 seconds the draft will remain open.*', delete_after=35) await confirm_message.add_reaction('✅') logger.debug('waiting for reaction confirmation') def check(reaction, user): e = str(reaction.emoji) return ((user == member) and (reaction.message.id == confirm_message.id) and e == '✅') try: reaction, user = await self.bot.wait_for('reaction_add', check=check, timeout=33) except asyncio.TimeoutError: logger.debug(f'No reaction to confirmation message.') return result_message_list = [f'Draft successfully closed by <@{member.id}>'] self.announcement_message = None await message.delete() async with channel.typing(): for old_free_agent in free_agent_role.members: await old_free_agent.remove_roles(free_agent_role, reason='Purging old free agents') logger.debug(f'Removing free agent role from {old_free_agent.name}') result_message_list.append(f'Removing free agent role from {old_free_agent.name} <@{old_free_agent.id}>') for new_free_agent in draftable_role.members: await new_free_agent.add_roles(free_agent_role, reason='New crop of free agents') logger.debug(f'Adding free agent role to {new_free_agent.name}') await new_free_agent.remove_roles(draftable_role, reason='Purging old free agents') logger.debug(f'Removing draftable role from {new_free_agent.name}') result_message_list.append(f'Removing draftable role from and applying free agent role to {new_free_agent.name} <@{new_free_agent.id}>') await self.send_to_log_channel(member.guild, '\n'.join(result_message_list))
async def ping(self, ctx, *, args=None): """ Ping everyone in one of your games with a message **Examples** `[p]ping 100 I won't be able to take my turn today` - Send a message to everyone in game 100 `[p]ping This game is amazing!` - You can omit the game ID if you send the command from a game-specific channel See `[p]help pingall` for a command to ping ALL incomplete games simultaneously. """ usage = (f'**Example usage:** `{ctx.prefix}ping 100 Here\'s a nice note for everyone in game 100.`\n' 'You can also omit the game ID if you use the command from a game-specific channel.') if not args: ctx.command.reset_cooldown(ctx) return await ctx.send(usage) if settings.is_mod(ctx): ctx.command.reset_cooldown(ctx) args = args.split() try: game_id = int(args[0]) message = ' '.join(args[1:]) except ValueError: game_id = None message = ' '.join(args) inferred_game = None if not game_id: try: inferred_game = models.Game.by_channel_id(chan_id=ctx.message.channel.id) except exceptions.TooManyMatches: logger.error(f'More than one game with matching channel {ctx.message.channel.id}') return await ctx.send('Error looking up game based on current channel - please contact the bot owner.') except exceptions.NoMatches: ctx.command.reset_cooldown(ctx) return await ctx.send(f'Game ID was not included. {usage}') logger.debug(f'Inferring game {inferred_game.id} from ping command used in channel {ctx.message.channel.id}') if not message: ctx.command.reset_cooldown(ctx) return await ctx.send(f'Message was not included. {usage}') if ctx.message.attachments: attachment_urls = '\n'.join([attachment.url for attachment in ctx.message.attachments]) message += f'\n{attachment_urls}' message = utilities.escape_role_mentions(message) if inferred_game: game = inferred_game else: game = await PolyGame().convert(ctx, int(game_id), allow_cross_guild=True) if not game.player(discord_id=ctx.author.id) and not settings.is_staff(ctx): ctx.command.reset_cooldown(ctx) return await ctx.send(f'You are not a player in game {game.id}') permitted_channels = settings.guild_setting(game.guild_id, 'bot_channels') permitted_channels_private = [] if settings.guild_setting(game.guild_id, 'game_channel_categories'): if game.game_chan: permitted_channels = [game.game_chan] + permitted_channels if game.smallest_team() > 1: permitted_channels_private = [gs.team_chan for gs in game.gamesides] permitted_channels = permitted_channels_private + permitted_channels # allows ping command to be used in private team channels - only if there is no solo squad in the game which would mean they cant see the message # this also adjusts where the @Mention is placed (sent to all team channels instead of simply in the ctx.channel) elif ctx.channel.id in [gs.team_chan for gs in game.gamesides]: channel_tags = [f'<#{chan_id}>' for chan_id in permitted_channels] ctx.command.reset_cooldown(ctx) return await ctx.send(f'This command cannot be used in this channel because there is at least one solo player without access to a team channel.\n' f'Permitted channels: {" ".join(channel_tags)}') if ctx.channel.id not in permitted_channels and ctx.channel.id not in settings.guild_setting(game.guild_id, 'bot_channels_private'): channel_tags = [f'<#{chan_id}>' for chan_id in permitted_channels] ctx.command.reset_cooldown(ctx) return await ctx.send(f'This command can not be used in this channel. Permitted channels: {" ".join(channel_tags)}') player_mentions = [f'<@{l.player.discord_member.discord_id}>' for l in game.lineup] full_message = f'Message from {ctx.author.mention} (**{ctx.author.name}**) regarding game {game.id} **{game.name}**:\n*{message}*' if ctx.channel.id in permitted_channels_private: logger.debug(f'Ping triggered in private channel {ctx.channel.id}') await game.update_squad_channels(self.bot.guilds, game.guild_id, message=f'{full_message}\n{" ".join(player_mentions)}') else: logger.debug(f'Ping triggered in non-private channel {ctx.channel.id}') await game.update_squad_channels(self.bot.guilds, ctx.guild.id, message=full_message) await ctx.send(f'{full_message}\n{" ".join(player_mentions)}')
async def ping(self, ctx, *, args=''): """ Ping everyone in one of your games with a message **Examples** `[p]ping 100 I won't be able to take my turn today` - Send a message to everyone in game 100 `[p]ping This game is amazing!` - You can omit the game ID if you send the command from a game-specific channel See `[p]help pingall` for a command to ping ALL incomplete games simultaneously. """ usage = ( f'**Example usage:** `{ctx.prefix}ping 100 Here\'s a nice note for everyone in game 100.`\n' 'You can also omit the game ID if you use the command from a game-specific channel.' ) if ctx.message.attachments: attachment_urls = '\n'.join( [attachment.url for attachment in ctx.message.attachments]) args += f'\n{attachment_urls}' if not args: ctx.command.reset_cooldown(ctx) return await ctx.send(usage) if settings.is_mod(ctx.author): ctx.command.reset_cooldown(ctx) args = args.split() try: game_id = int(args[0]) message = ' '.join(args[1:]) except ValueError: game_id = None message = ' '.join(args) # TODO: should prioritize inferred game above an integer. currently something like '$ping 1 city island plz restart' # will try to ping game ID #1 even if done within a game channel inferred_game = None if not game_id: try: inferred_game = models.Game.by_channel_id( chan_id=ctx.message.channel.id) except exceptions.TooManyMatches: logger.error( f'More than one game with matching channel {ctx.message.channel.id}' ) return await ctx.send( 'Error looking up game based on current channel - please contact the bot owner.' ) except exceptions.NoMatches: ctx.command.reset_cooldown(ctx) logger.debug('Could not infer game from current channel.') return await ctx.send(f'Game ID was not included. {usage}') logger.debug( f'Inferring game {inferred_game.id} from ping command used in channel {ctx.message.channel.id}' ) if not message: ctx.command.reset_cooldown(ctx) return await ctx.send(f'Message was not included. {usage}') message = utilities.escape_role_mentions(message) if inferred_game: game = inferred_game else: game = await PolyGame().convert(ctx, int(game_id), allow_cross_guild=True) if not game.player(discord_id=ctx.author.id) and not settings.is_staff( ctx.author): ctx.command.reset_cooldown(ctx) return await ctx.send(f'You are not a player in game {game.id}') permitted_channels = settings.guild_setting(game.guild_id, 'bot_channels').copy() if game.game_chan: permitted_channels.append(game.game_chan) game_player_ids = [ l.player.discord_member.discord_id for l in game.lineup ] game_members = [ctx.guild.get_member(p_id) for p_id in game_player_ids] player_mentions = [f'<@{p_id}>' for p_id in game_player_ids] game_channels = [gs.team_chan for gs in game.gamesides] game_channels = [chan for chan in game_channels if chan] # remove Nones mention_players_in_current_channel = True # False when done from game channel, True otherwise if ctx.channel.id in game_channels and len(game_channels) >= len( game.gamesides): logger.debug( 'Allowing ping since it is within a game channel, and all sides have a game channel' ) mention_players_in_current_channel = False elif settings.is_mod( ctx.author) and len(game_channels) >= len(game.gamesides): logger.debug( 'Allowing ping since it is from a mod and all sides have a game channel' ) mention_players_in_current_channel = False elif None not in game_members and all( ctx.channel.permissions_for(member).read_messages for member in game_members): logger.debug( 'Allowing ping since all members have read access to current channel' ) mention_players_in_current_channel = True elif ctx.channel.id in permitted_channels: logger.debug( 'Allowing ping since it is a bot channel or central game channel' ) mention_players_in_current_channel = True else: logger.debug(f'Not allowing ping in {ctx.channel.id}') if len(game_channels) >= len(game.gamesides): permitted_channels = game_channels + permitted_channels channel_tags = [f'<#{chan_id}>' for chan_id in permitted_channels] ctx.command.reset_cooldown(ctx) if len(game_channels) < len(game.gamesides): error_str = 'Not all sides have access to a private channel. ' else: error_str = '' return await ctx.send( f'This command can not be used in this channel. {error_str}Permitted channels: {" ".join(channel_tags)}' ) full_message = f'Message from **{ctx.author.display_name}** regarding game {game.id} **{game.name}**:\n*{message}*' models.GameLog.write( game_id=game, guild_id=game.guild_id, message= f'{models.GameLog.member_string(ctx.author)} pinged the game with message: *{discord.utils.escape_markdown(message)}*' ) try: if mention_players_in_current_channel: logger.debug( f'Ping triggered in non-private channel {ctx.channel.id}') await game.update_squad_channels(self.bot.guilds, ctx.guild.id, message=full_message, suppress_errors=True) await ctx.send(f'{full_message}\n{" ".join(player_mentions)}') else: logger.debug( f'Ping triggered in private channel {ctx.channel.id}') await game.update_squad_channels(self.bot.guilds, game.guild_id, message=f'{full_message}', suppress_errors=False, include_message_mentions=True) if ctx.channel.id not in game_channels: await ctx.send( f'Sending ping to game channels:\n{full_message}') except exceptions.CheckFailedError as e: channel_tags = [f'<#{chan_id}>' for chan_id in permitted_channels] return await ctx.send( f'{e}\nTry sending `{ctx.prefix}ping` from a public channel that all members can view: {" ".join(channel_tags)}' )
async def gamelog(self, ctx, *, search_term: str = None): """ *Staff*: Lists log entries related to a particular game **Examples** `[p]gamelog 1234` `[p]gamelog Nelluk` `[p]gamelogs` - *Mod only*: List last 50 log messages, regardless of game. """ # TODO: Might have issue with log entries leaking across servers. Could add a guild_id field to the log table and limit # searches to ctx.guild.id. Would need a one-time command to populate guild_id on old entries if ctx.invoked_with == 'gamelog': # look up history of one game if search_term: try: game_id = int(search_term) except (ValueError): game_id = None # search_term is string, used for text search of log contents else: # Numeric search term passed, if its <= 7 chars assume its a game ID and search that way if len(search_term) > 7: # if numeric string > 7 chars passed, assuming its not a game ID but will be used for a text search game_id = None else: return await ctx.send(f'No search term was entered') if game_id: message_list = [f'Listing all entries for game # {game_id}...'] entries = models.GameLog.select().where( (models.GameLog.game_id == game_id) & (models.GameLog.guild_id == ctx.guild.id)).order_by( -models.GameLog.message_ts) for entry in entries: message_list.append( f'`{entry.message_ts.strftime("%Y-%m-%d %H:%M:%S")}` - {entry.message}' ) else: message_list = [ f'Listing the 50 most recent entries matching **{search_term}**...' ] entries = models.GameLog.select().where( (models.GameLog.message.contains(search_term)) & (models.GameLog.guild_id == ctx.guild.id)).order_by( -models.GameLog.message_ts).limit(50) for entry in entries: message_list.append( f'`{entry.message_ts.strftime("%Y-%m-%d %H:%M:%S")}` - {entry.game_id} - {entry.message}' ) elif ctx.invoked_with == 'gamelogs' and settings.is_mod(ctx.author): # List 50 more recent logged actions if search_term and search_term.upper() == 'ALL': message_list = [ f'Listing the 50 most recent log items (across all guilds)...' ] entries = models.GameLog.select().order_by( -models.GameLog.message_ts).limit(50) else: message_list = [f'Listing the 50 most recent log items...'] entries = models.GameLog.select().where( models.GameLog.guild_id == ctx.guild.id).order_by( -models.GameLog.message_ts).limit(50) for entry in entries: message_list.append( f'`{entry.message_ts.strftime("%Y-%m-%d %H:%M:%S")}` - {entry.game_id} - {entry.message}' ) await utilities.buffered_send(destination=ctx, content='\n'.join(message_list))