Esempio n. 1
0
    async def task_broadcast_newbie_steam_message(self):
        await self.bot.wait_until_ready()
        while not self.bot.is_closed():
            sleep_cycle = (60 * 60 * 6)
            await asyncio.sleep(10)

            for guild in self.bot.guilds:
                broadcast_channel = guild.get_channel(
                    settings.guild_setting(guild.id, 'steam_game_channel'))
                if not broadcast_channel:
                    continue

                prefix = settings.guild_setting(guild.id, 'command_prefix')
                elo_guide_channel = 533391050014720040

                broadcast_message = (
                    f'To register for ELO leaderboards and matchmaking use the command __`{prefix}steamname Your Steam Name`__'
                )
                broadcast_message += f'\nTo get started with joining an open game, type __`{prefix}games`__ or open your own with __`{prefix}opensteam`__'
                broadcast_message += f'\nFor full information go read <#{elo_guide_channel}>.'

                message = await broadcast_channel.send(
                    broadcast_message, delete_after=(sleep_cycle - 5))
                self.bot.purgable_messages = self.bot.purgable_messages[
                    -20:] + [(guild.id, broadcast_channel.id, message.id)]

            await asyncio.sleep(sleep_cycle)
Esempio n. 2
0
def get_prefix(bot, message):
    # Guild-specific command prefixes
    if message.guild and message.guild.id in settings.config:
        # Current guild is allowed
        set_prefix = settings.guild_setting(message.guild.id, "command_prefix")
        if not set_prefix:
            logger.error(
                f'No prefix found in settings! Guild: {message.guild.id} {message.guild.name}'
            )
            return 'fakeprefix'

        # temp debug log to try to fix NoneType errors related to prefixes
        # logger.debug(f'Found prefix setting {settings.guild_setting(message.guild.id, "command_prefix")} for guild {message.guild.id}')
        return commands.when_mentioned_or(
            settings.guild_setting(message.guild.id,
                                   'command_prefix'))(bot, message)
    else:
        if message.guild:
            logger.error(
                f'Message received not from allowed guild. ID {message.guild.id }'
            )
        # probably a PM
        logger.warn(
            f'returning None prefix for received PM. Author: {message.author.name}'
        )
        return 'fakeprefix'
Esempio n. 3
0
    async def task_confirm_auto(self):
        await self.bot.wait_until_ready()
        sleep_cycle = (60 * 60 * 0.5)  # half hour cycle

        while not self.bot.is_closed():
            await asyncio.sleep(8)
            logger.debug('Task running: task_confirm_auto')

            utilities.connect()
            for guild in self.bot.guilds:
                staff_output_channel = guild.get_channel(
                    settings.guild_setting(guild.id, 'game_request_channel'))
                if not staff_output_channel:
                    logger.debug(
                        f'Could not load game_request_channel for server {guild.id} - skipping'
                    )
                    continue

                prefix = settings.guild_setting(guild.id, 'command_prefix')
                (unconfirmed_count, games_confirmed) = await self.confirm_auto(
                    guild, prefix, staff_output_channel)
                if games_confirmed:
                    await staff_output_channel.send(
                        f'Autoconfirm process complete. {games_confirmed} games auto-confirmed. {unconfirmed_count - games_confirmed} games left unconfirmed.'
                    )

            await asyncio.sleep(sleep_cycle)
Esempio n. 4
0
    async def task_broadcast_newbie_message(self):
        await self.bot.wait_until_ready()
        while not self.bot.is_closed():
            sleep_cycle = (60 * 60 * 3)
            await asyncio.sleep(10)

            for guild in self.bot.guilds:
                broadcast_channels = [guild.get_channel(chan) for chan in settings.guild_setting(guild.id, 'newbie_message_channels')]
                if not broadcast_channels:
                    continue

                prefix = settings.guild_setting(guild.id, 'command_prefix')
                # ranked_chan = settings.guild_setting(guild.id, 'ranked_game_channel')
                # unranked_chan = settings.guild_setting(guild.id, 'unranked_game_channel')
                bot_spam_chan = settings.guild_setting(guild.id, 'bot_channels_strict')[0]
                elo_guide_channel = 533391050014720040

                broadcast_message = (f'To register for ELO leaderboards and matchmaking use the command __`{prefix}setcode YOURCODEHERE`__')
                broadcast_message += f'\nTo get started with joining an open game, go to <#{bot_spam_chan}> and type __`{prefix}games`__'
                broadcast_message += f'\nFor full information go read <#{elo_guide_channel}>.'

                for broadcast_channel in broadcast_channels:
                    if broadcast_channel:
                        await broadcast_channel.send(broadcast_message, delete_after=(sleep_cycle - 5))

            await asyncio.sleep(sleep_cycle)
Esempio n. 5
0
    async def purge_game_channels(self, ctx, *, arg: str = None):
        # TODO: Remove references to deleted channels from database.  i dont -think- having ghost references will cause any usability problems

        if not settings.guild_setting(ctx.guild.id, 'game_channel_categories'):
            return await ctx.send(
                f'Cannot purge - this guild has no `game_channel_categories` setting'
            )

        channels = [
            chan for chan in ctx.guild.channels
            if chan.category_id in settings.guild_setting(
                ctx.guild.id, 'game_channel_categories')
        ]
        await ctx.send(f'Returned {len(channels)} channels')
        old_30d = (datetime.datetime.today() + datetime.timedelta(days=-30))

        for chan in channels:
            if chan.last_message_id:
                try:
                    messages = await chan.history(
                        limit=5, oldest_first=False).flatten()
                except discord.DiscordException as e:
                    logger.error(f'Could not load channel history: {e}')
                    continue
                if len(messages) > 3:
                    logger.debug(
                        f'{chan.name} not eligible for deletion - has at least 4 messages in history'
                    )
                    continue
                if messages[0].created_at > old_30d:
                    logger.debug(
                        f'{chan.name} not eligible for deletion - has a recent message in history'
                    )
                    continue
                logger.warn(
                    f'{chan.name} {chan.id} is eligible for deletion - few messages and no recent messages in history'
                )
                await ctx.send(
                    f'Deleting channel **{chan.name}** - few messages and no recent messages in history'
                )
                try:
                    logger.warn(f'Deleting channel {chan.name}')
                    await chan.delete(
                        reason='Purging game channels with inactive history')
                except discord.DiscordException as e:
                    logger.error(f'Could not delete channel: {e}')

        await ctx.send(f'Channel cleanup complete')
