itsVale/Vale.py

View on GitHub
cogs/general/meta.py

Summary

Maintainability
A
1 hr
Test Coverage
import inspect
import os
import platform
import re

import discord
import psutil
from discord.ext import commands

from utils.colors import random_color
from utils.converter import BotCommand
from utils.paginator import Paginator

# Thanks, Milky

VERSION_HEADER_PATTERN = re.compile(r'^## (\d+\.\d+\.\d+) - (\d{4}-\d{2}-\d{2}|Unreleased)$')
CHANGE_TYPE_PATTERN = re.compile(r'^### (Added|Changed|Deprecated|Removed|Fixed|Security)$')


def _is_bulleted(line):
    return line.startswith(('* ', '- '))


def _changelog_versions(lines):
    version = change_type = release_date = None
    changes = {}
    for line in lines:
        line = line.strip()
        if not line:
            continue

        match = VERSION_HEADER_PATTERN.match(line)
        if match:
            if version:
                yield version, {'release_date': release_date, 'changes': changes.copy()}
            version = match[1]
            release_date = match[2]
            changes.clear()
            continue

        match = CHANGE_TYPE_PATTERN.match(line)
        if match:
            change_type = match[1]
            continue

        if _is_bulleted(line):
            changes.setdefault(change_type, []).append(line)
        else:
            changes[change_type][-1] += ' ' + line.lstrip()
    yield version, {'release_date': release_date, 'changes': changes.copy()}


def _load_changes():
    with open('CHANGELOG.md') as f:
        return dict(_changelog_versions(f.readlines()))


_CHANGELOG = _load_changes()


def _format_line(line):
    if _is_bulleted(line):
        return '\u2022 ' + line[2:]

    return line


def _format_changelog_without_embed(version):
    changes = _CHANGELOG[version]
    nl_join = '\n'.join

    change_lines = '\n\n'.join(
        f'{type_}\n{nl_join(map(_format_line, lines))}'
        for type_, lines in changes['changes'].items()
    )

    return f'Version {version} \u2014 {changes["release_date"]}\n\n{change_lines}'


def _format_changelog_with_embed(version, url):
    changes = _CHANGELOG[version]
    nl_join = '\n'.join

    change_lines = '\n\n'.join(
        f'**__{type_}__**\n{nl_join(map(_format_line, lines))}'
        for type_, lines in changes['changes'].items()
    )

    embed = discord.Embed(description=change_lines)

    name = f'Version {version} \u2014 {changes["release_date"]}'
    embed.set_author(name=name, url=url)
    return embed


