Example #1
0
class DiscordBot(BaseBot):
    BOT_NAME = 'garyatrics.com'
    VERSION = '0.48.4'
    NEEDED_PERMISSIONS = [
        'add_reactions',
        'read_messages',
        'send_messages',
        'embed_links',
        'attach_files',
        'external_emojis',
        'manage_messages',
        'mention_everyone',
        'read_message_history',
    ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        log.debug(
            f'--------------------------- Starting {self.BOT_NAME} v{self.VERSION} --------------------------'
        )

        self.expander = TeamExpander()
        self.tower_data = TowerOfDoomData(self.my_emojis)
        self.prefix = models.Prefix(CONFIG.get('default_prefix'))
        self.language = models.Language(CONFIG.get('default_language'))
        self.subscriptions = models.Subscriptions()
        self.views = Views(emojis={})
        self.pet_rescues = []
        self.pet_rescue_config: PetRescueConfig = None
        token = CONFIG.get('dbl_token')
        self.dbl_client = None
        self.server_status_cache = {'last_updated': datetime.datetime.min}
        if token:
            self.dbl_client = dbl.DBLClient(self, token)

    async def on_guild_join(self, guild):
        await super().on_guild_join(guild)
        welcome_message = self.views.render_welcome_message(
            self.prefix.get(guild))
        for channel in guild.text_channels:
            if channel.permissions_for(guild.me).send_messages:
                await channel.send(embed=welcome_message)
                return

    async def on_slash_command(self, function, options, message):
        try:
            if 'lang' not in options:
                options['lang'] = self.language.get(message.guild)
            debug(message)
            await function(message=message, **options)
        except discord.HTTPException as e:
            log.debug(f'Could not answer to slash command: {e}')

    async def on_ready(self):
        if not self.bot_connect:
            self.bot_connect = datetime.datetime.now()
            log.debug(f'Connected at {self.bot_connect}.')
        else:
            await self.on_resumed()
        self.invite_url = discord.utils.oauth_url(client_id=self.user.id,
                                                  permissions=self.permissions)
        subscriptions = sum([s.get('pc', True) for s in self.subscriptions])
        log.info(f'{subscriptions} channels subscribed to PC news.')

        game = discord.Game("Gems of War")
        await self.change_presence(status=discord.Status.online, activity=game)

        await self.update_base_emojis()
        self.views.my_emojis = self.my_emojis
        log.info(f'Logged in as {self.user.name}')
        log.info(f'Active in {len(self.guilds)} guilds.')

        self.pet_rescue_config = PetRescueConfig()
        await self.pet_rescue_config.load()
        self.pet_rescues = await PetRescue.load_rescues(self)
        log.debug(f'Loaded {len(self.pet_rescues)} pet rescues after restart.')
        await self.register_slash_commands()

    async def get_function_for_command(self, user_command, user_prefix):
        for command in COMMAND_REGISTRY:
            match = command['pattern'].search(user_command)
            if not match:
                continue
            groups = match.groupdict()

            if groups.get('prefix', user_prefix) == user_prefix:
                return getattr(self, command['function']), groups
        return None, None

    @owner_required
    async def soulforge_preview(self,
                                message,
                                lang,
                                search_term,
                                release_date=None,
                                switch=False,
                                **kwargs):
        async with message.channel.typing():
            start = time.time()
            weapon_data = self.expander.get_soulforge_weapon_image_data(
                search_term, release_date, switch, lang)
            if not weapon_data:
                e = discord.Embed(
                    title=
                    f'Weapon search for `{search_term}` did not yield any result',
                    description=':(',
                    color=self.BLACK)
                return await self.answer(message, e)
            image_data = soulforge_preview.render_all(weapon_data)
            result = discord.File(image_data, f'soulforge_{release_date}.png')
            duration = time.time() - start
            log.debug(f'Soulforge generation took {duration:0.2f} seconds.')
            await message.channel.send(file=result)

    async def campaign(self, message, lang, tier=None, **kwargs):
        campaign_data = self.expander.get_campaign_tasks(lang, tier)
        if not campaign_data['has_content']:
            title = _('[NO_CURRENT_TASK]', lang)
            description = _('[CAMPAIGN_COMING_SOON]', lang)
            e = discord.Embed(title=title,
                              description=description,
                              color=self.WHITE)
            return await self.answer(message, e)

        for category, tasks in campaign_data['campaigns'].items():
            category_lines = [
                f'**{task["title"]}**: {task["name"]}' for task in tasks
            ]
            color = CAMPAIGN_COLORS.get(category, self.WHITE)
            skip_costs = f'{_("[SKIP_TASK]", lang)}: {TASK_SKIP_COSTS.get(category)} {_("[GEMS]", lang)}'
            e = discord.Embed(
                title=f'__**{_(category, lang)}**__ ({skip_costs})',
                description='\n'.join(category_lines),
                color=color)
            if any(['`?`' in line for line in category_lines]):
                e.set_footer(text=f'[?]: {_("[IN_PROGRESS]", lang)}')
            await self.answer(message, e)

    async def adventures(self, message, lang, **kwargs):
        adventures = self.expander.get_adventure_board(lang)
        e = self.views.render_adventure_board(adventures, lang)
        return await self.answer(message, e)

    async def spoilers(self, message, lang, **kwargs):
        _filter = kwargs.get('filter')
        spoilers = self.expander.get_spoilers(lang)
        e = discord.Embed(title='Spoilers', color=self.WHITE)
        troop_title = self.expander.translate_categories(['troop'],
                                                         lang)['troop']
        headers = ['Date', 'Rarity', 'Name (ID)']
        if not _filter or _filter.lower() == 'troop':
            troop_spoilers = [s for s in spoilers if s['type'] == 'troop']
            extra_spacing = 2
            rarity_width = max([len(t['rarity'])
                                for t in troop_spoilers]) + extra_spacing
            header_widths = [12, rarity_width, 5]
            header = ''.join([
                f'{h.ljust(header_widths[i])}' for i, h in enumerate(headers)
            ])
            message_lines = [header]

            for troop in troop_spoilers:
                message_lines.append(f'{troop["date"]}  '
                                     f'{troop["rarity"].ljust(rarity_width)}'
                                     f'{troop["event"]}'
                                     f'{troop["name"]} '
                                     f'({troop["id"]})')

            if len(message_lines) > 1:
                limit = 1024 - len('``````')
                result = self.views.trim_text_to_length(
                    '\n'.join(message_lines), limit)
                e.add_field(name=troop_title,
                            value=f'```{result}```',
                            inline=False)

        categories = ('kingdom', 'pet', 'weapon')
        translated = self.expander.translate_categories(categories, lang)

        for spoil_type in [
                c for c in categories if (not _filter or _filter.lower() == c)
        ]:
            message_lines = ['Date        Name (ID)']
            for spoiler in spoilers:
                if spoiler['type'] == spoil_type:
                    message_lines.append(
                        f'{spoiler["date"]}  {spoiler["name"]} ({spoiler["id"]})'
                    )
            if len(message_lines) > 1:
                result = '\n'.join(
                    self.views.trim_text_lines_to_length(message_lines, 900))
                e.add_field(name=translated[spoil_type],
                            value=f'```{result}```',
                            inline=False)
        await self.answer(message, e)

    async def soulforge(self, message, lang, **kwargs):
        title, craftable_items = self.expander.get_soulforge(lang)
        e = discord.Embed(title=title, color=self.WHITE)
        for category, recipes in craftable_items.items():
            recipes = sorted(recipes,
                             key=operator.itemgetter('rarity_number', 'id'))
            message_lines = '\n'.join(
                [f'{r["name"]} `#{r["id"]}` ({r["rarity"]})' for r in recipes])
            e.add_field(name=category, value=message_lines, inline=False)
        await self.answer(message, e)

    async def about(self, message, lang, **kwargs):
        color = discord.Color.from_rgb(*RARITY_COLORS['Mythic'])
        e = discord.Embed(title=_('[INFO]', lang),
                          description='<https://garyatrics.com/>',
                          color=color)
        e.set_thumbnail(url=self.user.avatar_url)
        version_title = _('[SETTINGS_VERSION_NO]', lang).replace(':', '')
        e.add_field(name=f'__{version_title}__:',
                    value=self.VERSION,
                    inline=False)

        with HumanizeTranslator(LANGUAGE_CODE_MAPPING.get(lang, lang)) as _t:
            offline = humanize.naturaldelta(self.downtimes)
            start_time = humanize.naturaltime(self.bot_start)
        e.add_field(name=f'__{_("[START]", lang)}__:', value=start_time)
        e.add_field(name=f'__{_("[OFF]", lang)}__:', value=offline)

        bot_runtime = datetime.datetime.now() - self.bot_start
        availability = (bot_runtime - self.downtimes) / bot_runtime
        e.add_field(name=f'__{_("[AVAILABLE]", lang)}__:',
                    value=f'{availability:.3%}')

        slash_invite = self.invite_url.replace('scope=bot',
                                               'scope=applications.commands')
        e.add_field(
            name=f'__{_("[INVITE]", lang)}__:',
            value=
            f'[Bot]({self.invite_url}) / [Slash Commands]({slash_invite})',
            inline=False)

        admin_invite = self.invite_url.split(
            'permissions')[0] + 'permissions=8'
        admin_slash_invite = admin_invite.replace(
            'scope=bot', 'scope=applications.commands')
        e.add_field(
            name=f'__{_("[INVITE]", lang)} ({_("[ADMIN]", lang)})__:',
            value=
            f'[Bot]({admin_invite}>) / [Slash Commands]({admin_slash_invite})',
            inline=False)

        my_prefix = self.prefix.get(message.guild)
        e.add_field(name=f'__{_("[HELP]", lang)}__:',
                    value=f'`{my_prefix}help` / `{my_prefix}quickhelp`',
                    inline=False)

        e.add_field(name=f'__{_("[SUPPORT]", lang)}__:',
                    value='<https://discord.gg/XWs7x3cFTU>',
                    inline=False)
        github = self.my_emojis.get('github')
        gold = self.my_emojis.get('gold')
        contribute = f'{gold} <https://www.buymeacoffee.com/garyatrics>\n' \
                     f'{github} <https://github.com/maduck/GoWDiscordTeamBot>'
        e.add_field(name=f'__{_("[CONTRIBUTE]", lang)}__:',
                    value=contribute,
                    inline=False)
        await self.answer(message, e)

    @owner_required
    async def stats(self, message, lang, **kwargs):
        color = discord.Color.from_rgb(*RARITY_COLORS['Mythic'])
        e = discord.Embed(title=_('[PVPSTATS]', lang),
                          description='<https://garyatrics.com/>',
                          color=color)
        collections = [
            f'**{_("[GUILD]", lang)} {_("[AMOUNT]", lang)}**: {len(self.guilds)}',
            f'**{_("[NEWS]", lang)} {_("[CHANNELS]", lang)} (PC)**: {sum([s.get("pc", True) for s in self.subscriptions])}',
            f'**{_("[NEWS]", lang)} {_("[CHANNELS]", lang)} (Switch)**: {sum([s.get("switch", True) for s in self.subscriptions])}',
            f'**{_("[PETRESCUE]", lang)} ({_("[JUST_NOW]", lang)})**: {len(self.pet_rescues)}',
        ]
        e.add_field(name=_("[COLLECTION]", lang), value='\n'.join(collections))

        await self.answer(message, e)

    async def events(self, message, lang, **kwargs):
        events = self.expander.get_events(lang)
        e = self.views.render_events(events, kwargs.get('filter'), lang)
        await self.answer(message, e)

    async def current_event(self, message, lang, **kwargs):
        lang = LANGUAGE_CODE_MAPPING.get(lang, lang)
        current_event = self.expander.get_current_event(lang)
        e = self.views.render_current_event(current_event, lang)
        await self.answer(message, e)

    async def color_kingdoms(self, message, lang, **kwargs):
        kingdoms = self.expander.get_color_kingdoms(lang)
        e = self.views.render_color_kingdoms(kingdoms, lang)
        await self.answer(message, e)

    async def troop_type_kingdoms(self, message, lang, **kwargs):
        kingdoms = self.expander.get_type_kingdoms(lang)
        e = self.views.render_type_kingdoms(kingdoms, lang)
        await self.answer(message, e)

    async def event_kingdoms(self, message, lang, **kwargs):
        events = self.expander.get_event_kingdoms(lang)
        e = self.views.render_event_kingdoms(events)
        await self.answer(message, e)

    async def levels(self, message, lang, **kwargs):
        levels = self.expander.get_levels(lang)
        e = self.views.render_levels(levels)
        await self.answer(message, e)

    async def help(self, message, lang, **kwargs):
        prefix = self.prefix.get(message.guild)
        lang = LANGUAGE_CODE_MAPPING.get(lang, lang)
        e = self.views.render_help(prefix, lang)
        await self.answer(message, e)

    async def show_tower_help(self, message, prefix, lang, **kwargs):
        e = self.views.render_tower_help(prefix, lang)
        await self.answer(message, e)

    async def quickhelp(self, message, lang, **kwargs):
        prefix = self.prefix.get(message.guild)
        e = self.views.render_quickhelp(prefix, lang, LANGUAGES)
        await self.answer(message, e)

    async def on_message(self, message):
        if message.author.bot:
            return

        await self.wait_until_ready()

        user_command = message.content.strip()
        my_prefix = self.prefix.get(message.guild)
        function, params = await self.get_function_for_command(
            user_command, my_prefix)
        if not function:
            return

        params['lang'] = params.get('lang') or self.language.get(message.guild)
        params['lang'] = params['lang'].lower()
        debug(message)
        await function(message=message, **params)

    @guild_required
    @admin_required
    async def change_prefix(self, message, new_prefix, **kwargs):
        my_prefix = self.prefix.get(message.guild)
        if len(new_prefix) != 1:
            e = self.generate_response(
                'Prefix change', self.RED, 'Error',
                f'Your new prefix has to be 1 characters long,'
                f' `{new_prefix}` has {len(new_prefix)}.')
            await self.answer(message, e)
            return
        await self.prefix.set(message.guild, new_prefix)
        e = self.generate_response(
            'Administrative action', self.RED, 'Prefix change',
            f'Prefix was changed from `{my_prefix}` to `{new_prefix}`')
        await self.answer(message, e)
        log.debug(
            f'[{message.guild.name}] Changed prefix from {my_prefix} to {new_prefix}'
        )

    async def handle_search(self,
                            message,
                            search_term,
                            lang,
                            title,
                            shortened=False,
                            formatter='{0[name]} `#{0[id]}`',
                            **kwargs):
        search_function = getattr(self.expander,
                                  'search_{}'.format(title.lower()))
        result = search_function(search_term, lang)
        if not result:
            e = discord.Embed(
                title=
                f'{title} search for `{search_term}` did not yield any result',
                description=':(',
                color=self.BLACK)
        elif len(result) == 1:
            view = getattr(self.views, 'render_{}'.format(title.lower()))
            e = view(result[0], shortened)
        else:
            e = discord.Embed(
                title=
                f'{title} search for `{search_term}` found {len(result)} matches.',
                color=self.WHITE)
            items_found = [formatter.format(item) for item in result]
            chunk_size = 30
            item_chunks = chunks(items_found, chunk_size)
            for i, chunk in enumerate(item_chunks):
                chunk_message = '\n'.join(chunk)
                e.add_field(
                    name=
                    f'results {chunk_size * i + 1} - {chunk_size * i + len(chunk)}',
                    value=chunk_message)
        await self.answer(message, e)

    class_ = partialmethod(handle_search, title='Class')
    kingdom = partialmethod(handle_search, title='Kingdom')
    pet = partialmethod(handle_search, title='Pet')
    weapon = partialmethod(handle_search, title='Weapon')
    affix = partialmethod(
        handle_search,
        title='Affix',
        formatter='{0[name]} ({0[num_weapons]} {0[weapons_title]})')
    troop = partialmethod(handle_search, title='Troop')
    trait = partialmethod(handle_search, title='Trait', formatter='{0[name]}')
    talent = partialmethod(handle_search,
                           title='Talent',
                           formatter='{0[name]}')
    traitstones = partialmethod(handle_search,
                                title='Traitstone',
                                formatter='{0[name]}')

    async def pet_rescue(self,
                         message,
                         search_term,
                         lang,
                         time_left=59,
                         mention='',
                         **kwargs):
        pets = self.expander.pets.search(search_term, lang)
        if len(pets) != 1:
            e = discord.Embed(
                title=
                f'Pet search for `{search_term}` yielded {len(pets)} results.',
                description='Try again with a different search.',
                color=self.BLACK)
            return await self.answer(message, e)
        pet = pets[0]

        rescue = PetRescue(pet, time_left, message, mention, lang, self.answer,
                           self.pet_rescue_config)
        e = self.views.render_pet_rescue(rescue)
        await rescue.create_or_edit_posts(e)
        await rescue.add(self.pet_rescues)

    async def show_pet_rescue_config(self, message, lang, **kwargs):
        config = self.pet_rescue_config.get(message.channel)

        e = self.views.render_pet_rescue_config(config, lang)
        await self.answer(message, e)

    @admin_required
    async def set_pet_rescue_config(self, message, key, value, lang, **kwargs):
        key = key.lower()
        valid_keys = self.pet_rescue_config.get(message.channel).keys()
        if key not in valid_keys:
            answer = f'Error: `{key}` is not a valid setting for pet rescues.\n' \
                     f'Try one of those: `{"`, `".join(valid_keys)}`'
            e = self.generate_response(_('[PETRESCUE]', lang), self.BLACK,
                                       _("[SETTINGS]", lang), answer)
            return await self.answer(message, e)
        guild = message.guild
        channel = message.channel
        on = _('[ON]', lang)
        yes = _('[YES]', lang)
        translated_trues = [on.lower(), yes.lower()]
        await self.pet_rescue_config.update(guild, channel, key, value,
                                            translated_trues)
        await self.show_pet_rescue_config(message, lang)

    async def class_summary(self, message, lang, **kwargs):
        result = self.expander.class_summary(lang)

        table = prettytable.PrettyTable()
        table.field_names = [
            _('[NAME_A_Z]', lang),
            _('[FILTER_TROOPTYPE]', lang),
            _('[FILTER_KINGDOMS]', lang)
        ]
        table.align = 'l'
        table.hrules = prettytable.HEADER
        table.vrules = prettytable.NONE
        [
            table.add_row(
                [_class['name'], _class['type_short'], _class['kingdom']])
            for _class in result
        ]

        e = await self.generate_embed_from_text(table.get_string().split('\n'),
                                                _('[CLASS]', lang),
                                                _('[OVERVIEW]', lang))
        await self.answer(message, e)

    async def kingdom_summary(self, message, lang, **kwargs):
        result = self.expander.kingdom_summary(lang)

        table = prettytable.PrettyTable()
        table.field_names = [
            _('[NAME_A_Z]', lang),
            _('[TROOPS]', lang),
            _('[FACTIONS]', lang),
        ]
        table.align = 'l'
        table.hrules = prettytable.HEADER
        table.vrules = prettytable.NONE
        [
            table.add_row([
                kingdom['name'],
                len(kingdom['troops']), kingdom['linked_kingdom'] or '-'
            ]) for kingdom in result
        ]

        e = await self.generate_embed_from_text(table.get_string().split('\n'),
                                                _('[KINGDOMS]', lang),
                                                _('[OVERVIEW]', lang))
        await self.answer(message, e)

    @staticmethod
    def generate_response(title, color, name, value):
        e = discord.Embed(title=title, color=color)
        e.add_field(name=name, value=value)
        return e

    async def handle_team_code(self,
                               message,
                               lang,
                               team_code,
                               shortened='',
                               **kwargs):
        team = self.expander.get_team_from_message(team_code, lang)
        if not team or not team['troops']:
            log.debug(f'nothing found in message {team_code}.')
            return
        author = message.author.display_name
        author = await pluralize_author(author)
        if kwargs.get('title') is None:
            team_code = None
        e = self.views.render_team(team,
                                   author,
                                   shortened,
                                   team_code=team_code,
                                   title=kwargs.get('title'))
        await self.answer(message, e)
        if team_code:
            await message.channel.send(content=f'[{team_code}]')

    async def waffles(self, message, lang, waffle_no, **kwargs):
        random_title = _('[SPELLEFFECT_CAUSERANDOM]', lang)
        max_waffles = 67
        if waffle_no and waffle_no.isdigit(
        ) and 1 <= int(waffle_no) <= max_waffles:
            waffle_no = int(waffle_no)
            image_no = f'~~{random_title}~~ #{waffle_no}'
        else:
            waffle_no = random.randint(0, max_waffles)
            image_no = f'{random_title} #{waffle_no}'

        title = _('[QUEST9480_OBJ0_MSG]', lang)
        subtitle = _('[HAND_FEED]', lang)

        e = self.generate_response(title, self.WHITE, subtitle, image_no)
        url = f'https://garyatrics.com/images/waffles/{waffle_no:03d}.jpg'
        e.set_image(url=url)
        await self.answer(message, e)

    async def memes(self, message, lang, meme_no, **kwargs):
        base_url = 'https://garyatrics.com/images/memes'
        r = requests.get(f'{base_url}/index.txt')
        available_memes = [m for m in r.text.split('\n') if m]
        random_title = _('[SPELLEFFECT_CAUSERANDOM]', lang)
        if meme_no and 1 <= int(meme_no) <= len(available_memes):
            meme = available_memes[int(meme_no) - 1]
            image_no = f'~~{random_title}~~ meme `#{int(meme_no)}`'
        else:
            meme_no = random.randint(0, len(available_memes) - 1)
            meme = available_memes[meme_no]
            image_no = f'{random_title} meme `#{meme_no}`'

        title = _('[Troop_K02_07_DESC]', lang)
        subtitle = _(f'[FUNNY_LOAD_TEXT_{random.randint(0, 19)}]', lang)
        meme = urllib.parse.quote(meme)
        url = f'{base_url}/{meme}'

        e = self.generate_response(title, self.WHITE, subtitle, image_no)
        e.set_image(url=url)
        await self.answer(message, e)

    async def server_status(self, message, **kwargs):
        if self.server_status_cache['last_updated'] <= datetime.datetime.utcnow(
        ) - datetime.timedelta(seconds=30):
            async with message.channel.typing():
                r = requests.get(
                    'https://status.infinityplustwo.net/status_v2.txt')
                await asyncio.sleep(2)
                status = {'pGameArray': []}
                if r.status_code == 200:
                    status = r.json()
                self.server_status_cache['status'] = status['pGameArray'][:-1]
                self.server_status_cache[
                    'last_updated'] = datetime.datetime.utcnow()
        e = self.views.render_server_status(self.server_status_cache)
        await self.answer(message, e)

    async def show_prefix(self, message, prefix, **kwargs):
        e = self.generate_response('Prefix', self.WHITE,
                                   'The current prefix is', f'`{prefix}`')
        await self.answer(message, e)

    @guild_required
    async def show_tower_config(self, message, prefix, **kwargs):
        e = self.tower_data.format_output_config(prefix=prefix,
                                                 guild=message.guild,
                                                 color=self.WHITE)
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def set_tower_config_option(self, message, option, value, **kwargs):
        old_value, new_value = self.tower_data.set_option(guild=message.guild,
                                                          option=option,
                                                          value=value)

        if old_value is None and new_value is None:
            e = self.generate_response(
                'Administrative action', self.RED, 'Tower change rejected',
                f'Invalid option `{option}` specified.')
            await self.answer(message, e)
            return

        e = self.generate_response(
            'Administrative action', self.RED, 'Tower change accepted',
            f'Option {option} changed from `{old_value}` to `{new_value}`')
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def set_tower_config_alias(self, message, category, field, values,
                                     **kwargs):
        old_values, new_values = self.tower_data.set_alias(guild=message.guild,
                                                           category=category,
                                                           field=field,
                                                           values=values)

        if old_values is None and new_values is None:
            e = self.generate_response('Administrative action', self.RED,
                                       'Tower change rejected',
                                       f'Invalid data specified.')
            await self.answer(message, e)
            return

        e = self.generate_response(
            'Administrative action', self.RED, 'Tower change accepted',
            f'Alias {category}: `{field}` was changed from `{old_values}` to `{new_values}`.'
        )
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def import_tower_from_taran(self, message, map_name, **kwargs):
        e = self.tower_data.download_from_taran(message,
                                                map_name,
                                                version=self.VERSION)
        await self.answer(message, e)

    @guild_required
    async def show_tower_data(self, message, **kwargs):
        _range = kwargs.get('range')
        shortened = kwargs.get('shortened')
        e = self.tower_data.format_output(guild=message.guild,
                                          channel=message.channel,
                                          color=self.WHITE,
                                          prefix=kwargs['prefix'],
                                          _range=_range,
                                          shortened=shortened)
        await self.answer(message, e)

    @guild_required
    async def edit_tower_single(self, message, floor, room, scroll, **kwargs):
        success, response = self.tower_data.edit_floor(message=message,
                                                       floor=floor,
                                                       room=room,
                                                       scroll=scroll)
        if self.tower_data.get(message.guild)['short']:
            return await self.react(message, bool_to_emoticon(success))

        e = self.generate_response('Tower of Doom', self.WHITE,
                                   'Success' if success else 'Failure',
                                   response)
        await self.answer(message, e)

    @guild_required
    async def edit_tower_floor(self,
                               message,
                               floor,
                               scroll_ii,
                               scroll_iii,
                               scroll_iv,
                               scroll_v,
                               scroll_vi=None,
                               **kwargs):

        rooms = ('ii', 'iii', 'iv', 'v', 'vi')
        scrolls = (scroll_ii, scroll_iii, scroll_iv, scroll_v, scroll_vi)

        rooms = [
            self.tower_data.edit_floor(message=message,
                                       floor=floor,
                                       room=room,
                                       scroll=scrolls[room_id])
            for room_id, room in enumerate(rooms[:-1])
        ]
        success = all([r[0] for r in rooms])

        if self.tower_data.get(message.guild)['short']:
            return await self.react(message, bool_to_emoticon(success))

        e = discord.Embed(title='Tower of Doom', color=self.WHITE)
        edit_text = '\n'.join([
            f"{'Success' if room[0] else 'Failure'}: {room[1]}"
            for room in rooms
        ])

        e.add_field(name='Edit Tower (Floor)', value=edit_text)
        await self.answer(message, e)

    async def drop_rates(self, message, lang, **kwargs):
        drop_chances = self.expander.get_drop_chances(lang)
        e = self.views.render_drop_chances(drop_chances, lang)
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def reset_tower_config(self, message, **kwargs):
        self.tower_data.reset_config(message.guild)

        e = self.generate_response('Administrative action', self.RED,
                                   'Success', 'Cleared tower config')
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def clear_tower_data(self, message, prefix, **kwargs):
        self.tower_data.clear_data(message)
        e = self.generate_response(
            'Tower of Doom', self.WHITE, 'Success',
            f'Cleared tower data for #{message.channel.name}')
        await self.answer(message, e)

    @guild_required
    async def show_permissions(self, message, **kwargs):
        channel_permissions = message.channel.permissions_for(message.guild.me)
        permissions = {}
        for permission in self.NEEDED_PERMISSIONS:
            has_permission = getattr(channel_permissions, permission)
            permissions[permission] = '✅' if has_permission else '❌'
        e = self.views.render_permissions(message.channel, permissions)
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def news_subscribe(self, message, platform, **kwargs):
        if not platform:
            platform = CONFIG.get('default_news_platform')
        await self.subscriptions.add(message.guild, message.channel, platform)

        e = self.generate_response(
            'News management', self.WHITE, f'News for {platform.title()}',
            f'Channel {message.channel.name} is now subscribed and will receive future news.'
        )
        await self.answer(message, e)

    async def show_bookmark(self,
                            message,
                            bookmark_id,
                            lang,
                            shortened='',
                            **kwargs):
        bookmark = self.expander.bookmarks.get(bookmark_id)
        if not bookmark:
            e = self.generate_response(
                'Bookmark', self.BLACK, 'Error',
                f'Bookmark id `{bookmark_id}` does not exist.')
            return await self.answer(message, e)
        title = f'Bookmark `{bookmark_id}` by {bookmark["author_name"]}\n{bookmark["description"]}'
        return await self.handle_team_code(message,
                                           lang,
                                           bookmark['team_code'],
                                           title=title,
                                           shortened=shortened)

    async def show_my_bookmarks(self, message, **kwargs):
        bookmarks = self.expander.bookmarks.get_my_bookmarks(message.author.id)
        e = self.views.render_my_bookmarks(bookmarks,
                                           message.author.display_name)
        await self.answer(message, e)

    async def create_bookmark(self,
                              message,
                              description,
                              team_code,
                              lang,
                              shortened='',
                              **kwargs):
        bookmark_id = await self.expander.bookmarks.add(
            message.author.id, message.author.display_name, description,
            team_code)
        return await self.show_bookmark(message, bookmark_id, lang, shortened)

    async def delete_bookmark(self, message, bookmark_id, lang, **kwargs):
        try:
            await self.expander.bookmarks.remove(message.author.id,
                                                 bookmark_id)
            e = self.generate_response(
                'Bookmark', self.WHITE, 'Deletion',
                f'Bookmark `{bookmark_id}` was successfully deleted.')
        except BookmarkError as te:
            e = self.generate_response('Bookmark', self.BLACK,
                                       'There was a problem', str(te))
        await self.answer(message, e)

    async def show_toplist(self, message, toplist_id, lang, **kwargs):
        toplist = self.expander.translate_toplist(toplist_id, lang)
        e = self.views.render_toplist(toplist)
        await self.answer(message, e)

    async def create_toplist(self, message, description, items, lang,
                             **kwargs):
        try:
            toplist_ids = self.expander.get_toplist_troop_ids(items, lang)
            items = ','.join(toplist_ids)
            toplist = await self.expander.create_toplist(
                message,
                description,
                items,
                lang,
                update_id=kwargs.get('bookmark_id'))
            e = self.views.render_toplist(toplist)
        except ToplistError as te:
            e = self.generate_response('Toplist', self.BLACK,
                                       'There was a problem', str(te))
        await self.answer(message, e)

    update_toplist = create_toplist

    async def append_toplist(self, message, toplist_id, items, lang, **kwargs):
        try:
            toplist_ids = self.expander.get_toplist_troop_ids(items, lang)
            items = ','.join(toplist_ids)
            await self.expander.toplists.append(toplist_id, message.author.id,
                                                message.author.display_name,
                                                items)
            toplist = self.expander.translate_toplist(toplist_id, lang)
            e = self.views.render_toplist(toplist)
        except ToplistError as te:
            e = self.generate_response('Toplist', self.BLACK,
                                       'There was a problem', str(te))
        await self.answer(message, e)

    async def delete_toplist(self, message, toplist_id, **kwargs):
        try:
            await self.expander.toplists.remove(message.author.id, toplist_id)
            e = self.generate_response(
                'Toplist', self.WHITE, 'Deletion',
                f'Toplist `{toplist_id}` was successfully deleted.')
        except ToplistError as te:
            e = self.generate_response('Toplist', self.BLACK,
                                       'There was a problem', str(te))
        await self.answer(message, e)

    async def show_my_toplists(self, message, **kwargs):
        toplists = self.expander.toplists.get_my_toplists(message.author.id)
        e = self.views.render_my_toplists(toplists,
                                          message.author.display_name)
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def news_unsubscribe(self, message, **kwargs):
        await self.subscriptions.remove(message.guild, message.channel)

        e = self.generate_response(
            'News management', self.WHITE, f'News for all platforms',
            f'News will *not* be posted into channel {message.channel.name} anymore.'
        )
        await self.answer(message, e)

    @guild_required
    async def news_status(self, message, **kwargs):
        subscribed = self.subscriptions.is_subscribed(message.guild,
                                                      message.channel)
        answer_text = f'Channel {message.channel.name} is *not* subscribed to any news, ' \
                      f'try `{kwargs["prefix"]}news subscribe`.'
        if subscribed:
            platforms = ('PC', 'Switch')
            subscribed_platforms = [
                p for p in platforms if subscribed.get(p.lower())
            ]
            platforms_text = ' and '.join(subscribed_platforms)
            answer_text = f'{platforms_text} news for will be posted into channel {message.channel.name}.'

        e = self.generate_response('News management', self.WHITE, 'Status',
                                   answer_text)
        await self.answer(message, e)

    async def class_level(self, message, **kwargs):
        def xp_for(level):
            return int(1 / 2 * (level**2 + level))

        low, high = sorted([
            int(kwargs.get('from') if kwargs.get('from') else 0),
            int(kwargs.get('to'))
        ])
        low = max(0, low)
        high = min(100, high)

        xp_required = xp_for(high) - xp_for(low)
        speeds = {
            xp_per_min: str(round(xp_required / (60 * xp_per_min)))
            for xp_per_min in (2, 4, 6)
        }
        lang = kwargs.get('lang', 'en')
        e = self.views.render_class_level(low, high, xp_required, speeds, lang)
        await self.answer(message, e)

    async def show_latest_news(self):
        if not self.is_ready():
            return

        with open(NewsDownloader.NEWS_FILENAME) as f:
            articles = json.load(f)
            articles.reverse()
        if articles:
            log.debug(
                f'Distributing {len(articles)} news articles to {len(self.subscriptions)} channels.'
            )
        for article in articles:
            embeds = self.views.render_news(article)
            for subscription in self.subscriptions:
                relevant_news = subscription.get(article['platform'])
                if not relevant_news:
                    continue
                channel = self.get_channel(subscription['channel_id'])
                if not channel:
                    log.debug(
                        f'Subscription {subscription} is broken, skipping.')
                    continue
                log.debug(
                    f'Sending [{article["platform"]}] {article["title"]} to {channel.guild.name}/{channel.name}.'
                )
                if not await self.is_writable(channel):
                    message = 'is not writable' if channel else 'does not exist'
                    log.debug(f'Channel {message}.')
                    continue
                try:
                    for e in embeds:
                        await channel.send(embed=e)
                except Exception as ex:
                    log.error('Could not send out news, exception follows')
                    log.error(repr(e.fields))
                    log.exception(ex)
        with open(NewsDownloader.NEWS_FILENAME, 'w') as f:
            f.write('[]')

    @guild_required
    @admin_required
    async def change_language(self, message, new_language, **kwargs):
        my_language = self.language.get(message.guild)
        if new_language not in LANGUAGES:
            e = discord.Embed(title='Default Language', color=self.BLACK)
            e.add_field(
                name='Error',
                value=f'`{new_language}` is not a valid language code.')
            self.add_available_languages(e)
            await self.answer(message, e)
            return

        await self.language.set(message.guild, new_language)
        e = self.generate_response(
            'Default Language', self.WHITE,
            f'Default language for {message.guild}',
            f'Default language was changed from `{my_language}` to `{new_language}`.'
        )
        await self.answer(message, e)
        log.debug(
            f'[{message.guild.name}] Changed language from {my_language} to {new_language}.'
        )

    @guild_required
    async def show_languages(self, message, **kwargs):
        e = discord.Embed(title='Default Language', color=self.WHITE)
        e.add_field(name=f'Default language for {message.guild}',
                    value=f'`{self.language.get(message.guild)}`',
                    inline=False)

        self.add_available_languages(e)
        await self.answer(message, e)

    async def tools(self, message, **kwargs):
        e = self.views.render_tools()
        await self.answer(message, e)

    @staticmethod
    def add_available_languages(e):
        available_langs = ', '.join(
            [f'`{lang_code}`' for lang_code in LANGUAGES])
        e.add_field(name='Available languages',
                    value=available_langs,
                    inline=False)

    async def register_slash_commands(self):
        log.debug('Deregistering all slash commands...')
        guild_id = CONFIG.get('slash_command_guild_id')
        for command in await get_all_commands(self.user.id,
                                              TOKEN,
                                              guild_id=guild_id):
            await remove_slash_command(self.user.id, TOKEN, guild_id,
                                       command['id'])
        if not CONFIG.get('register_slash_commands'):
            return
        log.debug(f'Registering slash commands...')
        for command in COMMAND_REGISTRY:
            if 'description' not in command:
                continue
            await add_slash_command(self.user.id,
                                    bot_token=TOKEN,
                                    guild_id=guild_id,
                                    cmd_name=command['function'],
                                    description=command['description'],
                                    options=command.get('options', []))
Example #2
0
class DiscordBot(BaseBot):
    BOT_NAME = 'garyatrics.com'
    VERSION = '0.18.1'
    NEEDED_PERMISSIONS = [
        'add_reactions',
        'read_messages',
        'send_messages',
        'embed_links',
        'attach_files',
        'external_emojis',
    ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        log.debug(
            f'--------------------------- Starting {self.BOT_NAME} v{self.VERSION} --------------------------'
        )

        self.expander = TeamExpander()
        self.tower_data = TowerOfDoomData(self.my_emojis)
        self.prefix = models.Prefix(CONFIG.get('default_prefix'))
        self.language = models.Language(CONFIG.get('default_language'))
        self.subscriptions = models.Subscriptions()
        self.views = Views(emojis={})

    async def on_ready(self):
        if not self.bot_connect:
            self.bot_connect = datetime.datetime.now()
            log.debug(f'Connected at {self.bot_connect}.')
        else:
            await self.on_resumed()
        self.invite_url = f'https://discordapp.com/api/oauth2/authorize' \
                          f'?client_id={self.user.id}' \
                          f'&scope=bot' \
                          f'&permissions={self.permissions.value}'
        log.info(f'Logged in as {self.user.name}')

        subscriptions = sum([s.get('pc', True) for s in self.subscriptions])
        log.info(f'{subscriptions} channels subscribed to news.')
        log.info(f'Active in {len(self.guilds)} guilds.')

        game = discord.Game("Gems of War")
        await self.change_presence(status=discord.Status.online, activity=game)
        await self.update_base_emojis()
        self.views.my_emojis = self.my_emojis

    async def get_function_for_command(self, user_command, user_prefix):
        for command in COMMAND_REGISTRY:
            match = command['pattern'].search(user_command)
            if not match:
                continue
            groups = match.groupdict()

            if groups.get('prefix', user_prefix) == user_prefix:
                return getattr(self, command['function']), groups
        return None, None

    async def show_campaign_tasks(self, message, lang, tier, **kwargs):
        campaign_data = self.expander.get_campaign_tasks(lang, tier)
        if not campaign_data['has_content']:
            title = _('[NO_CURRENT_TASK]', lang)
            description = _('[CAMPAIGN_COMING_SOON]', lang)
            e = discord.Embed(title=title,
                              description=description,
                              color=self.WHITE)
            return await self.answer(message, e)

        for category, tasks in campaign_data['campaigns'].items():
            category_lines = [
                f'**{task["title"]}**: {task["name"]}' for task in tasks
            ]
            color = CAMPAIGN_COLORS.get(category, self.WHITE)
            e = discord.Embed(title=f'__**{category}**__',
                              description='\n'.join(category_lines),
                              color=color)
            await self.answer(message, e)

    async def show_spoilers(self, message, lang, _filter, **kwargs):
        spoilers = self.expander.get_spoilers(lang)
        e = discord.Embed(title='Spoilers', color=self.WHITE)
        troop_title = self.expander.translate_categories(['troop'],
                                                         lang)['troop']
        headers = ['Date', 'Rarity', 'Name (ID)']
        if not _filter or _filter.lower() == 'troop':
            troop_spoilers = [s for s in spoilers if s['type'] == 'troop']
            extra_spacing = 2
            rarity_width = max([len(t['rarity'])
                                for t in troop_spoilers]) + extra_spacing
            header_widths = [12, rarity_width, 5]
            header = ''.join([
                f'{h.ljust(header_widths[i])}' for i, h in enumerate(headers)
            ])
            message_lines = [header]

            for troop in troop_spoilers:
                message_lines.append(f'{troop["date"]}  '
                                     f'{troop["rarity"].ljust(rarity_width)}'
                                     f'{troop["event"]}'
                                     f'{troop["name"]} '
                                     f'({troop["id"]})')

            if len(message_lines) > 1:
                limit = 1024 - len('``````')
                result = self.views.trim_text_to_length(
                    '\n'.join(message_lines), limit)
                e.add_field(name=troop_title,
                            value=f'```{result}```',
                            inline=False)

        categories = ('kingdom', 'pet', 'weapon')
        translated = self.expander.translate_categories(categories, lang)

        for spoil_type in [
                c for c in categories if (not _filter or _filter.lower() == c)
        ]:
            message_lines = ['Date        Name (ID)']
            for spoiler in spoilers:
                if spoiler['type'] == spoil_type:
                    message_lines.append(
                        f'{spoiler["date"]}  {spoiler["name"]} ({spoiler["id"]})'
                    )
            if len(message_lines) > 1:
                result = '\n'.join(
                    self.views.trim_text_lines_to_length(message_lines, 900))
                e.add_field(name=translated[spoil_type],
                            value=f'```{result}```',
                            inline=False)
        await self.answer(message, e)

    async def show_soulforge(self, message, lang, **kwargs):
        title, craftable_items = self.expander.get_soulforge(lang)
        e = discord.Embed(title=title, color=self.WHITE)
        for category, recipes in craftable_items.items():
            recipes = sorted(recipes, key=operator.itemgetter('rarity', 'id'))
            message_lines = '\n'.join(
                [f'{r["name"]} `{r["id"]}`' for r in recipes])
            e.add_field(name=category, value=message_lines, inline=False)
        await self.answer(message, e)

    async def show_uptime(self, message, lang, **kwargs):
        e = discord.Embed(title='Uptime', color=self.WHITE)
        lang = LANGUAGE_CODE_MAPPING.get(lang, lang)
        with HumanizeTranslator(lang) as _t:
            uptime = humanize.naturaltime(self.bot_start)
            downtime = humanize.naturaldelta(self.downtimes)
        e.add_field(name='Bot running since', value=uptime, inline=False)
        e.add_field(name='Offline for', value=downtime, inline=False)
        bot_runtime = datetime.datetime.now() - self.bot_start
        availability = (bot_runtime - self.downtimes) / bot_runtime
        e.add_field(name='Availability', value=f'{availability:.3%}')
        await self.answer(message, e)

    async def show_version(self, message, **kwargs):
        e = discord.Embed(title='Version',
                          description=self.VERSION,
                          color=self.WHITE)
        await self.answer(message, e)

    async def show_events(self, message, lang, **kwargs):
        events = self.expander.get_events(lang)
        e = self.views.render_events(events)
        await self.answer(message, e)

    async def show_levels(self, message, lang, **kwargs):
        levels = self.expander.get_levels(lang)
        e = self.views.render_levels(levels)
        await self.answer(message, e)

    async def show_help(self, message, prefix, lang, **kwargs):
        e = self.views.render_help(prefix, lang)
        await self.answer(message, e)

    async def show_tower_help(self, message, prefix, lang, **kwargs):
        help_title, help_text = get_tower_help_text(prefix, lang)

        e = discord.Embed(title=help_title, color=self.WHITE)
        for section, text in help_text.items():
            e.add_field(name=section, value=text, inline=False)
        await self.answer(message, e)

    async def show_quickhelp(self, message, prefix, **kwargs):
        e = discord.Embed(title='Quick Help', color=self.WHITE)
        langs = '|'.join(LANGUAGES)
        e.add_field(
            name='How to read',
            value=
            'Square brackets `[]` show optional parameters, except for troop code.\n'
            'Vertical lines `|` mean "or": this|that.\n'
            f'valid language codes are `{langs}`.')
        e.add_field(name='Searches',
                    value=f'`{prefix}help`\n'
                    f'`{prefix}quickhelp`\n'
                    f'`{prefix}towerhelp`\n'
                    f'`[lang][-][<troopcode>]`\n'
                    f'`[lang][-]{prefix}troop <search>`\n'
                    f'`[lang][-]{prefix}weapon <search>`\n'
                    f'`[lang][-]{prefix}pet <search>`\n'
                    f'`[lang][-]{prefix}class <search>`\n'
                    f'`[lang][-]{prefix}kingdom <search>`\n'
                    f'`[lang][-]{prefix}talent <search>`\n'
                    f'`[lang][-]{prefix}trait <search>`\n'
                    f'`[lang][-]{prefix}traitstone <search>`\n',
                    inline=False)
        e.add_field(
            name='Overviews',
            value=f'`[lang]{prefix}class summary`\n'
            f'`[lang]{prefix}kingdom summary`\n'
            f'`[lang]{prefix}spoiler[s] [pet[s]|troop[s]|weapon[s]|kingdom[s]|event[s]]`\n'
            f'`[lang]{prefix}event[s]`\n'
            f'`[lang]{prefix}campaign [bronze|silver|gold]`\n'
            f'`[lang]{prefix}soulforge`\n'
            f'`[lang]{prefix}levels`\n',
            inline=False)
        e.add_field(name='Admin Commands',
                    value=f'`{prefix}towerconfig`\n'
                    f'`{prefix}towerclear`\n'
                    f'`{prefix}news [[un]subscribe [pc|switch]]`\n'
                    f'`{prefix}prefix [new_prefix]`\n'
                    f'`{prefix}lang[uage[s]] [new_language]`\n',
                    inline=False)
        e.add_field(name='Bot specific',
                    value=f'`{prefix}version`\n'
                    f'`{prefix}uptime`\n'
                    f'`{prefix}invite`\n'
                    f'`{prefix}waffles`\n',
                    inline=False)
        await self.answer(message, e)

    async def on_message(self, message):
        if message.author.bot:
            return

        user_command = message.content.lower().strip()
        my_prefix = self.prefix.get(message.guild)
        function, params = await self.get_function_for_command(
            user_command, my_prefix)
        if not function:
            return

        params['lang'] = params.get('lang') or self.language.get(message.guild)
        debug(message)
        await function(message=message, **params)

    async def show_invite_link(self, message, **kwargs):
        e = self.generate_response('Bot invite link', self.WHITE,
                                   'Feel free to share!', self.invite_url)
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def change_prefix(self, message, new_prefix, **kwargs):
        my_prefix = self.prefix.get(message.guild)
        if len(new_prefix) != 1:
            e = self.generate_response(
                'Prefix change', self.RED, 'Error',
                f'Your new prefix has to be 1 characters long,'
                f' `{new_prefix}` has {len(new_prefix)}.')
            await self.answer(message, e)
            return
        await self.prefix.set(message.guild, new_prefix)
        e = self.generate_response(
            'Administrative action', self.RED, 'Prefix change',
            f'Prefix was changed from `{my_prefix}` to `{new_prefix}`')
        await self.answer(message, e)
        log.debug(
            f'[{message.guild.name}] Changed prefix from {my_prefix} to {new_prefix}'
        )

    async def handle_search(self,
                            message,
                            search_term,
                            lang,
                            title,
                            shortened,
                            formatter='{0[name]} `#{0[id]}`',
                            **kwargs):
        search_function = getattr(self.expander,
                                  'search_{}'.format(title.lower()))
        result = search_function(search_term, lang)
        if not result:
            e = discord.Embed(
                title=
                f'{title} search for `{search_term}` did not yield any result',
                description=':(',
                color=self.BLACK)
        elif len(result) == 1:
            view = getattr(self.views, 'render_{}'.format(title.lower()))
            e = view(result[0], shortened)
        else:
            e = discord.Embed(
                title=
                f'{title} search for `{search_term}` found {len(result)} matches.',
                color=self.WHITE)
            items_found = [formatter.format(item) for item in result]
            chunk_size = 30
            item_chunks = chunks(items_found, chunk_size)
            for i, chunk in enumerate(item_chunks):
                chunk_message = '\n'.join(chunk)
                e.add_field(
                    name=
                    f'results {chunk_size * i + 1} - {chunk_size * i + len(chunk)}',
                    value=chunk_message)
        await self.answer(message, e)

    handle_class_search = partialmethod(handle_search, title='Class')
    handle_kingdom_search = partialmethod(handle_search, title='Kingdom')
    handle_pet_search = partialmethod(handle_search, title='Pet')
    handle_weapon_search = partialmethod(handle_search, title='Weapon')
    handle_affix_search = partialmethod(
        handle_search,
        title='Affix',
        formatter='{0[name]} ({0[num_weapons]} {0[weapons_title]})')
    handle_troop_search = partialmethod(handle_search, title='Troop')
    handle_trait_search = partialmethod(handle_search,
                                        title='Trait',
                                        formatter='{0[name]}')
    handle_talent_search = partialmethod(handle_search,
                                         title='Talent',
                                         formatter='{0[name]}')
    handle_traitstone_search = partialmethod(handle_search,
                                             title='Traitstone',
                                             formatter='{0[name]}')

    async def show_class_summary(self, message, lang, **kwargs):
        result = self.expander.search_class('summary', lang)
        result.sort(key=operator.itemgetter('name'))

        table = prettytable.PrettyTable()
        table.field_names = [
            _('[NAME_A_Z]', lang),
            _('[FILTER_TROOPTYPE]', lang),
            _('[FILTER_KINGDOMS]', lang)
        ]
        table.align = 'l'
        table.hrules = prettytable.HEADER
        table.vrules = prettytable.NONE
        [
            table.add_row(
                [_class['name'], _class['type_short'], _class['kingdom']])
            for _class in result
        ]

        e = await self.generate_embed_from_text(table.get_string().split('\n'),
                                                _('[CLASS]', lang),
                                                _('[OVERVIEW]', lang))
        await self.answer(message, e)

    async def show_kingdom_summary(self, message, lang, **kwargs):
        result = self.expander.search_kingdom('summary', lang)
        result.sort(key=operator.itemgetter('name'))

        table = prettytable.PrettyTable()
        table.field_names = [
            _('[NAME_A_Z]', lang),
            _('[TROOPS]', lang),
            _('[FACTIONS]', lang),
        ]
        table.align = 'l'
        table.hrules = prettytable.HEADER
        table.vrules = prettytable.NONE
        [
            table.add_row([
                kingdom['name'],
                len(kingdom['troops']), kingdom['linked_kingdom'] or '-'
            ]) for kingdom in result
        ]

        e = await self.generate_embed_from_text(table.get_string().split('\n'),
                                                _('[KINGDOMS]', lang),
                                                _('[OVERVIEW]', lang))
        await self.answer(message, e)

    @staticmethod
    def generate_response(title, color, name, value):
        e = discord.Embed(title=title, color=color)
        e.add_field(name=name, value=value)
        return e

    async def handle_team_code(self,
                               message,
                               lang,
                               team_code,
                               shortened='',
                               **kwargs):
        team = self.expander.get_team_from_message(team_code, lang)
        if not team or not team['troops']:
            log.debug(f'nothing found in message {team_code}.')
            return
        author = message.author.display_name
        author = await pluralize_author(author)
        e = self.views.render_team(team, author, shortened)
        await self.answer(message, e)

    async def waffles(self, message, **kwargs):
        waffle_no = random.randint(0, 66)
        e = self.generate_response('Waffles', self.WHITE, 'random waffle',
                                   f'number {waffle_no}')
        e.set_image(
            url=f'https://garyatrics.com/images/waffles/{waffle_no:03d}.jpg')
        await self.answer(message, e)

    async def show_prefix(self, message, prefix, **kwargs):
        e = self.generate_response('Prefix', self.WHITE,
                                   'The current prefix is', f'`{prefix}`')
        await self.answer(message, e)

    @guild_required
    async def show_tower_config(self, message, prefix, **kwargs):
        e = self.tower_data.format_output_config(prefix=prefix,
                                                 guild=message.guild,
                                                 color=self.WHITE)
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def set_tower_config_option(self, message, option, value, **kwargs):
        old_value, new_value = self.tower_data.set_option(guild=message.guild,
                                                          option=option,
                                                          value=value)

        if old_value is None and new_value is None:
            e = self.generate_response(
                'Administrative action', self.RED, 'Tower change rejected',
                f'Invalid option `{option}` specified.')
            await self.answer(message, e)
            return

        e = self.generate_response(
            'Administrative action', self.RED, 'Tower change accepted',
            f'Option {option} changed from `{old_value}` to `{new_value}`')
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def set_tower_config_alias(self, message, category, field, values,
                                     **kwargs):
        old_values, new_values = self.tower_data.set_alias(guild=message.guild,
                                                           category=category,
                                                           field=field,
                                                           values=values)

        if old_values is None and new_values is None:
            e = self.generate_response('Administrative action', self.RED,
                                       'Tower change rejected',
                                       f'Invalid data specified.')
            await self.answer(message, e)
            return

        e = self.generate_response(
            'Administrative action', self.RED, 'Tower change accepted',
            f'Alias {category}: `{field}` was changed from `{old_values}` to `{new_values}`.'
        )
        await self.answer(message, e)

    @guild_required
    async def show_tower_data(self, message, **kwargs):
        _range = kwargs.get('range')
        shortened = kwargs.get('shortened')
        e = self.tower_data.format_output(guild=message.guild,
                                          channel=message.channel,
                                          color=self.WHITE,
                                          prefix=kwargs['prefix'],
                                          _range=_range,
                                          shortened=shortened)
        await self.answer(message, e)

    @guild_required
    async def edit_tower_single(self, message, floor, room, scroll, **kwargs):
        success, response = self.tower_data.edit_floor(message=message,
                                                       floor=floor,
                                                       room=room,
                                                       scroll=scroll)
        if self.tower_data.get(message.guild)['short']:
            return await self.react(message, bool_to_emoticon(success))

        e = self.generate_response('Tower of Doom', self.WHITE,
                                   'Success' if success else 'Failure',
                                   response)
        await self.answer(message, e)

    @guild_required
    async def edit_tower_floor(self,
                               message,
                               floor,
                               scroll_ii,
                               scroll_iii,
                               scroll_iv,
                               scroll_v,
                               scroll_vi=None,
                               **kwargs):

        rooms = ('ii', 'iii', 'iv', 'v', 'vi')
        scrolls = (scroll_ii, scroll_iii, scroll_iv, scroll_v, scroll_vi)

        rooms = [
            self.tower_data.edit_floor(message=message,
                                       floor=floor,
                                       room=room,
                                       scroll=scrolls[room_id])
            for room_id, room in enumerate(rooms[:-1])
        ]
        success = all([r[0] for r in rooms])

        if self.tower_data.get(message.guild)['short']:
            return await self.react(message, bool_to_emoticon(success))

        e = discord.Embed(title='Tower of Doom', color=self.WHITE)
        edit_text = '\n'.join([
            f"{'Success' if room[0] else 'Failure'}: {room[1]}"
            for room in rooms
        ])

        e.add_field(name='Edit Tower (Floor)', value=edit_text)
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def reset_tower_config(self, message, **kwargs):
        self.tower_data.reset_config(message.guild)

        e = self.generate_response('Administrative action', self.RED,
                                   'Success', 'Cleared tower config')
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def clear_tower_data(self, message, prefix, **kwargs):
        self.tower_data.clear_data(message)

        e = self.generate_response(
            'Tower of Doom', self.WHITE, 'Success',
            f'Cleared tower data for #{message.channel.name}')
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def news_subscribe(self, message, platform, **kwargs):
        if not platform:
            platform = CONFIG.get('default_news_platform')
        await self.subscriptions.add(message.guild, message.channel, platform)

        e = self.generate_response(
            'News management', self.WHITE, f'News for {platform.title()}',
            f'Channel {message.channel.name} is now subscribed and will receive future news.'
        )
        await self.answer(message, e)

    @guild_required
    @admin_required
    async def news_unsubscribe(self, message, **kwargs):
        await self.subscriptions.remove(message.guild, message.channel)

        e = self.generate_response(
            'News management', self.WHITE, f'News for all platforms',
            f'News will *not* be posted into channel {message.channel.name} anymore.'
        )
        await self.answer(message, e)

    @guild_required
    async def news_status(self, message, **kwargs):
        subscribed = self.subscriptions.is_subscribed(message.guild,
                                                      message.channel)
        answer_text = f'Channel {message.channel.name} is *not* subscribed to any news, ' \
                      f'try `{kwargs["prefix"]}news subscribe`.'
        if subscribed:
            platforms = ('PC', 'Switch')
            subscribed_platforms = [
                p for p in platforms if subscribed.get(p.lower())
            ]
            platforms_text = ' and '.join(subscribed_platforms)
            answer_text = f'{platforms_text} news for will be posted into channel {message.channel.name}.'

        e = self.generate_response('News management', self.WHITE, 'Status',
                                   answer_text)
        await self.answer(message, e)

    async def show_latest_news(self):
        if not self.is_ready():
            return

        with open(NewsDownloader.NEWS_FILENAME) as f:
            articles = json.load(f)
            articles.reverse()
        if articles:
            log.debug(
                f'Distributing {len(articles)} news articles to {len(self.subscriptions)} channels.'
            )
        for article in articles:
            e = discord.Embed(title='Gems of War news',
                              color=self.WHITE,
                              url=article['url'])
            content = self.views.trim_news_to_length(article['content'],
                                                     article['url'])
            e.add_field(name=article['title'], value=content)
            embeds = [e]
            for image_url in article['images']:
                e = discord.Embed(type='image', color=self.WHITE)
                e.set_image(url=image_url)
                embeds.append(e)

            for subscription in self.subscriptions:
                relevant_news = subscription.get(article['platform'])
                if not relevant_news:
                    continue
                channel = self.get_channel(subscription['channel_id'])
                if not channel:
                    log.debug(
                        f'Subscription {subscription} is broken, skipping.')
                    continue
                log.debug(
                    f'Sending [{article["platform"]}] {article["title"]} to {channel.guild.name}/{channel.name}.'
                )
                if not await self.is_writable(channel):
                    message = 'is not writable' if channel else 'does not exist'
                    log.debug(f'Channel {message}.')
                    continue
                try:
                    for e in embeds:
                        await channel.send(embed=e)
                except Exception as e:
                    log.error('Could not send out news, exception follows')
                    log.exception(e)
        with open(NewsDownloader.NEWS_FILENAME, 'w') as f:
            f.write('[]')

    @guild_required
    @admin_required
    async def change_language(self, message, new_language, **kwargs):
        my_language = self.language.get(message.guild)
        if new_language not in LANGUAGES:
            e = discord.Embed(title='Default Language', color=self.BLACK)
            e.add_field(
                name='Error',
                value=f'`{new_language}` is not a valid language code.')
            self.add_available_languages(e)
            await self.answer(message, e)
            return

        await self.language.set(message.guild, new_language)
        e = self.generate_response(
            'Default Language', self.WHITE,
            f'Default language for {message.guild}',
            f'Default language was changed from `{my_language}` to `{new_language}`.'
        )
        await self.answer(message, e)
        log.debug(
            f'[{message.guild.name}] Changed language from {my_language} to {new_language}.'
        )

    @guild_required
    async def show_languages(self, message, **kwargs):
        e = discord.Embed(title='Default Language', color=self.WHITE)
        e.add_field(name=f'Default language for {message.guild}',
                    value=f'`{self.language.get(message.guild)}`',
                    inline=False)

        self.add_available_languages(e)
        await self.answer(message, e)

    @staticmethod
    def add_available_languages(e):
        available_langs = ', '.join(
            [f'`{lang_code}`' for lang_code in LANGUAGES])
        e.add_field(name='Available languages',
                    value=available_langs,
                    inline=False)