Esempio n. 6
0
    async def task_broadcast_newbie_message(self):
        await self.bot.wait_until_ready()
        while not self.bot.is_closed():
            sleep_cycle = (60 * 60 * 2)
            await asyncio.sleep(10)

            for guild in self.bot.guilds:
                broadcast_channels = [
                    guild.get_channel(chan) for chan in settings.guild_setting(
                        guild.id, 'newbie_message_channels')
                ]
                if not broadcast_channels:
                    continue

                prefix = settings.guild_setting(guild.id, 'command_prefix')
                ranked_chan = settings.guild_setting(guild.id,
                                                     'ranked_game_channel')
                unranked_chan = settings.guild_setting(
                    guild.id, 'unranked_game_channel')
                # bot_spam_chan = settings.guild_setting(guild.id, 'bot_channels_strict')[0]
                elo_guide_channel = 533391050014720040

                broadcast_message = (
                    'I am here to help improve Polytopia multiplayer with matchmaking and leaderboards!\n'
                    f'To **register your code** with me, type __`{prefix}setcode YOURCODEHERE`__'
                )

                if ranked_chan:
                    broadcast_message += (
                        f'\n\nTo find **ranked** games that count for the leaderboard, join <#{ranked_chan}>. '
                        f'Type __`{prefix}opengames`__ to see what games are available to join. '
                        f'To host your own game, try __`{prefix}opengame 1v1`__ to host a 1v1 duel. '
                        'I will alert you once someone else has joined, and then you will add your opponent\'s friend code and create the game in Polytopia.'
                    )

                if unranked_chan:
                    broadcast_message += (
                        f'\n\nYou can also find unranked games - use the same commands as above in <#{unranked_chan}>. '
                        'Start here if you are new to Polytopia multiplayer.')

                broadcast_message += f'\n\nFor full information go read <#{elo_guide_channel}>.'

                for broadcast_channel in broadcast_channels:
                    if broadcast_channel:
                        await broadcast_channel.send(
                            broadcast_message, delete_after=(sleep_cycle - 5))

            await asyncio.sleep(sleep_cycle)
def export_game_data():
    import csv
    filename = 'games_export.csv'
    with open(filename, mode='w') as export_file:
        game_writer = csv.writer(export_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)

        header = ['game_id', 'server', 'game_name', 'game_type', 'game_date', 'rank_unranked', 'completed_timestamp', 'side_id', 'side_name', 'player_name', 'winner', 'player_elo', 'player_elo_change', 'squad_elo', 'squad_elo_change', 'tribe']
        game_writer.writerow(header)

        query = models.Lineup.select().join(models.Game).where(
            (models.Game.is_confirmed == 1)
        ).order_by(models.Lineup.gameside_id).order_by(models.Lineup.game_id)

        for q in query:
            is_winner = True if q.game.winner_id == q.gameside_id else False
            ranked_status = 'Ranked' if q.game.is_ranked else 'Unranked'
            row = [q.game_id, settings.guild_setting(q.game.guild_id, 'display_name'), q.game.name, q.game.size_string(),
                   ranked_status, str(q.game.date), str(q.game.completed_ts), q.gameside_id,
                   q.gameside.name(), q.player.name, is_winner, q.elo_after_game,
                   q.elo_change_player, q.gameside.squad_id if q.gameside.squad else '', q.gameside.squad.elo if q.gameside.squad else '',
                   q.tribe.tribe.name if q.tribe else '']

            game_writer.writerow(row)

    print(f'Game data written to file {filename} in bot.py directory')
Esempio n. 8
0
    async def tribe_emoji(self, ctx, tribe_name: str, emoji):
        """*Mod*: Assign an emoji to a tribe
        The emoji chosen will be used on *all* servers that this bot is on.
        It can only be triggered by an admin on a server that contributes to the Global ELO leaderboard.
        **Example:**
        `[p]tribe_emoji Bardur :new_bardur_emoji:`
        """
        if not settings.guild_setting(
                ctx.guild.id,
                'include_in_global_lb') and ctx.author.id != settings.owner_id:
            return await ctx.send(
                f'This command can only be run in a Global ELO server (ie. PolyChampions or Polytopia Main'
            )

        if len(emoji) != 1 and ('<:' not in emoji):
            return await ctx.send(
                'Valid emoji not detected. Example: `{}tribe_emoji Tribename :my_custom_emoji:`'
                .format(ctx.prefix))

        try:
            tribe = models.Tribe.update_emoji(name=tribe_name, emoji=emoji)
        except exceptions.CheckFailedError as e:
            return await ctx.send(e)

        await ctx.send(
            f'Tribe {tribe.name} updated with new emoji: {tribe.emoji}')
Esempio n. 9
0
    async def send_to_log_channel(self, guild, message):

        logger.debug(f'Sending log message to game_request_channel: {message}')
        staff_output_channel = guild.get_channel(settings.guild_setting(guild.id, 'game_request_channel'))
        if not staff_output_channel:
            logger.warn(f'Could not load game_request_channel for server {guild.id} - skipping')
        else:
            await utilities.buffered_send(destination=staff_output_channel, content=message)
Esempio n. 10
0
def get_prefix(bot, message):
    # Guild-specific command prefixes
    if message.guild and message.guild.id in settings.config:
        # Current guild is allowed
        return commands.when_mentioned_or(
            settings.guild_setting(message.guild.id,
                                   'command_prefix'))(bot, message)
    else:
        if message.guild:
            logging.error(
                f'Message received not from allowed guild. ID {message.guild.id }'
            )
        # probably a PM
        return None