class Meta(commands.Cog):
    """Primary a class that provides some meta information about the bot."""

    def __init__(self, bot):
        self.bot = bot

    @property
    def emojis(self):
        return self.bot.bot_emojis

    @staticmethod
    async def _get_commits(repo):
        cmd = r'git show -s HEAD~5..HEAD --format="[{}](https://github.com/' + repo + '/commit/%H) %s (%cr)"'  # 10 commits
        if os.name == 'posix':
            cmd = cmd.format(r'\`%h\`')
        else:
            cmd = cmd.format(r'`%h`')

        try:
            revision = os.popen(cmd).read().strip()
        except OSError:
            revision = 'Couldn\'t fetch commits. Either a memory error or a non-existant repository was provided.'

        return revision

    @staticmethod
    def _get_os_information(cpu, memory):
        return inspect.cleandoc(f"""
        **System information:**

        ```yaml
        :Architecture: -{platform.architecture()[0]}-

        :System:       -{platform.system()}-
        :Node:         -{platform.node()}-
        :Release:      -{platform.release()}-
        :Version:      -{platform.version()}-
        :Machine:      -{platform.version()}-
        :Processor:    -{platform.processor()}-

        :CPU usage:    -{cpu}-
        :Memory usage: -{memory}-
        ```
        """)

    @commands.command(name='about')
    async def _about(self, ctx):
        """Get some cool information about the bot."""

        pages = []

        process = self.bot.process
        cpu = process.cpu_percent() / psutil.cpu_count()
        memory = process.memory_info().rss / float(2 ** 20)
        latency = round(self.bot.latency * 1000, 2)
        shards = len(self.bot.shards)
        version = '.'.join(map(str, ctx.bot.version_info[:3]))
        changelog = (
            f'**{self.emojis.get("announcements")} Recent updates:**\n\n'
            f'```css\n{_format_changelog_without_embed(version)}```'
        )

        commits = await self._get_commits('itsVale/Vale.py')
        system = self._get_os_information(cpu, memory)
        python = platform.python_version()
        postgres = '.'.join(map(str, ctx.db.get_server_version()[:3]))

        pages = [
            (
                f'[`Source Code`]({self.bot.source})\n'
                f'[`Invite me with minimal perms`]({self.bot.minimal_invite_url})\n'
                f'[`Invite me with full perms (Required for certain commands to work)`]({self.bot.invite_url})\n\n'
                f'[__**Need help with something? Check out the support server!**__]({self.bot.support_server})'
            ),
            (
                f'{self.emojis.get("version")} Version: **{version}**\n'
                f'{self.emojis.get("status")} Online for: **{self.bot.uptime}**\n'
                f'{self.emojis.get("signal")} Latency: **{latency} ms**\n'
                f'{self.emojis.get("server")} Guilds: **{self.bot.guild_count}**\n'
                f'{self.emojis.get("cpu")} CPU usage: **{cpu:.2f}%**\n'
                f'{self.emojis.get("memory")} RAM usage: **{memory:.2f} Mb**\n'
                f'{self.emojis.get("shard")} Shards: **{shards}**\n'
                f'{self.emojis.get("python")} Python version: **{python}**\n'
                f'{self.emojis.get("discordpy")} discord.py version: **{discord.__version__}**\n'
                f'{self.emojis.get("postgres")} PostgreSQL version: **{postgres}**\n'
            ),
            system,
            f'**\N{WRITING HAND} Latest commits:**\n\n' + commits,
            changelog
        ]

        paginator = Paginator(ctx, pages, per_page=1, title=f'{self.emojis.get("statistics")} Stats for Vale.py')
        await paginator.interact()

    @commands.command(name='source', aliases=['skid', 'steal'])
    async def _source(self, ctx, *, command: BotCommand = None):
        """Displays the source code for a command.

        If the source code has too many lines, it will send a GitHub URL instead.
        """

        if not command:
            return await ctx.send(self.bot.source)

        paginator = commands.Paginator(prefix='```py')

        source = command.callback.__code__
        lines, firstlineno = inspect.getsourcelines(command.callback)
        if len(lines) < 20:
            for line in lines:
                paginator.add_line(line.rstrip().replace('`', '\u200b'))

            for page in paginator.pages:
                await ctx.send(page)

            return

        lastline = firstlineno + len(lines) - 1
        location = os.path.relpath(source.co_filename).replace('\\', '/')

        url = f'<{self.bot.source}/tree/master/{location}#L{firstlineno}-L{lastline}>'
        await ctx.send(url)

    @commands.command(name='stats')
    async def _stats(self, ctx):
        """Shows some usage statistics about this bot."""

        content = (
            f'__**Usage statistics:**__\n',
            f'Commands invoked in total: **{self.bot.command_counter.get("total")}**',
            f'Commands invoked in this guild: **{self.bot.command_counter.get(str(ctx.guild.id))}**',
            f'Commands invoked in DMs: **{self.bot.command_counter.get("in DMs")}**\n',
            f'And here are the commands, which were invoked successfully in total: **{self.bot.command_counter.get("succeeded")}**\n',
            f'*Only applies to the period from since the bot was restarted for the last time until now.*',
        )

        if ctx.bot_has_embed_links():
            await ctx.send(embed=discord.Embed(description='\n'.join(content), color=random_color()))
        else:
            await ctx.send('\n'.join(content))


def setup(bot):
    bot.add_cog(Meta(bot))