From 248228408df99195dd6c55c98517cf5aaf9abec2 Mon Sep 17 00:00:00 2001 From: Romain J Date: Sun, 5 Jan 2020 01:01:06 +0100 Subject: [PATCH] feat(command|help): finish help command --- cogs/Admin.py | 95 +++++++++------ cogs/Help.py | 159 ++++++++++++++++++++++++-- cogs/Logs.py | 20 ++-- cogs/Polls.py | 8 +- cogs/Useful.py | 96 +++++++++------- cogs/User.py | 14 ++- extras/locales/en/LC_MESSAGES/help.po | 14 ++- extras/locales/fr/LC_MESSAGES/help.po | 14 ++- logs/tuxbot.log | 85 ++++---------- utils/paginator.py | 134 +++++++++++++++------- 10 files changed, 434 insertions(+), 205 deletions(-) diff --git a/cogs/Admin.py b/cogs/Admin.py index d8dc58d..eeaed28 100644 --- a/cogs/Admin.py +++ b/cogs/Admin.py @@ -65,8 +65,9 @@ class Admin(commands.Cog): ########################################################################### - @groupExtra(name='say', invoke_without_command=True, category='admin', - description=Texts('admin_help').get('_say')) + @groupExtra(name='say', invoke_without_command=True, category='text', + description=Texts('admin_help').get('_say'), + short_doc=Texts('admin_help').get('_say__short')) async def _say(self, ctx: commands.Context, *, content: str): if ctx.invoked_subcommand is None: try: @@ -77,7 +78,8 @@ class Admin(commands.Cog): await ctx.send(content) @_say.command(name='edit', - description=Texts('admin_help').get('_say_edit')) + description=Texts('admin_help').get('_say_edit'), + short_doc=Texts('admin_help').get('_say_edit__short')) async def _say_edit(self, ctx: commands.Context, message_id: int, *, content: str): try: @@ -95,7 +97,8 @@ class Admin(commands.Cog): delete_after=5) @_say.command(name='to', - description=Texts('admin_help').get('_say_to')) + description=Texts('admin_help').get('_say_to'), + short_doc=Texts('admin_help').get('_say_to__short')) async def _say_to(self, ctx: commands.Context, channel: Union[discord.TextChannel, discord.User], *, content): @@ -108,8 +111,9 @@ class Admin(commands.Cog): ########################################################################### - @commandExtra(name='ban', category='admin', - description=Texts('admin_help').get('_ban')) + @commandExtra(name='ban', category='administration', + description=Texts('admin_help').get('_ban'), + short_doc=Texts('admin_help').get('_ban__short')) async def _ban(self, ctx: commands.Context, user: discord.Member, *, reason=""): try: @@ -136,8 +140,9 @@ class Admin(commands.Cog): ########################################################################### - @commandExtra(name='kick', category='admin', - description=Texts('admin_help').get('_kick')) + @commandExtra(name='kick', category='administration', + description=Texts('admin_help').get('_kick'), + short_doc=Texts('admin_help').get('_kick__short')) async def _kick(self, ctx: commands.Context, user: discord.Member, *, reason=""): try: @@ -164,8 +169,9 @@ class Admin(commands.Cog): ########################################################################### - @commandExtra(name='clear', category='admin', - description=Texts('admin_help').get('_clear')) + @commandExtra(name='clear', category='text', + description=Texts('admin_help').get('_clear'), + short_doc=Texts('admin_help').get('_clear__short')) async def _clear(self, ctx: commands.Context, count: int): try: await ctx.message.delete() @@ -175,14 +181,16 @@ class Admin(commands.Cog): ########################################################################### - @groupExtra(name='react', category='admin', - description=Texts('admin_help').get('_react')) + @groupExtra(name='react', category='text', + description=Texts('admin_help').get('_react'), + short_doc=Texts('admin_help').get('_react__short')) async def _react(self, ctx: commands.Context): if ctx.invoked_subcommand is None: await ctx.send_help('react') @_react.command(name='add', - description=Texts('admin_help').get('admin._react_add')) + description=Texts('admin_help').get('_react_add'), + short_doc=Texts('admin_help').get('_react_add__short')) async def _react_add(self, ctx: commands.Context, message_id: int, *, emojis: str): emojis: list = emojis.split(' ') @@ -199,7 +207,8 @@ class Admin(commands.Cog): delete_after=5) @_react.command(name='clear', - description=Texts('admin_help').get('_react_remove')) + description=Texts('admin_help').get('_react_remove'), + short_doc=Texts('admin_help').get('_react_remove__short')) async def _react_remove(self, ctx: commands.Context, message_id: int): try: message: discord.Message = await ctx.channel.fetch_message( @@ -213,8 +222,9 @@ class Admin(commands.Cog): ########################################################################### @groupExtra(name='delete', invoke_without_command=True, - category='admin', - description=Texts('admin_help').get('_delete')) + category='text', + description=Texts('admin_help').get('_delete'), + short_doc=Texts('admin_help').get('_delete__short')) async def _delete(self, ctx: commands.Context, message_id: int): try: await ctx.message.delete() @@ -231,7 +241,8 @@ class Admin(commands.Cog): delete_after=5) @_delete.command(name='from', aliases=['to', 'in'], - description=Texts('admin_help').get('_delete_from')) + description=Texts('admin_help').get('_delete_from'), + short_doc=Texts('admin_help').get('_delete_from__short')) async def _delete_from(self, ctx: commands.Context, channel: discord.TextChannel, message_id: int): try: @@ -259,7 +270,8 @@ class Admin(commands.Cog): if member: warns = self.bot.database.session \ .query(WarnModel) \ - .filter(WarnModel.user_id == member.id, WarnModel.created_at > week_ago, + .filter(WarnModel.user_id == member.id, + WarnModel.created_at > week_ago, WarnModel.server_id == ctx.guild.id) \ .order_by(WarnModel.created_at.desc()) else: @@ -287,14 +299,16 @@ class Admin(commands.Cog): reason): now = datetime.datetime.now() - warn = WarnModel(server_id=ctx.guild.id, user_id=member.id, reason=reason, + warn = WarnModel(server_id=ctx.guild.id, user_id=member.id, + reason=reason, created_at=now) self.bot.database.session.add(warn) self.bot.database.session.commit() - @groupExtra(name='warn', aliases=['warns'], category='admin', - description=Texts('admin_help').get('_warn')) + @groupExtra(name='warn', aliases=['warns'], category='administration', + description=Texts('admin_help').get('_warn'), + short_doc=Texts('admin_help').get('_warn__short')) async def _warn(self, ctx: commands.Context): await ctx.trigger_typing() if ctx.invoked_subcommand is None: @@ -307,7 +321,8 @@ class Admin(commands.Cog): await ctx.send(embed=e) @_warn.command(name='add', aliases=['new'], - description=Texts('admin_help').get('_warn_new')) + description=Texts('admin_help').get('_warn_new'), + short_doc=Texts('admin_help').get('_warn_new__short')) async def _warn_new(self, ctx: commands.Context, member: discord.Member, *, reason="N/A"): member = await ctx.guild.fetch_member(member.id) @@ -387,7 +402,8 @@ class Admin(commands.Cog): ) @_warn.command(name='remove', aliases=['revoke', 'del', 'delete'], - description=Texts('admin_help').get('_warn_remove')) + description=Texts('admin_help').get('_warn_remove'), + short_doc=Texts('admin_help').get('_warn_remove__short')) async def _warn_remove(self, ctx: commands.Context, warn_id: int): warn = self.bot.database.session \ .query(WarnModel) \ @@ -400,7 +416,8 @@ class Admin(commands.Cog): f" {Texts('admin', ctx).get('successfully removed')}") @_warn.command(name='show', aliases=['list', 'all'], - description=Texts('admin_help').get('_warn_show')) + description=Texts('admin_help').get('_warn_show'), + short_doc=Texts('admin_help').get('_warn_show__short')) async def _warn_show(self, ctx: commands.Context, member: discord.Member): warns_list, warns = await self.get_warn(ctx, member) @@ -412,7 +429,8 @@ class Admin(commands.Cog): await ctx.send(embed=e) @_warn.command(name='edit', aliases=['change', 'modify'], - description=Texts('admin_help').get('_warn_edit')) + description=Texts('admin_help').get('_warn_edit'), + short_doc=Texts('admin_help').get('_warn_edit__short')) async def _warn_edit(self, ctx: commands.Context, warn_id: int, *, reason): warn = self.bot.database.session \ .query(WarnModel) \ @@ -428,8 +446,9 @@ class Admin(commands.Cog): ########################################################################### @commandExtra(name='language', aliases=['lang', 'langue', 'langage'], - category='admin', - description=Texts('admin_help').get('_language')) + category='server', + description=Texts('admin_help').get('_language'), + short_doc=Texts('admin_help').get('_language__short')) async def _language(self, ctx: commands.Context, locale: str): available = self.bot.database.session \ .query(LangModel.value) \ @@ -450,7 +469,8 @@ class Admin(commands.Cog): current.value = locale.lower() self.bot.database.session.commit() else: - new_row = LangModel(key=str(ctx.guild.id), value=locale.lower()) + new_row = LangModel(key=str(ctx.guild.id), + value=locale.lower()) self.bot.database.session.add(new_row) self.bot.database.session.commit() @@ -459,14 +479,16 @@ class Admin(commands.Cog): ########################################################################### - @groupExtra(name='prefix', aliases=['prefixes'], category='admin', - description=Texts('admin_help').get('_prefix')) + @groupExtra(name='prefix', aliases=['prefixes'], category='server', + description=Texts('admin_help').get('_prefix'), + short_doc=Texts('admin_help').get('_prefix_short')) async def _prefix(self, ctx: commands.Context): if ctx.invoked_subcommand is None: await ctx.send_help('prefix') @_prefix.command(name='add', aliases=['set', 'new'], - description=Texts('admin_help').get('_prefix_add')) + description=Texts('admin_help').get('_prefix_add'), + short_doc=Texts('admin_help').get('_prefix_add__short')) async def _prefix_add(self, ctx: commands.Context, prefix: str): if str(ctx.guild.id) in self.bot.prefixes: prefixes = self.bot.prefixes.get( @@ -500,7 +522,9 @@ class Admin(commands.Cog): ) @_prefix.command(name='remove', aliases=['drop', 'del', 'delete'], - description=Texts('admin_help').get('_prefix_remove')) + description=Texts('admin_help').get('_prefix_remove'), + short_doc=Texts('admin_help').get( + '_prefix_remove__short')) async def _prefix_remove(self, ctx: commands.Context, prefix: str): if str(ctx.guild.id) in self.bot.prefixes: prefixes = self.bot.prefixes.get( @@ -529,7 +553,8 @@ class Admin(commands.Cog): ) @_prefix.command(name='list', aliases=['show', 'all'], - description=Texts('admin_help').get('_prefix_list')) + description=Texts('admin_help').get('_prefix_list'), + short_doc=Texts('admin_help').get('_prefix_list__short')) async def _prefix_list(self, ctx: commands.Context): extras = ['.'] if ctx.message.guild is not None: @@ -546,10 +571,10 @@ class Admin(commands.Cog): prefixes.extend(extras) if len(prefixes) <= 1: - text = Texts('admin', ctx)\ + text = Texts('admin', ctx) \ .get('The only prefix for this guild is :\n') else: - text = Texts('admin', ctx)\ + text = Texts('admin', ctx) \ .get('Available prefixes for this guild are :\n') await ctx.send(text + "\n • ".join(prefixes)) diff --git a/cogs/Help.py b/cogs/Help.py index 64ba6a9..152d6a3 100644 --- a/cogs/Help.py +++ b/cogs/Help.py @@ -4,9 +4,11 @@ import logging import discord from discord.ext import commands +from discord import utils from bot import TuxBot from utils import Texts +from utils.paginator import FieldPages log = logging.getLogger(__name__) @@ -14,8 +16,46 @@ log = logging.getLogger(__name__) class HelpCommand(commands.HelpCommand): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.ignore_cogs = ["Monitoring", "Help", "Logs"] - self.owner_cogs = [] + self.ignore_cogs = ["Monitoring", "Help", "Jishaku"] + self.owner_cogs = ["Admin"] + + def common_command_formatting(self, emb, command): + prefix = self.context.prefix if str(self.context.bot.user.id) in self.context.prefix else f"@{self.context.bot.user.name}" + + emb.title = self.get_command_signature(command) + + emb.description = command.description + usage = command.description + "todo: usage" + + try: + usg = command.description + "todo: usage" + emb.add_field( + name=usage, + value=f"{prefix}{command.qualified_name} " + usg + ) + except KeyError: + emb.add_field( + name=usage, + value=f"{prefix}{command.qualified_name}" + ) + + aliases = "`" + '`, `'.join(command.aliases) + "`" + if aliases == "``": + aliases = Texts( + 'help', self.context + ).get( + 'command_help.no_aliases' + ) + emb.add_field( + name=Texts( + 'help', self.context + ).get( + 'command_help.aliases' + ), + value=aliases + ) + + return emb async def send_bot_help(self, mapping): owners = self.context.bot.owners @@ -23,9 +63,10 @@ class HelpCommand(commands.HelpCommand): f"{owner.name}#{owner.discriminator}" for owner in owners ] + prefix = self.context.prefix if str(self.context.bot.user.id) not in self.context.prefix else f"@{self.context.bot.user.name} " e = discord.Embed( - color=discord.colour.Color.blue(), + color=discord.Color.blue(), description=Texts( 'help', self.context ).get( @@ -34,22 +75,28 @@ class HelpCommand(commands.HelpCommand): ', '.join(owners_name[:-1]) + ' & ' + owners_name[-1] ) ) - e.set_author( icon_url=self.context.author.avatar_url_as(format='png'), name=self.context.author ) + e.set_footer( + text=Texts( + 'help', self.context + ).get( + 'main_page.footer' + ).format( + prefix + ) + ) cogs = "" for extension in self.context.bot.cogs.values(): if self.context.author not in owners \ - and extension.qualified_name in self.owner_cogs: + and extension.__class__.__name__ in self.owner_cogs: continue - if self.context.author in owners \ - and extension.qualified_name in self.ignore_cogs: - continue - if extension.qualified_name == "Jishaku": + if extension.__class__.__name__ in self.ignore_cogs: continue + cogs += f"• {extension.icon} **{extension.qualified_name}**\n" e.add_field( @@ -63,6 +110,100 @@ class HelpCommand(commands.HelpCommand): await self.context.send(embed=e) + async def send_cog_help(self, cog): + pages = {} + prefix = self.context.prefix if str(self.context.bot.user.id) in self.context.prefix else f"@{self.context.bot.user.name}" + + if cog.__class__.__name__ in self.owner_cogs \ + and self.context.author not in self.context.bot.owners: + return self.command_not_found(cog.qualified_name) + + for cmd in cog.get_commands(): + if self.context.author not in self.context.bot.owners \ + and (cmd.hidden or cmd.category == "Hidden"): + continue + + if cmd.category not in pages: + pages[cmd.category] = "```asciidoc\n" + + pages[cmd.category] \ + += f"{cmd.name}" \ + + ' ' * int(17 - len(cmd.name)) \ + + f":: {cmd.short_doc}\n" + + if isinstance(cmd, commands.Group): + for group_command in cmd.commands: + pages[cmd.category] \ + += f"━ {group_command.name}" \ + + ' ' * int(15 - len(group_command.name)) \ + + f":: {cmd.short_doc}\n" + for e in pages: + pages[e] += "```" + formatted = [] + for name, cont in pages.items(): + formatted.append((name, cont)) + footer_text = Texts('help', self.context) \ + .get('main_page.footer') \ + .format(prefix) + + pages = FieldPages( + self.context, + embed_color=discord.Color.blue(), + entries=formatted, + title=cog.qualified_name.upper(), + thumbnail=cog.big_icon, + footericon=self.context.bot.user.avatar_url, + footertext=footer_text, + per_page=1 + ) + await pages.paginate() + + async def send_group_help(self, group): + if group.cog_name in self.ignore_cogs: + return await self.send_error_message( + self.command_not_found(group.name) + ) + + formatted = self.common_command_formatting( + discord.Embed(color=discord.Color.blue()), + group + ) + sub_cmd_list = "" + for group_command in group.commands: + sub_cmd_list += f"└> **{group_command.name}** - {group_command.description}\n" + subcommands = Texts( + 'help', self.context + ).get( + 'command_help.subcommands' + ) + + formatted.add_field(name=subcommands, value=sub_cmd_list, inline=False) + await self.context.send(embed=formatted) + + async def send_command_help(self, command): + if isinstance(command, commands.Group): + return await self.send_group_help(command) + + if command.cog_name in self.ignore_cogs: + return await self.send_error_message( + self.command_not_found(command.name)) + + formatted = self.common_command_formatting( + discord.Embed(color=discord.Color.blue()), + command + ) + + await self.context.send(embed=formatted) + + def command_not_found(self, command): + return Texts( + 'help', self.context + ).get( + 'main_page.not_found' + ).format( + command + ) + class Help(commands.Cog): def __init__(self, bot: TuxBot): diff --git a/cogs/Logs.py b/cogs/Logs.py index 2a551f2..64577af 100644 --- a/cogs/Logs.py +++ b/cogs/Logs.py @@ -1,7 +1,7 @@ """ Based on https://github.com/Rapptz/RoboDanny/blob/3d94e89ef27f702a5f57f432a9131bdfb60bb3ec/cogs/stats.py -Rewrite by Romain J. +Adapted by Romain J. """ @@ -52,6 +52,9 @@ class Logs(commands.Cog): self._resumes = [] self._identifies = defaultdict(list) + self.icon = ":newspaper:" + self.big_icon = "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/twitter/233/newspaper_1f4f0.png" + def _clear_gateway_data(self): one_week_ago = datetime.datetime.utcnow() - datetime.timedelta(days=7) to_remove = [ @@ -240,8 +243,9 @@ class Logs(commands.Cog): msg = f'{emoji} `[{dt:%Y-%m-%d %H:%M:%S}] {record.message}`' await self.webhook.send(msg) - @commandExtra(name='commandstats', hidden=True, category='logs', - description=Texts('logs_help').get('_commandstats')) + @commandExtra(name='commandstats', hidden=True, category='misc', + description=Texts('logs_help').get('_commandstats'), + short_doc=Texts('logs_help').get('_commandstats__short')) @commands.is_owner() async def _commandstats(self, ctx, limit=20): counter = self.bot.command_stats @@ -256,8 +260,9 @@ class Logs(commands.Cog): await ctx.send(f'```\n{output}\n```') - @commandExtra(name='socketstats', hidden=True, category='logs', - description=Texts('logs_help').get('_socketstats')) + @commandExtra(name='socketstats', hidden=True, category='misc', + description=Texts('logs_help').get('_socketstats'), + short_doc=Texts('logs_help').get('_socketstats__short')) @commands.is_owner() async def _socketstats(self, ctx): delta = datetime.datetime.utcnow() - self.bot.uptime @@ -267,8 +272,9 @@ class Logs(commands.Cog): await ctx.send( f'{total} socket events observed ({cpm:.2f}/minute):\n{self.bot.socket_stats}') - @commandExtra(name='uptime', category='logs', - description=Texts('logs_help').get('_uptime')) + @commandExtra(name='uptime', category='misc', + description=Texts('logs_help').get('_uptime'), + short_doc=Texts('logs_help').get('_uptime__short')) async def _uptime(self, ctx): """Tells you how long the bot has been up for.""" uptime = humanize.naturaltime( diff --git a/cogs/Polls.py b/cogs/Polls.py index be74efa..2f77f78 100644 --- a/cogs/Polls.py +++ b/cogs/Polls.py @@ -204,15 +204,17 @@ class Polls(commands.Cog): poll.content = json.dumps(content) self.bot.database.session.commit() - @groupExtra(name='sondage', aliases=['poll'], + @groupExtra(name='poll', aliases=['sondage'], category='poll', - description=Texts('poll_help').get('_poll')) + description=Texts('poll_help').get('_poll'), + short_doc=Texts('poll_help').get('_poll__short')) async def _poll(self, ctx: commands.Context): if ctx.invoked_subcommand is None: await ctx.send_help('sondage') @_poll.group(name='create', aliases=['new', 'nouveau'], - description=Texts('poll_help').get('_poll_create')) + description=Texts('poll_help').get('_poll_create'), + short_doc=Texts('poll_help').get('_poll_create__short')) async def _poll_create(self, ctx: commands.Context, *, poll: str): is_anonymous = '--anonyme' in poll poll = poll.replace('--anonyme', '') diff --git a/cogs/Useful.py b/cogs/Useful.py index e9bd97a..cbeb5ba 100644 --- a/cogs/Useful.py +++ b/cogs/Useful.py @@ -36,10 +36,41 @@ class Useful(commands.Cog): return os.popen(cmd).read().strip() + @staticmethod + def fetch_info(): + total_lines = 0 + total_python_lines = 0 + file_amount = 0 + python_file_amount = 0 + ENV = "env" + + for path, _, files in os.walk("."): + for name in files: + file_dir = str(pathlib.PurePath(path, name)) + if ( + not name.endswith(".py") + and not name.endswith(".po") + and not name.endswith(".json") + ) or ENV in file_dir: + continue + file_amount += 1 + python_file_amount += 1 if name.endswith(".py") else 0 + with open(file_dir, "r", encoding="utf-8") as file: + for line in file: + if not line.strip().startswith("#") \ + or not line.strip(): + total_lines += 1 + total_python_lines += 1 if name.endswith(".py") \ + else 0 + + return (file_amount, total_lines), ( + python_file_amount, total_python_lines) + ########################################################################### - @commandExtra(name='iplocalise', category='useful', - description=Texts('useful_help').get('_iplocalise')) + @commandExtra(name='iplocalise', category='network', + description=Texts('useful_help').get('_iplocalise'), + short_doc=Texts('useful_help').get('_iplocalise__short')) async def _iplocalise(self, ctx: commands.Context, addr, ip_type=''): addr = re.sub(r'http(s?)://', '', addr) addr = addr[:-1] if addr.endswith('/') else addr @@ -103,8 +134,9 @@ class Useful(commands.Cog): ########################################################################### - @commandExtra(name='getheaders', category='useful', - description=Texts('useful_help').get('_getheaders')) + @commandExtra(name='getheaders', category='network', + description=Texts('useful_help').get('_getheaders'), + short_doc=Texts('useful_help').get('_getheaders__short')) async def _getheaders(self, ctx: commands.Context, addr: str): if (addr.startswith('http') or addr.startswith('ftp')) is not True: addr = f"http://{addr}" @@ -127,7 +159,7 @@ class Useful(commands.Cog): e.add_field(name=key, value=value, inline=True) await ctx.send(embed=e) - except aiohttp.client_exceptions.ClientError: + except aiohttp.ClientError: await ctx.send( f"{Texts('useful', ctx).get('Cannot connect to host')} {addr}" ) @@ -135,8 +167,9 @@ class Useful(commands.Cog): ########################################################################### @commandExtra(name='git', aliases=['sources', 'source', 'github'], - category='useful', - description=Texts('useful_help').get('_git')) + category='misc', + description=Texts('useful_help').get('_git'), + short_doc=Texts('useful_help').get('_git__short')) async def _git(self, ctx): e = discord.Embed( title=Texts('useful', ctx).get('git repo'), @@ -151,8 +184,9 @@ class Useful(commands.Cog): ########################################################################### - @commandExtra(name='quote', category='useful', - description=Texts('useful_help').get('_quote')) + @commandExtra(name='quote', category='misc', + description=Texts('useful_help').get('_quote'), + short_doc=Texts('useful_help').get('_quote__short')) async def _quote(self, ctx, message_id: discord.Message): e = discord.Embed( colour=message_id.author.colour, @@ -174,8 +208,9 @@ class Useful(commands.Cog): ########################################################################### - @commandExtra(name='ping', category='useful', - description=Texts('useful_help').get('_ping')) + @commandExtra(name='ping', category='network', + description=Texts('useful_help').get('_ping'), + short_doc=Texts('useful_help').get('_ping__short')) async def _ping(self, ctx: commands.Context): start = time.perf_counter() await ctx.trigger_typing() @@ -193,37 +228,9 @@ class Useful(commands.Cog): ########################################################################### - @staticmethod - def fetch_info(): - total_lines = 0 - total_python_lines = 0 - file_amount = 0 - python_file_amount = 0 - ENV = "env" - - for path, _, files in os.walk("."): - for name in files: - file_dir = str(pathlib.PurePath(path, name)) - if ( - not name.endswith(".py") - and not name.endswith(".po") - and not name.endswith(".json") - ) or ENV in file_dir: - continue - file_amount += 1 - python_file_amount += 1 if name.endswith(".py") else 0 - with open(file_dir, "r", encoding="utf-8") as file: - for line in file: - if not line.strip().startswith("#") \ - or not line.strip(): - total_lines += 1 - total_python_lines += 1 if name.endswith(".py") \ - else 0 - - return (file_amount, total_lines), (python_file_amount, total_python_lines) - - @commandExtra(name='info', aliases=['about'], category='useful', - description=Texts('useful_help').get('_info')) + @commandExtra(name='info', aliases=['about'], category='misc', + description=Texts('useful_help').get('_info'), + short_doc=Texts('useful_help').get('_info__short')) async def _info(self, ctx: commands.Context): proc = psutil.Process() total, python = self.fetch_info() @@ -306,8 +313,9 @@ class Useful(commands.Cog): ########################################################################### @commandExtra(name='credits', aliases=['contributors', 'authors'], - category='useful', - description=Texts('useful_help').get('_credits')) + category='misc', + description=Texts('useful_help').get('_credits'), + short_doc=Texts('useful_help').get('_credits__short')) async def _credits(self, ctx: commands.Context): e = discord.Embed( title=Texts('useful', ctx).get('Contributors'), diff --git a/cogs/User.py b/cogs/User.py index 0ccfd98..a1f44ec 100644 --- a/cogs/User.py +++ b/cogs/User.py @@ -19,14 +19,16 @@ class User(commands.Cog): ########################################################################### - @groupExtra(name='alias', aliases=['aliases'], category='user', - description=Texts('user_help').get('user._alias')) + @groupExtra(name='alias', aliases=['aliases'], category='alias', + description=Texts('user_help').get('_alias'), + short_doc=Texts('user_help').get('_alias__short')) async def _alias(self, ctx: commands.Context): if ctx.invoked_subcommand is None: await ctx.send_help('alias') @_alias.command(name='add', aliases=['set', 'new'], - description=Texts('user_help').get('_alias_add')) + description=Texts('user_help').get('_alias_add'), + short_doc=Texts('user_help').get('_alias_add__short')) async def _alias_add(self, ctx: commands.Context, *, user_alias: str): is_global = False if '--global' in user_alias: @@ -54,12 +56,14 @@ class User(commands.Cog): self.bot.database.session.commit() @_alias.command(name='remove', aliases=['drop', 'del', 'delete'], - description=Texts('user_help').get('_alias_remove')) + description=Texts('user_help').get('_alias_remove'), + short_doc=Texts('user_help').get('_alias_remove__short')) async def _alias_remove(self, ctx: commands.Context, prefix: str): ... @_alias.command(name='list', aliases=['show', 'all'], - description=Texts('user_help').get('_alias_list')) + description=Texts('user_help').get('_alias_list'), + short_doc=Texts('user_help').get('_alias_list__short')) async def _alias_list(self, ctx: commands.Context): ... diff --git a/extras/locales/en/LC_MESSAGES/help.po b/extras/locales/en/LC_MESSAGES/help.po index 8e535cf..327f385 100644 --- a/extras/locales/en/LC_MESSAGES/help.po +++ b/extras/locales/en/LC_MESSAGES/help.po @@ -19,4 +19,16 @@ msgid 'main_page.description' msgstr "Made by {}\nWhen using commands, <> means a required argument and [] means an optional argument.\n***(These symbols shouldn't be written)***" msgid 'main_page.categories' -msgstr 'Categories' \ No newline at end of file +msgstr 'Categories' + +msgid 'main_page.footer' +msgstr '- Send {}help to see more help about a command.' + +msgid 'main_page.not_found' +msgstr 'No command called "{}" found.' + +msgid 'command_help.subcommands' +msgstr 'Subcommands' + +msgid 'command_help.no_aliases' +msgstr 'No aliases' \ No newline at end of file diff --git a/extras/locales/fr/LC_MESSAGES/help.po b/extras/locales/fr/LC_MESSAGES/help.po index 13ad1f4..5b2645a 100644 --- a/extras/locales/fr/LC_MESSAGES/help.po +++ b/extras/locales/fr/LC_MESSAGES/help.po @@ -19,4 +19,16 @@ msgid 'main_page.description' msgstr "Créé par {}\nLorsque vous utilisez les commandes, <> correspond à un argument obligatoire et [] à un argument optionnel.\n***(Vous ne devez pas écrire ces symboles)***" msgid 'main_page.categories' -msgstr 'Catégories' \ No newline at end of file +msgstr 'Catégories' + +msgid 'main_page.footer' +msgstr "- Envoyez {}help pour avoir plus d'aide sur la commande." + +msgid 'main_page.not_found' +msgstr 'Impossible de trouver la commande {}.' + +msgid 'command_help.subcommands' +msgstr 'Sous-commandes' + +msgid 'command_help.no_aliases' +msgstr 'Aucun alias' \ No newline at end of file diff --git a/logs/tuxbot.log b/logs/tuxbot.log index 77d9fac..1e48432 100644 --- a/logs/tuxbot.log +++ b/logs/tuxbot.log @@ -1,60 +1,25 @@ -[INFO ] [2020-01-04 18:47:27] discord.client: logging in using static token -[INFO ] [2020-01-04 18:47:28] discord.gateway: Shard ID 0 has sent the IDENTIFY payload. -[INFO ] [2020-01-04 18:47:28] discord.gateway: Shard ID 0 has connected to Gateway: ["gateway-prd-main-1mb3",{"micros":59339,"calls":["discord-sessions-prd-1-26",{"micros":56634,"calls":["start_session",{"micros":36442,"calls":["api-prd-main-wpx2",{"micros":33053,"calls":["get_user",{"micros":2542},"add_authorized_ip",{"micros":1944},"get_guilds",{"micros":2441},"coros_wait",{"micros":1}]}]},"guilds_connect",{"micros":7,"calls":[]},"presence_connect",{"micros":1,"calls":[]}]}]}] (Session ID: 2e6c7d848985fce96ccf1d3a03a9878b). -[INFO ] [2020-01-04 18:47:38] discord.state: Processed a chunk for 462 members in guild ID 280805240977227776. -[INFO ] [2020-01-04 18:47:38] discord.state: Processed a chunk for 794 members in guild ID 331981755177238530. -[INFO ] [2020-01-04 18:47:38] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:39] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:39] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:40] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:40] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:41] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:41] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:42] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:42] discord.state: Processed a chunk for 810 members in guild ID 296698073177128962. -[INFO ] [2020-01-04 18:47:45] cogs.Logs: 2020-01-04 17:48:03.502000: Romain#5117 in #general (Gnous): rm-dev01.info -[INFO ] [2020-01-04 18:49:07] cogs.Logs: 2020-01-04 17:49:25.331000: loup#8965 in #general (Gnous): rm-dev01.info -[INFO ] [2020-01-04 18:50:52] cogs.Logs: 2020-01-04 17:51:11.819000: Romain#5117 in #bot (Gnous): rm-dev01.jsk reloac cogs.Utility -[INFO ] [2020-01-04 18:50:58] cogs.Logs: 2020-01-04 17:51:16.650000: loup#8965 in #bot (Gnous): rm-dev01.info -[INFO ] [2020-01-04 18:50:58] cogs.Logs: 2020-01-04 17:51:17.057000: Romain#5117 in #bot (Gnous): rm-dev01.jsk reload cogs.Utility -[INFO ] [2020-01-04 18:51:09] cogs.Logs: 2020-01-04 17:51:28.317000: Romain#5117 in #bot (Gnous): rm-dev01.jsk reload cogs.Useful -[INFO ] [2020-01-04 18:51:18] cogs.Logs: 2020-01-04 17:51:36.366000: Romain#5117 in #bot (Gnous): rm-dev01.info -[INFO ] [2020-01-04 18:52:22] cogs.Logs: 2020-01-04 17:52:41.348000: loup#8965 in #bot (Gnous): rm-dev01.jsk -[INFO ] [2020-01-04 18:53:20] cogs.Logs: 2020-01-04 17:53:38.893000: Romain#5117 in #bots-spam (Mounak): rm-dev01.info -[INFO ] [2020-01-04 18:57:52] cogs.Logs: 2020-01-04 17:58:10.611000: Romain#5117 in #bot (Gnous): rm-dev01.jsk -[INFO ] [2020-01-04 18:59:40] cogs.Logs: 2020-01-04 17:59:59.117000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk py -conf = _bot.config.get("permission", "Owners") -[INFO ] [2020-01-04 18:59:49] cogs.Logs: 2020-01-04 18:00:08.291000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk py -conf = _bot.config.get("permissions", "Owners") -[INFO ] [2020-01-04 18:59:59] cogs.Logs: 2020-01-04 18:00:17.851000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk py -conf = _bot.config.get("permissions", "Owners") - -await _ctx.send(conf) -[INFO ] [2020-01-04 19:01:46] cogs.Logs: 2020-01-04 18:02:04.343000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk py -conf = _bot.config.get("permissions", "Owners") -conf = conf.split(', ') -conf.append(str(329989396218773504)) - -conf = ', '.join(conf) - -await _ctx.send(conf) -[INFO ] [2020-01-04 19:02:06] cogs.Logs: 2020-01-04 18:02:25.221000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk py -conf = _bot.config.get("permissions", "Owners") -conf = conf.split(', ') -conf.append(str(a)) - -conf = ', '.join(conf) - -_bot.config.set("permissions", "Owners", conf) -[INFO ] [2020-01-04 19:02:17] cogs.Logs: 2020-01-04 18:02:36.778000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk py -conf = _bot.config.get("permissions", "Owners") -conf = conf.split(', ') -conf.append(str(123456789)) - -conf = ', '.join(conf) - -_bot.config.set("permissions", "Owners", conf) -[INFO ] [2020-01-04 19:05:37] discord.client: Cleaning up tasks. -[INFO ] [2020-01-04 19:05:37] discord.client: Cleaning up after 6 tasks. -[INFO ] [2020-01-04 19:05:37] discord.client: All tasks finished cancelling. -[INFO ] [2020-01-04 19:05:37] discord.client: Closing the event loop. +[INFO ] [2020-01-05 00:57:22] discord.client: logging in using static token +[INFO ] [2020-01-05 00:57:23] discord.gateway: Shard ID 0 has sent the IDENTIFY payload. +[INFO ] [2020-01-05 00:57:24] discord.gateway: Shard ID 0 has connected to Gateway: ["gateway-prd-main-gl57",{"micros":945031,"calls":["discord-sessions-prd-1-25",{"micros":942744,"calls":["start_session",{"micros":32305,"calls":["api-prd-main-7v2n",{"micros":29524,"calls":["get_user",{"micros":1434},"add_authorized_ip",{"micros":1485},"get_guilds",{"micros":2100},"coros_wait",{"micros":1}]}]},"guilds_connect",{"micros":7,"calls":[]},"presence_connect",{"micros":877400,"calls":[]}]}]}] (Session ID: 4f9781c6360b7abd8e741bf8ebc2dfc7). +[INFO ] [2020-01-05 00:57:33] discord.state: Processed a chunk for 462 members in guild ID 280805240977227776. +[INFO ] [2020-01-05 00:57:33] discord.state: Processed a chunk for 794 members in guild ID 331981755177238530. +[INFO ] [2020-01-05 00:57:34] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:34] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:35] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:35] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:36] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:36] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:36] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:37] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:37] discord.state: Processed a chunk for 840 members in guild ID 296698073177128962. +[INFO ] [2020-01-05 00:57:39] cogs.Logs: 2020-01-04 23:57:58.535000: Romain#5117 in #tuxbot-test (Gnous): <@!301062143942590465> help +[INFO ] [2020-01-05 00:58:20] cogs.Logs: 2020-01-04 23:58:38.646000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk reload cogs.Help +[INFO ] [2020-01-05 00:58:22] cogs.Logs: 2020-01-04 23:58:41.511000: Romain#5117 in #tuxbot-test (Gnous): <@!301062143942590465> help +[INFO ] [2020-01-05 00:58:53] cogs.Logs: 2020-01-04 23:59:12.305000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk reload cogs.Help +[INFO ] [2020-01-05 00:58:57] cogs.Logs: 2020-01-04 23:59:16.559000: Romain#5117 in #tuxbot-test (Gnous): <@!301062143942590465> help +[INFO ] [2020-01-05 00:59:09] cogs.Logs: 2020-01-04 23:59:28.908000: Romain#5117 in #tuxbot-test (Gnous): rm-dev01.jsk reload cogs.Help +[INFO ] [2020-01-05 00:59:17] cogs.Logs: 2020-01-04 23:59:36.767000: Romain#5117 in #bots-spam (Mounak): <@!301062143942590465> help +[INFO ] [2020-01-05 01:00:48] discord.client: Cleaning up tasks. +[INFO ] [2020-01-05 01:00:48] discord.client: Cleaning up after 6 tasks. +[INFO ] [2020-01-05 01:00:49] discord.client: All tasks finished cancelling. +[INFO ] [2020-01-05 01:00:49] discord.client: Closing the event loop. diff --git a/utils/paginator.py b/utils/paginator.py index a1d065c..b18bd84 100644 --- a/utils/paginator.py +++ b/utils/paginator.py @@ -1,10 +1,21 @@ +""" + +Based on https://github.com/Rapptz/RoboDanny/blob/3ec71c4c4031f868caff3027d71aecdebc3c5cec/cogs/utils/paginator.py +Adapted by Romain J. + +""" + import asyncio + import discord +from discord.ext import commands from discord.ext.commands import Paginator as CommandPaginator + class CannotPaginate(Exception): pass + class Pages: """Implements a paginator that queries the user for the pagination interface. @@ -30,28 +41,35 @@ class Pages: permissions: discord.Permissions Our permissions for the channel. """ - def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True): + + def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True, + embed_color=discord.Color.blurple(), title=None, + thumbnail=None, footericon=None, footertext=None, author=None, + delete_after=None): self.bot = ctx.bot self.entries = entries self.message = ctx.message self.channel = ctx.channel - self.author = ctx.author + self.author = author if author else ctx.author + self.thumbnail = thumbnail + self.footericon = footericon + self.footertext = footertext + self.title = title + self.delete_after = delete_after self.per_page = per_page pages, left_over = divmod(len(self.entries), self.per_page) if left_over: pages += 1 self.maximum_pages = pages - self.embed = discord.Embed(colour=discord.Colour.blurple()) + self.embed = discord.Embed(colour=embed_color) self.paginating = len(entries) > per_page self.show_entry_count = show_entry_count self.reaction_emojis = [ - ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page), + ('\U000023ee\U0000fe0f', self.first_page), ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page), + ('\U000023f9', self.stop_pages), ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page), - ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page), - ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ), - ('\N{BLACK SQUARE FOR STOP}', self.stop_pages), - ('\N{INFORMATION SOURCE}', self.show_help), + ('\U000023ed\U0000fe0f', self.last_page) ] if ctx.guild is not None: @@ -60,18 +78,24 @@ class Pages: self.permissions = self.channel.permissions_for(ctx.bot.user) if not self.permissions.embed_links: - raise CannotPaginate('Bot does not have embed links permission.') + raise commands.BotMissingPermissions( + 'I do not have permissions to : Embed links.' + ) if not self.permissions.send_messages: - raise CannotPaginate('Bot cannot send messages.') + raise commands.BotMissingPermissions('Bot cannot send messages.') if self.paginating: # verify we can actually use the pagination session if not self.permissions.add_reactions: - raise CannotPaginate('Bot does not have add reactions permission.') + raise commands.BotMissingPermissions( + 'I do not have permissions to : Add Reactions.' + ) if not self.permissions.read_message_history: - raise CannotPaginate('Bot does not have Read Message History permission.') + raise commands.BotMissingPermissions( + 'I do not have permissions to : Read Message History.' + ) def get_page(self, page): base = (page - 1) * self.per_page @@ -86,22 +110,25 @@ class Pages: def prepare_embed(self, entries, page, *, first=False): p = [] - for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)): - p.append(f'{index}. {entry}') + for index, entry in enumerate(entries, + 1 + ((page - 1) * self.per_page)): + p.append(f'`{index}.` {entry}') if self.maximum_pages > 1: if self.show_entry_count: - text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)' + text = f'Showing page {page}/{self.maximum_pages} ({len(self.entries)} entries)' else: - text = f'Page {page}/{self.maximum_pages}' + text = f'Showing page {page}/{self.maximum_pages}' self.embed.set_footer(text=text) if self.paginating and first: p.append('') - p.append('Confused? React with \N{INFORMATION SOURCE} for more info.') self.embed.description = '\n'.join(p) + self.embed.title = self.title or discord.Embed.Empty + self.embed.set_author(icon_url=self.author.avatar_url, + name=str(self.author)) async def show_page(self, page, *, first=False): self.current_page = page @@ -153,7 +180,8 @@ class Pages: async def numbered_page(self): """lets you type a page number to go to""" to_delete = [] - to_delete.append(await self.channel.send('What page do you want to go to?')) + to_delete.append( + await self.channel.send('What page do you want to go to?')) def message_check(m): return m.author == self.author and \ @@ -161,7 +189,11 @@ class Pages: m.content.isdigit() try: - msg = await self.bot.wait_for('message', check=message_check, timeout=30.0) + msg = await self.bot.wait_for( + 'message', + check=message_check, + timeout=30.0 + ) except asyncio.TimeoutError: to_delete.append(await self.channel.send('Took too long.')) await asyncio.sleep(5) @@ -171,7 +203,8 @@ class Pages: if page != 0 and page <= self.maximum_pages: await self.show_page(page) else: - to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})')) + to_delete.append(await self.channel.send( + f'Invalid page given. ({page}/{self.maximum_pages})')) await asyncio.sleep(5) try: @@ -182,8 +215,9 @@ class Pages: async def show_help(self): """shows this message""" messages = ['Welcome to the interactive paginator!\n'] - messages.append('This interactively allows you to see pages of text by navigating with ' \ - 'reactions. They are as follows:\n') + messages.append( + 'This interactively allows you to see pages of text by navigating with ' \ + 'reactions. They are as follows:\n') for (emoji, func) in self.reaction_emojis: messages.append(f'{emoji} {func.__doc__}') @@ -191,7 +225,8 @@ class Pages: embed = self.embed.copy() embed.clear_fields() embed.description = '\n'.join(messages) - embed.set_footer(text=f'We were on page {self.current_page} before this message.') + embed.set_footer( + text=f'We were on page {self.current_page} before this message.') await self.message.edit(content=None, embed=embed) async def go_back_to_current_page(): @@ -205,16 +240,15 @@ class Pages: await self.message.delete() self.paginating = False - def react_check(self, payload): - if payload.user_id != self.author.id: + def react_check(self, reaction, user): + if user is None or user.id != self.author.id: return False - if payload.message_id != self.message.id: + if reaction.message.id != self.message.id: return False - to_check = str(payload.emoji) for (emoji, func) in self.reaction_emojis: - if to_check == emoji: + if reaction.emoji == emoji: self.match = func return True return False @@ -230,52 +264,72 @@ class Pages: while self.paginating: try: - payload = await self.bot.wait_for('raw_reaction_add', check=self.react_check, timeout=120.0) + reaction, user = await self.bot.wait_for( + 'reaction_add', + check=self.react_check, + timeout=self.delete_after + ) except asyncio.TimeoutError: self.paginating = False try: - await self.message.clear_reactions() + await self.message.delete() except: pass finally: break try: - await self.message.remove_reaction(payload.emoji, discord.Object(id=payload.user_id)) + await self.message.remove_reaction(reaction, user) except: - pass # can't remove it so don't bother doing so + pass # can't remove it so don't bother doing so await self.match() + class FieldPages(Pages): """Similar to Pages except entries should be a list of tuples having (key, value) to show as embed fields instead. """ + def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True, + title, thumbnail, footericon, footertext, + embed_color=discord.Color.blurple()): + super().__init__(ctx, entries=entries, per_page=per_page, + show_entry_count=show_entry_count, title=title, + thumbnail=thumbnail, footericon=footericon, + footertext=footertext, embed_color=embed_color) + def prepare_embed(self, entries, page, *, first=False): self.embed.clear_fields() - self.embed.description = discord.Embed.Empty for key, value in entries: self.embed.add_field(name=key, value=value, inline=False) + self.embed.title = self.title + if self.maximum_pages > 1: if self.show_entry_count: - text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)' + text = f' [{page}/{self.maximum_pages}]' else: - text = f'Page {page}/{self.maximum_pages}' + text = f' [{page}/{self.maximum_pages}]' + self.embed.title = self.title + text + + self.embed.set_footer(icon_url=self.footericon, text=self.footertext) + self.embed.set_thumbnail(url=self.thumbnail) - self.embed.set_footer(text=text) class TextPages(Pages): """Uses a commands.Paginator internally to paginate some text.""" - def __init__(self, ctx, text, *, prefix='```', suffix='```', max_size=2000): - paginator = CommandPaginator(prefix=prefix, suffix=suffix, max_size=max_size - 200) + def __init__(self, ctx, text, *, prefix='```', suffix='```', + max_size=2000): + paginator = CommandPaginator(prefix=prefix, suffix=suffix, + max_size=max_size - 200) for line in text.split('\n'): paginator.add_line(line) - super().__init__(ctx, entries=paginator.pages, per_page=1, show_entry_count=False) + super().__init__(ctx, entries=paginator.pages, per_page=1, + show_entry_count=False) def get_page(self, page): return self.entries[page - 1] @@ -286,4 +340,4 @@ class TextPages(Pages): def get_content(self, entry, page, *, first=False): if self.maximum_pages > 1: return f'{entry}\nPage {page}/{self.maximum_pages}' - return entry \ No newline at end of file + return entry