Esempio n. 11
0
    async def undrafted_novas(self, ctx, *, arg=None):
        """Prints list of Novas who meet graduation requirements but have not been drafted
        """
        grad_list = []
        grad_role = discord.utils.get(ctx.guild.roles, name='Free Agent')
        inactive_role = discord.utils.get(ctx.guild.roles,
                                          name=settings.guild_setting(
                                              ctx.guild.id, 'inactive_role'))
        # recruiter_role = discord.utils.get(ctx.guild.roles, name='Team Recruiter')
        if ctx.guild.id == settings.server_ids['test']:
            grad_role = discord.utils.get(ctx.guild.roles, name='Team Leader')

        for member in grad_role.members:
            if inactive_role and inactive_role in member.roles:
                logger.debug(
                    f'Skipping {member.name} since they have Inactive role')
                continue
            try:
                dm = models.DiscordMember.get(discord_id=member.id)
                player = models.Player.get(discord_member=dm,
                                           guild_id=ctx.guild.id)
            except peewee.DoesNotExist:
                logger.debug(f'Player {member.name} not registered.')
                continue

            g_wins, g_losses = dm.get_record()
            wins, losses = player.get_record()
            recent_games = dm.games_played(in_days=14).count()
            all_games = dm.games_played().count()

            message = (
                f'**{player.name}**'
                f'\n\u00A0\u00A0 \u00A0\u00A0 \u00A0\u00A0 {recent_games} games played in last 14 days, {all_games} all-time'
                f'\n\u00A0\u00A0 \u00A0\u00A0 \u00A0\u00A0 ELO:  {dm.elo} *global* / {player.elo} *local*\n'
                f'\u00A0\u00A0 \u00A0\u00A0 \u00A0\u00A0 __W {g_wins} / L {g_losses}__ *global* \u00A0\u00A0 - \u00A0\u00A0 __W {wins} / L {losses}__ *local*\n'
            )

            grad_list.append((message, all_games))

        await ctx.send(
            f'Listing {len(grad_list)} active members with the **{grad_role.name}** role...'
        )

        grad_list.sort(
            key=lambda tup: tup[1],
            reverse=False)  # sort the list ascending by num games played
        for grad in grad_list:
            await ctx.send(grad[0])
Esempio n. 12
0
    async def deactivate_players(self, ctx):
        """*Mods*: Add Inactive role to inactive players
        Apply the 'Inactive' role to any player who has not been activate lately.
        - No games started in 45 days, and does not have a protected role (Team Leadership or Mod roles)
        """

        inactive_role = discord.utils.get(ctx.guild.roles,
                                          name=settings.guild_setting(
                                              ctx.guild.id, 'inactive_role'))
        protected_roles = [
            discord.utils.get(ctx.guild.roles, name='Team Recruiter'),
            discord.utils.get(ctx.guild.roles, name='Mod'),
            discord.utils.get(ctx.guild.roles, name='Team Leader'),
            discord.utils.get(ctx.guild.roles, name='Team Co-Leader')
        ]

        activity_time = (datetime.datetime.now() +
                         datetime.timedelta(days=-45))
        if not inactive_role:
            return await ctx.send('Error loading Inactive role')

        query = models.Player.select(models.DiscordMember.discord_id).join(
            models.Lineup).join(models.Game).join_from(
                models.Player, models.DiscordMember).where(
                    (models.Lineup.player == models.Player.id)
                    & (models.Game.date > activity_time)
                    & (models.Game.guild_id == ctx.guild.id)).group_by(
                        models.DiscordMember.discord_id).having(
                            peewee.fn.COUNT(models.Lineup.id) > 0)

        list_of_active_player_ids = [p[0] for p in query.tuples()]

        defunct_members = []
        async with ctx.typing():
            for member in ctx.guild.members:
                if member.id in list_of_active_player_ids or inactive_role in member.roles:
                    continue
                if any(protected_role in member.roles
                       for protected_role in protected_roles):
                    await ctx.send(
                        f'Skipping inactive member **{member.name}** because they have a protected role.'
                    )
                    logger.debug(
                        f'Skipping inactive member **{member.name}** because they have a protected role.'
                    )
                    continue
                if member.joined_at > activity_time:
                    logger.debug(
                        f'Skipping {member.name} since they joined recently.')
                    continue

                defunct_members.append(member.mention)
                await member.add_roles(
                    inactive_role,
                    reason='Appeared inactive via deactivate_players command')
                logger.debug(f'{member.name} is inactive')

        if not defunct_members:
            return await ctx.send(f'No inactive members found!')

        members_str = ' / '.join(defunct_members)
        await utilities.buffered_send(
            destination=ctx,
            content=
            f'Found {len(defunct_members)} inactive members - *{inactive_role.name}* has been applied to each: {members_str}'
        )
Esempio n. 13
0
    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)}'
            )
Esempio n. 14
0
    async def staffhelp(self, ctx, *, message: str = ''):
        """
        Get staff help on bot usage or game disputes

        The message will be relayed to a staff channel and someone should assist you shortly.
        You can attach screenshots or links to the message.

        **Example:**
        `[p]staffhelp Game 42500 was claimed incorrectly`
        `[p]staffhelp Game 42500 Does this screenshot show a restartable spawn?`
        """

        potential_game_id = re.search(r'\d{4,6}', message)
        game_id_search = potential_game_id[0] if potential_game_id else None
        try:
            related_game = models.Game.by_channel_or_arg(
                chan_id=ctx.channel.id, arg=game_id_search)
            guild_id = related_game.guild_id
            # Send game embed summary if message includes a numeric ID of a game or command is used in a game channel
        except (ValueError, exceptions.MyBaseException):
            related_game = None
            guild_id = ctx.guild.id

        guild = self.bot.get_guild(guild_id)
        if guild:
            channel = guild.get_channel(
                settings.guild_setting(guild_id, 'staff_help_channel'))
        else:
            channel = None

        if not channel:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send(
                'Cannot load staff channel. You will need to ping a staff member.'
            )

        if ctx.message.attachments:
            attachment_urls = '\n'.join(
                [attachment.url for attachment in ctx.message.attachments])
            message += f'\n{attachment_urls}'

        if not message or len(message) < 7:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send(
                f'You must supply a help request, ie: `{ctx.prefix}{ctx.invoked_with} Game 42500 Does this screenshot show a restartable spawn?`'
            )

        helper_role_str = 'server staff'
        helper_role_name = settings.guild_setting(guild_id, 'helper_roles')[0]
        helper_role = discord.utils.get(guild.roles, name=helper_role_name)
        helper_role_str = f'{helper_role.mention}' if helper_role else 'server staff'

        if ctx.channel.guild.id == guild_id:
            chan_str = f'({ctx.channel.name})'
        else:
            chan_str = f'({ctx.channel.name} on __{ctx.guild.name}__)'
        await channel.send(
            f'Attention {helper_role_str} - {ctx.author.mention} ({ctx.author.name}) asked for help from channel <#{ctx.channel.id}> {chan_str}:\n{ctx.message.jump_url}\n{message}'
        )

        if related_game:
            embed, content = related_game.embed(guild=guild, prefix=ctx.prefix)
            await channel.send(embed=embed, content=content)
            game_id = related_game.id
        else:
            game_id = 0

        models.GameLog.write(
            game_id=game_id,
            guild_id=ctx.guild.id,
            message=
            f'{models.GameLog.member_string(ctx.author)} requested staffhelp: *{message}*'
        )
        await ctx.send(
            'Your message has been sent to server staff. Please wait patiently or send additional information on your issue.'
        )
Esempio n. 15
0
    async def league_balance(self, ctx, *, arg=None):
        league_teams = [('Ronin', ['The Ronin', 'The Bandits']),
                        ('Jets', ['The Jets', 'The Cropdusters']),
                        ('Bombers', ['The Bombers', 'The Dynamite']),
                        ('Lightning', ['The Lightning', 'The Pulse']),
                        ('Cosmonauts', ['The Cosmonauts', 'The Space Cadets']),
                        ('Crawfish', ['The Crawfish', 'The Shrimps']),
                        ('Sparkies', ['The Sparkies', 'The Pups']),
                        ('Wildfire', ['The Wildfire', 'The Flames']),
                        ('Mallards', ['The Mallards', 'The Drakes']),
                        ('Plague', ['The Plague', 'The Rats']),
                        ('Dragons', ['The Dragons', 'The Narwhals'])]

        league_balance = []
        indent_str = '\u00A0\u00A0 \u00A0\u00A0 \u00A0\u00A0'
        mia_role = discord.utils.get(ctx.guild.roles,
                                     name=settings.guild_setting(
                                         ctx.guild.id, 'inactive_role'))

        for team, team_roles in league_teams:

            pro_role = discord.utils.get(ctx.guild.roles, name=team_roles[0])
            junior_role = discord.utils.get(ctx.guild.roles,
                                            name=team_roles[1])

            if not pro_role or not junior_role:
                logger.warn(
                    f'Could not load one team role from guild, using args: {team_roles}'
                )
                continue

            try:
                pro_team = models.Team.get_or_except(team_roles[0],
                                                     ctx.guild.id)
                junior_team = models.Team.get_or_except(
                    team_roles[1], ctx.guild.id)
            except exceptions.NoSingleMatch:
                logger.warn(
                    f'Could not load one team from database, using args: {team_roles}'
                )
                continue

            pro_members, junior_members, pro_discord_ids, junior_discord_ids, mia_count = [], [], [], [], 0

            for member in pro_role.members:
                if mia_role in member.roles:
                    mia_count += 1
                else:
                    pro_members.append(member)
                    pro_discord_ids.append(member.id)
            for member in junior_role.members:
                if mia_role in member.roles:
                    mia_count += 1
                else:
                    junior_members.append(member)
                    junior_discord_ids.append(member.id)

            logger.info(team)
            combined_elo, player_games_total = models.Player.weighted_elo_of_player_list(
                list_of_discord_ids=junior_discord_ids + pro_discord_ids,
                guild_id=ctx.guild.id)

            league_balance.append((
                team,
                pro_team,
                junior_team,
                len(pro_members),
                len(junior_members),
                mia_count,
                # pro_elo,
                # junior_elo,
                combined_elo,
                player_games_total))

        league_balance.sort(key=lambda tup: tup[6],
                            reverse=True)  # sort by combined_elo

        embed = discord.Embed(title='PolyChampions League Balance Summary')
        for team in league_balance:
            embed.add_field(
                name=
                (f'{team[1].emoji} {team[0]} ({team[3] + team[4]}) {team[2].emoji}\n{indent_str} \u00A0\u00A0 ActiveELO™: {team[6]}'
                 f'\n{indent_str} \u00A0\u00A0 Recent member-games: {team[7]}'
                 ),
                value=
                (f'-{indent_str}__**{team[1].name}**__ ({team[3]}) **ELO: {team[1].elo}**\n'
                 f'-{indent_str}__**{team[2].name}**__ ({team[4]}) **ELO: {team[2].elo}**\n'
                 ),
                inline=False)

        embed.set_footer(
            text=
            'ActiveELO™ is the mean ELO of members weighted by how many games each member has played in the last 30 days.'
        )

        await ctx.send(embed=embed)
Esempio n. 16
0
    async def ping(self, ctx, game_id: int = None, *, message: str = 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

        See `[p]help pingall` for a command to ping ALL incomplete games simultaneously.

        """
        if not game_id:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send(
                f'Game ID was not included. Example usage: `{ctx.prefix}ping 100 Here\'s a nice note for everyone in game 100.`'
            )

        if not message:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send(
                f'Message was not included. Example usage: `{ctx.prefix}ping 100 Here\'s a nice note`'
            )

        if ctx.message.attachments:
            attachment_urls = '\n'.join(
                [attachment.url for attachment in ctx.message.attachments])
            message += f'\n{attachment_urls}'

        # message = discord.utils.escape_mentions(message)  # to prevent @everyone vulnerability
        message = utilities.escape_role_mentions(message)

        try:
            game = models.Game.get(id=int(game_id))
        except ValueError:
            return await ctx.send(f'Invalid game ID "{game_id}".')
        except peewee.DoesNotExist:
            return await ctx.send(f'Game with ID {game_id} cannot be found.')

        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)}')
Esempio n. 17
0
    async def roleelo(self, ctx, *, arg=None):
        """Prints list of players with a given role and their ELO stats

        Use one of the following options as the first argument to change the sorting:
        **g_elo** - Global ELO (default)
        **elo** - Local ELO
        **games** - Total number of games played
        **recent** - Recent games played (14 days)

        Members with the Inactive role will be skipped.

        This command has some shortcuts:
        `[p]draftable` - List members with the Draftable role
        `[p]freeagents` - List members with the Free Agent role

        **Examples**
        `[p]roleelo novas` - List all members with a role matching 'novas'
        `[p]roleelo elo novas` - List all members with a role matching 'novas', sorted by local elo
        `[p]draftable recent` - List all members with the Draftable role sorted by recent games
        """
        args = arg.split() if arg else []
        usage = (f'**Example usage:** `{ctx.prefix}roleelo Ronin`\n'
                    f'See `{ctx.prefix}help roleelo` for sorting options and more examples.')

        if ctx.invoked_with in ['ble', 'bge']:
            return await ctx.send(f'The `{ctx.prefix}{ctx.invoked_with}` command has been replaced by `{ctx.prefix}roleelo`\n{usage}')

        if args and args[0].upper() == 'G_ELO':
            sort_key = 1
            args = ' '.join(args[1:])
            sort_str = 'Global ELO'
        elif args and args[0].upper() == 'ELO':
            sort_key = 2
            args = ' '.join(args[1:])
            sort_str = 'Local ELO'
        elif args and args[0].upper() == 'GAMES':
            sort_key = 3
            args = ' '.join(args[1:])
            sort_str = 'total games played'
        elif args and args[0].upper() == 'RECENT':
            sort_key = 4
            args = ' '.join(args[1:])
            sort_str = 'recent games played'
        else:
            sort_key = 1  # No argument supplied, use g_elo default
            args = ' '.join(args)
            sort_str = 'Global ELO'

        if ctx.invoked_with == 'draftable':
            role_check_name = draftable_role_name
        elif ctx.invoked_with == 'freeagents':
            role_check_name = free_agent_role_name
        elif ctx.invoked_with == 'roleelo':
            role_check_name = args
            if not args:
                return await ctx.send(f'No role name was supplied.\n{usage}')

        player_list = []
        checked_role = None

        for role in ctx.guild.roles:
            if role_check_name.upper() in role.name.upper():
                checked_role = role

        if not checked_role:
            return await ctx.send(f'Could not load a role from the guild with the name **{role_check_name}**. Make sure the match is exact.')

        inactive_role = discord.utils.get(ctx.guild.roles, name=settings.guild_setting(ctx.guild.id, 'inactive_role'))
        for member in checked_role.members:
            if inactive_role and inactive_role in member.roles:
                logger.debug(f'Skipping {member.name} since they have Inactive role')
                continue

            try:
                dm = models.DiscordMember.get(discord_id=member.id)
                player = models.Player.get(discord_member=dm, guild_id=ctx.guild.id)
            except peewee.DoesNotExist:
                logger.debug(f'Player {member.name} not registered.')
                continue

            g_wins, g_losses = dm.get_record()
            wins, losses = player.get_record()
            recent_games = dm.games_played(in_days=14).count()
            all_games = dm.games_played().count()

            # TODO: Mention players without pinging them once discord.py 1.4 is out https://discordpy.readthedocs.io/en/latest/api.html#discord.TextChannel.send

            message = (f'**{player.name}**'
                f'\n\u00A0\u00A0 \u00A0\u00A0 \u00A0\u00A0 {recent_games} games played in last 14 days, {all_games} all-time'
                f'\n\u00A0\u00A0 \u00A0\u00A0 \u00A0\u00A0 ELO:  {dm.elo} *global* / {player.elo} *local*\n'
                f'\u00A0\u00A0 \u00A0\u00A0 \u00A0\u00A0 __W {g_wins} / L {g_losses}__ *global* \u00A0\u00A0 - \u00A0\u00A0 __W {wins} / L {losses}__ *local*\n')

            player_list.append((message, dm.elo, player.elo, all_games, recent_games))

        await ctx.send(f'Listing {len(player_list)} active members with the **{utilities.escape_role_mentions(checked_role.name)}** role (sorted by {sort_str})...')
        # without the escape then 'everyone.name' still is a mention

        player_list.sort(key=lambda tup: tup[sort_key], reverse=False)     # sort the list by argument supplied

        message = []
        for grad in player_list:
            # await ctx.send(grad[0])
            message.append(grad[0])

        async with ctx.typing():
            await utilities.buffered_send(destination=ctx, content=''.join(message).replace(".", "\u200b "))
Esempio n. 18
0
def get_channel_category(guild,
                         team_name: str = None,
                         using_team_server_flag: bool = False):
    # Returns (DiscordCategory, Bool_IsTeamCategory?) or None
    # Bool_IsTeamCategory? == True if its using a team-specific category, False if using a central games category

    list_of_generic_team_names = [
        a[0] for a in settings.generic_teams_long
    ] + [a[0] for a in settings.generic_teams_short]

    if guild.me.guild_permissions.manage_channels is not True:
        logger.warn(
            'manage_channels permission is false.'
        )  # TODO: change this to see if bot has this perm in the category it selects
        # return None, None

    if team_name:
        team_name_lc = team_name.lower().replace(
            'the', '').strip()  # The Ronin > ronin
        # first seek a channel named something like 'Polychamps Ronin Games', fallback to any category with 'Ronin' in the name.
        for cat in guild.categories:
            if 'polychamp' in cat.name.lower(
            ) and team_name_lc in cat.name.lower():
                logger.debug(
                    f'Using {cat.id} - {cat.name} as a team channel category')
                return cat, True
        for cat in guild.categories:
            if team_name_lc in cat.name.lower():
                logger.debug(
                    f'Using {cat.id} - {cat.name} as a team channel category')
                return cat, True
        if team_name in list_of_generic_team_names and using_team_server_flag:
            for cat in guild.categories:
                if 'polychamp' in cat.name.lower(
                ) and 'other' in cat.name.lower():
                    logger.debug(
                        f'Mixed team - Using {cat.id} - {cat.name} as a team channel category'
                    )
                    return cat, True

    # No team category found - using default category. ie. intermingled home/away games or channel for entire game

    for game_channel_category in settings.guild_setting(
            guild.id, 'game_channel_categories'):

        chan_category = discord.utils.get(guild.categories,
                                          id=int(game_channel_category))
        if chan_category is None:
            logger.warn(
                f'chans_category_id {game_channel_category} was supplied but cannot be loaded'
            )
            continue

        if len(chan_category.channels) >= 50:
            logger.warn(
                f'chans_category_id {game_channel_category} was supplied but is full'
            )
            continue

        logger.debug(
            f'using {chan_category.id} - {chan_category.name} for channel category'
        )
        return chan_category, False  # Successfully use this chan_category

    logger.error('could not successfully load a channel category')
    return None, None
Esempio n. 19
0
def export_game_data_brief(query):
    import csv
    import gzip
    # only supports two-sided games, one winner and one loser

    filename = 'games_export-brief.csv.gz'
    connect()
    with gzip.open(filename, mode='wt') as export_file:
        game_writer = csv.writer(export_file,
                                 delimiter=',',
                                 quotechar='"',
                                 quoting=csv.QUOTE_MINIMAL)

        header = [
            'game_id', 'server', 'season', 'game_name', 'game_type',
            'headline', 'rank_unranked', 'game_date', 'completed_timestamp',
            'winning_side', 'winning_roster', 'winning_side_elo',
            'losing_side', 'losing_roster', 'losing_side_elo'
        ]
        game_writer.writerow(header)

        for game in query:
            if len(game.size) != 2:
                logger.info(
                    f'Skipping export of game {game.id} - this export requires two-sided games.'
                )
                continue
            if not game.is_completed or not game.is_confirmed:
                logger.info(
                    f'Skipping export of game {game.id} - this export completed and confirmed games.'
                )
                continue

            losing_side = game.gamesides[0] if game.gamesides[
                1] == game.winner else game.gamesides[1]
            winning_side = game.winner
            ranked_status = 'Ranked' if game.is_ranked else 'Unranked'

            season_status = game.is_season_game()  # (Season, League) or ()
            season_str = str(season_status[0]) if season_status else ''

            winning_roster = [
                f'{p[0].name} {p[1]} {p[2]}' for p in winning_side.roster()
            ]
            losing_roster = [
                f'{p[0].name} {p[1]} {p[2]}' for p in losing_side.roster()
            ]

            row = [
                game.id,
                settings.guild_setting(game.guild_id, 'display_name'),
                season_str, game.name,
                game.size_string(),
                game.get_gamesides_string(), ranked_status,
                str(game.date),
                str(game.completed_ts),
                winning_side.name(), " / ".join(winning_roster),
                winning_side.elo_strings()[0],
                losing_side.name(), " / ".join(losing_roster),
                losing_side.elo_strings()[0]
            ]

            game_writer.writerow(row)

    print(f'Game data written to file {filename} in bot.py directory')
    return filename
Esempio n. 20
0
    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)}')
Esempio n. 21
0
    async def defeat(self, ctx, *input_args):
        """Enter a game result

        Use /loseto to enter a game where you are the loser, or /defeat to enter the game where you are the winner.

        If a winner is claiming a game, the loser must use /loseto to confirm the result and finalize the ELO changes. Pending games
        will auto-confirm after a period of time.

        Ranked games are assumed to be first-to-3, with possible scores of 3-0, 3-1, or 3-2. Enter the losing player's score as the second argument.
        This can be omitted if you are confirming a loss.

        You can enter a game name, such as the invite code word pair, at the end of the command.

        **Example:**
        `[p]defeat @Nelluk 2` - Claim a 3-2 win against Nelluk
        `[p]loseto @DuffyDood 1` - Acknowledge a 3-1 loss against DuffyDood.
        `[p]loseto @DuffyDood` - Confirm an already-claimed loss
        `[p]defeat @Nelluk 0 Loud Wire` - Enter a 3-0 claim and include a game name
        """
        game_name = None
        args = list(input_args)
        if not args:
            return await ctx.send(
                f'**Usage:** `{ctx.prefix}{ctx.invoked_with} @Opponent [Losing Score] "Optional Game Name"`'
            )

        losing_score = None
        for arg in args:
            try:
                if int(arg) in [0, 1, 2]:
                    losing_score = int(arg)
                    args.remove(arg)
                    break
            except ValueError:
                pass  # arg is non-numeric

        guild_matches = await utilities.get_guild_member(ctx, args[0])
        if len(guild_matches) == 0:
            return await ctx.send(
                f'Could not find any server member matching *{args[0]}*. Try specifying with an @Mention'
            )
        elif len(guild_matches) > 1:
            return await ctx.send(
                f'Found {len(guild_matches)} server members matching *{args[0]}*. Try specifying with an @Mention'
            )
        target_discord_member = guild_matches[0]

        if target_discord_member.id == ctx.author.id:
            return await ctx.send('Stop beating yourself up.')

        if target_discord_member.bot:
            return await ctx.send('Nice try, bot-bully.')

        if len(args) > 1:
            # Combine all args after the first one into a game name
            game_name = ' '.join(args[1:])

        if ctx.invoked_with == 'defeat':
            winning_player, _ = Player.get_or_create(
                discord_id=ctx.author.id,
                defaults={'name': ctx.author.display_name})
            losing_player, _ = Player.get_or_create(
                discord_id=target_discord_member.id,
                defaults={'name': target_discord_member.display_name})
            winning_member = ctx.author
            confirm_win = False
        else:
            # invoked with 'loseto', so swap player targets and confirm the game in one step
            losing_player, _ = Player.get_or_create(
                discord_id=ctx.author.id,
                defaults={'name': ctx.author.display_name})
            winning_player, _ = Player.get_or_create(
                discord_id=target_discord_member.id,
                defaults={'name': target_discord_member.display_name})
            winning_member = ctx.guild.get_member(target_discord_member.id)
            confirm_win = True

        if losing_player.is_banned or winning_player.is_banned:
            return await ctx.send(
                f'Your opponent has the **ELO Banned** role and can not participate in ELO Games.'
            )

        game, created = Game.get_or_create_pending_game(
            winning_player=winning_player,
            losing_player=losing_player,
            name=game_name,
            losing_score=losing_score)
        if not game:
            return await ctx.send(
                f'The loser player\'s score is required to calculate margin of victory. **Example:**: `{ctx.prefix}{ctx.invoked_with} @Nelluk 0` for a 3-0 game. Value must be 0, 1, or 2. '
                'The score can be omitted if you are confirming a pending loss.'
            )

        if not confirm_win:
            if not created:
                return await ctx.send(
                    f'There is already an unconfirmed game with these two opponents. Game {game.id} must be confirmed or deleted before another game is entered.'
                )

            confirm_msg = await ctx.send(
                f'Game {game.id} created and waiting for defeated player <@{losing_player.discord_id}> to confirm loss. React below.'
            )
            confirm_status = await utilities.wait_for_confirmation(
                self.bot,
                ctx,
                game=game,
                losing_member=guild_matches[0],
                message=confirm_msg)

            try:
                game = game.refresh(
                )  # Update game from database in case is_confirmed flag changed during reaction wait time
            except peewee.DoesNotExist:
                return await ctx.send(
                    f'Game {game.id} cannot be found. Most likely it was deleted by a user while waiting for confirmation. No ELO has changed.'
                )

            if confirm_status:
                confirm_win = True
            else:
                if game.is_confirmed:
                    return await ctx.send(
                        f'Game {game.id} is already marked as confirmed.')
                return await ctx.send(
                    f'Confirmation has been *rejected*. Game {game.id} is still pending. Contact your opponent <@{winning_player.discord_id}> or server staff '
                    f'to resolve the dispute. To manually confirm the game please use the command `{ctx.prefix}loseto @{winning_player.name}`'
                )
        if confirm_win:
            # not using an else since confirm_win value can change after it is checked as False
            try:
                winning_player_new_elo, losing_player_new_elo = game.confirm()
            except ValueError:
                return await ctx.send(
                    f'Game {game.id} is already marked as confirmed.')

            rank_winner, _ = winning_player.leaderboard_rank(
                date_cutoff=settings.date_cutoff)
            rank_loser, _ = losing_player.leaderboard_rank(
                date_cutoff=settings.date_cutoff)

            champion_role = discord.utils.get(ctx.guild.roles,
                                              name=settings.guild_setting(
                                                  ctx.guild.id,
                                                  'champion_role_name'))
            hero_role = discord.utils.get(ctx.guild.roles,
                                          name=settings.guild_setting(
                                              ctx.guild.id, 'hero_role_name'))

            if rank_winner == 1 and champion_role and winning_member and champion_role not in winning_member.roles:
                for member in hero_role.members:
                    try:
                        await member.remove_roles(champion_role,
                                                  reason='Dethroned champion')
                    except discord.DiscordException as e:
                        logger.warn(f'Could not remove champion role: {e}')
                        await ctx.send(
                            f'**Warning** Tried to remove champion role from {member.display_name} but got a discord error: {e}'
                        )
                try:
                    await winning_member.add_roles(champion_role,
                                                   reason='New champion')
                except discord.DiscordException as e:
                    logger.warn(f'Could not apply champion role: {e}')
                    await ctx.send(
                        f'**Warning** Tried to apply champion role to {winning_member.display_name} but got a discord error: {e}'
                    )

            if hero_role and winning_player_new_elo > 1200 and hero_role not in winning_member.roles:
                try:
                    await winning_member.add_roles(hero_role,
                                                   reason='New Hero')
                except discord.DiscordException as e:
                    logger.warn(f'Could not apply Hero role: {e}')
                    await ctx.send(
                        f'**Warning** Tried to apply Hero role to {winning_member.display_name} but got a discord error: {e}'
                    )

            return await ctx.send(
                f'Game {game.id} has been confirmed with <@{winning_player.discord_id}> `({winning_player_new_elo} +{game.elo_change_winner} 📈{rank_winner})` '
                f'defeating <@{losing_player.discord_id}> `({losing_player_new_elo} {game.elo_change_loser} 📉{rank_loser})`. Good game! '
            )
Esempio n. 22
0
    async def task_purge_incomplete(self):
        await self.bot.wait_until_ready()
        sleep_cycle = (60 * 60 * 2)  # 2 hour cycle

        while not self.bot.is_closed():
            await asyncio.sleep(20)
            logger.debug('Task running: task_purge_incomplete')

            old_60d = (datetime.date.today() + datetime.timedelta(days=-60))
            old_90d = (datetime.date.today() + datetime.timedelta(days=-90))
            old_120d = (datetime.date.today() + datetime.timedelta(days=-120))
            old_150d = (datetime.date.today() + datetime.timedelta(days=-150))

            for guild in self.bot.guilds:
                staff_output_channel = guild.get_channel(
                    settings.guild_setting(guild.id, 'game_request_channel'))
                if not staff_output_channel:
                    logger.debug(
                        f'Could not load game_request_channel for server {guild.id} {guild.name} - skipping'
                    )
                    continue

                utilities.connect()

                def async_game_search():
                    utilities.connect()
                    query = models.Game.search(status_filter=2,
                                               guild_id=guild.id)
                    query = list(
                        query
                    )  # reversing 'Incomplete' queries so oldest is at top
                    query.reverse()
                    return query

                game_list = await self.bot.loop.run_in_executor(
                    None, async_game_search)

                delete_result = []
                for game in game_list[:500]:
                    game_size = len(game.lineup)
                    rank_str = ' - *Unranked*' if not game.is_ranked else ''
                    if game_size == 2 and game.date < old_60d and not game.is_completed:
                        delete_result.append(
                            f'Deleting incomplete 1v1 game older than 60 days. - {game.get_headline()} - {game.date}{rank_str}'
                        )
                        # await self.bot.loop.run_in_executor(None, game.delete_game)
                        game.delete_game()

                    if game_size == 3 and game.date < old_90d and not game.is_completed:
                        delete_result.append(
                            f'Deleting incomplete 3-player game older than 90 days. - {game.get_headline()} - {game.date}{rank_str}'
                        )
                        await game.delete_game_channels(
                            self.bot.guilds, guild.id)
                        # await self.bot.loop.run_in_executor(None, game.delete_game)
                        game.delete_game()

                    if game_size == 4:
                        if game.date < old_90d and not game.is_completed and not game.is_ranked:
                            delete_result.append(
                                f'Deleting incomplete 4-player game older than 90 days. - {game.get_headline()} - {game.date}{rank_str}'
                            )
                            await game.delete_game_channels(
                                self.bot.guilds, guild.id)
                            await self.bot.loop.run_in_executor(
                                None, game.delete_game)
                            game.delete_game()
                        if game.date < old_120d and not game.is_completed and game.is_ranked:
                            delete_result.append(
                                f'Deleting incomplete ranked 4-player game older than 120 days. - {game.get_headline()} - {game.date}{rank_str}'
                            )
                            await game.delete_game_channels(
                                self.bot.guilds, guild.id)
                            # await self.bot.loop.run_in_executor(None, game.delete_game)
                            game.delete_game()

                    if (
                            game_size == 5 or game_size == 6
                    ) and game.is_ranked and game.date < old_150d and not game.is_completed:
                        # Max out ranked game deletion at game_size==6
                        delete_result.append(
                            f'Deleting incomplete ranked {game_size}-player game older than 150 days. - {game.get_headline()} - {game.date}{rank_str}'
                        )
                        await game.delete_game_channels(
                            self.bot.guilds, guild.id)
                        # await self.bot.loop.run_in_executor(None, game.delete_game)
                        game.delete_game()

                    if game_size >= 5 and not game.is_ranked and game.date < old_120d and not game.is_completed:
                        # no cap on unranked game deletion above 120 days old
                        delete_result.append(
                            f'Deleting incomplete unranked {game_size}-player game older than 120 days. - {game.get_headline()} - {game.date}{rank_str}'
                        )
                        await game.delete_game_channels(
                            self.bot.guilds, guild.id)
                        # await self.bot.loop.run_in_executor(None, game.delete_game)
                        game.delete_game()

                delete_str = '\n'.join(delete_result)
                logger.info(
                    f'Purging incomplete games for guild {guild.name}:\n{delete_str}'
                )
                if len(delete_result):
                    await staff_output_channel.send(
                        f'{delete_str[:1900]}\nFinished - purged {len(delete_result)} games'
                    )

            await asyncio.sleep(sleep_cycle)
Esempio n. 23
0
    async def kick_inactive(self, ctx, *, arg=None):
        """*Mods*: Kick players from server who don't meet activity requirements

        Kicks members from server who either:
        - Joined the server more than a week ago but have not registered a Poly code, or
        - Joined more than a month ago but have played zero ELO games in the last month.

        If a member has any role assigned they will not be kicked, beyond this list of 'kickable' roles:
        Inactive, The Novas, Novas Red, Novas Blue, ELO Rookie, ELO Player

        For example, Someone with role The Novas that has played zero games in the last month will be kicked.
        """

        count = 0
        last_week = (datetime.datetime.now() + datetime.timedelta(days=-7))
        last_month = (datetime.datetime.now() + datetime.timedelta(days=-30))
        inactive_role_name = settings.guild_setting(ctx.guild.id,
                                                    'inactive_role')
        kickable_roles = [
            discord.utils.get(ctx.guild.roles, name=inactive_role_name),
            discord.utils.get(ctx.guild.roles, name='The Novas'),
            discord.utils.get(ctx.guild.roles, name='ELO Rookie'),
            discord.utils.get(ctx.guild.roles, name='ELO Player'),
            discord.utils.get(ctx.guild.roles, name='@everyone'),
            discord.utils.get(ctx.guild.roles, name='Novas Blue'),
            discord.utils.get(ctx.guild.roles, name='Novas Red')
        ]

        async with ctx.typing():
            for member in ctx.guild.members:
                remaining_member_roles = [
                    x for x in member.roles if x not in kickable_roles
                ]
                if len(remaining_member_roles) > 0:
                    continue  # Skip if they have any assigned roles beyond a 'purgable' role
                logger.debug(
                    f'Member {member.name} qualifies based on roles...')
                if member.joined_at > last_week:
                    logger.debug(f'Joined in the previous week. Skipping.')
                    continue

                try:
                    dm = models.DiscordMember.get(discord_id=member.id)
                except peewee.DoesNotExist:
                    logger.debug(
                        f'Player {member.name} has not registered with PolyELO Bot.'
                    )

                    if member.joined_at < last_week:
                        logger.info(
                            f'Joined more than a week ago with no code on file. Kicking from server'
                        )
                        await member.kick(reason='No role, no code on file')
                        count += 1
                    continue
                else:
                    if member.joined_at < last_month:
                        if dm.games_played(in_days=30):
                            logger.debug(
                                'Has played recent ELO game on at least one server. Skipping.'
                            )
                        else:
                            logger.info(
                                f'Joined more than a month ago and has played zero ELO games. Kicking from server'
                            )
                            await member.kick(
                                reason=
                                'No role, no ELO games in at least 30 days.')
                            count += 1

        await ctx.send(
            f'Kicking {count} members without any assigned role and have insufficient ELO history.'
        )
Esempio n. 24
0
    async def grad_novas(self, ctx, *, arg=None):
        """*Staff*: Check Novas for graduation requirements
        Apply the 'Free Agent' role to any Novas who meets requirements:
        - Three ranked team games, and ranked games with members of at least three League teams
        """

        grad_count = 0
        role = discord.utils.get(ctx.guild.roles, name='The Novas')
        grad_role = discord.utils.get(ctx.guild.roles, name='Free Agent')
        recruiter_role = discord.utils.get(ctx.guild.roles,
                                           name='Team Recruiter')
        drafter_role = discord.utils.get(ctx.guild.roles, name='Drafter')
        inactive_role = discord.utils.get(ctx.guild.roles,
                                          name=settings.guild_setting(
                                              ctx.guild.id, 'inactive_role'))
        grad_chan = ctx.guild.get_channel(
            540332800927072267)  # Novas draft talk
        if ctx.guild.id == settings.server_ids['test']:
            role = discord.utils.get(ctx.guild.roles, name='testers')
            grad_role = discord.utils.get(ctx.guild.roles, name='Team Leader')
            recruiter_role = discord.utils.get(ctx.guild.roles, name='role1')
            drafter_role = recruiter_role
            grad_chan = ctx.guild.get_channel(479292913080336397)  # bot spam

        await ctx.send(f'Auto-graduating Novas')
        async with ctx.typing():
            for member in role.members:
                if inactive_role and inactive_role in member.roles:
                    continue
                try:
                    dm = models.DiscordMember.get(discord_id=member.id)
                    player = models.Player.get(discord_member=dm,
                                               guild_id=ctx.guild.id)
                except peewee.DoesNotExist:
                    logger.debug(f'Player {member.name} not registered.')
                    continue
                if grad_role in member.roles:
                    logger.debug(
                        f'Player {player.name} already has the graduate role.')
                    continue
                if player.completed_game_count() < 3:
                    logger.debug(
                        f'Player {player.name} has not completed enough ranked games ({player.completed_game_count()} completed).'
                    )
                    continue
                if player.games_played(in_days=7).count() == 0:
                    logger.debug(
                        f'Player {player.name} has not played in any recent games.'
                    )
                    continue

                team_game_count = 0
                league_teams_represented, qualifying_games = [], []

                for lineup in player.games_played():
                    game = lineup.game
                    if not game.is_ranked or game.largest_team() == 1:
                        continue
                    team_game_count += 1
                    for lineup in game.lineup:
                        if lineup.player.team not in league_teams_represented and lineup.player.team != player.team and lineup.gameside.team != player.team:
                            league_teams_represented.append(lineup.player.team)
                            if str(game.id) not in qualifying_games:
                                qualifying_games.append(str(game.id))
                if team_game_count < 3:
                    logger.debug(
                        f'Player {player.name} has not completed enough team games.'
                    )
                    continue
                if len(league_teams_represented) < 3:
                    logger.debug(
                        f'Player {player.name} has not played with enough league members.'
                    )
                    continue

                wins, losses = dm.get_record()
                logger.debug(
                    f'Player {player.name} meets qualifications: {qualifying_games}'
                )
                grad_count += 1
                await member.add_roles(grad_role)
                await grad_chan.send(
                    f'Player {member.mention} (*Global ELO: {dm.elo} \u00A0\u00A0\u00A0\u00A0W {wins} / L {losses}*) qualifies for graduation on the basis of games: `{" ".join(qualifying_games)}`'
                )
            if grad_count:
                await grad_chan.send(
                    f'{recruiter_role.mention} the above player(s) meet the qualifications for graduation. DM {drafter_role.mention} to express interest.'
                )

            await ctx.send(f'Completed auto-grad: {grad_count} new graduates.')