diff --git a/bot.py b/bot.py index d362a97..89333a9 100644 --- a/bot.py +++ b/bot.py @@ -12,6 +12,8 @@ from cogs.utils.localizer import Localizer from cogs.utils.localizer import LocalizerWrapper from cogs.utils.alias import Aliaser from cogs.utils.context import Context +from cogs.helpformatter import commandhelper +from cogs.utils.paginator import Scroller with codecs.open("data/config.yaml", 'r', encoding='utf8') as f: @@ -21,7 +23,8 @@ with codecs.open("data/config.yaml", 'r', encoding='utf8') as f: initial_extensions = [ 'cogs.cogs', 'cogs.settings', - 'cogs.misc' + 'cogs.misc', + 'cogs.helpformatter' ] @@ -58,15 +61,9 @@ class Bot(commands.Bot): if not self.debug: if (isinstance(err, commands.MissingRequiredArgument) or isinstance(err, commands.BadArgument)): - formatter = ctx.bot.formatter - if ctx.invoked_subcommand is None: - _help = await formatter.format_help_for(ctx, ctx.command) - else: - _help = await formatter.format_help_for(ctx, - ctx.invoked_subcommand) - - for message in _help: - await ctx.send(message) + paginator = commandhelper(ctx, ctx.command, ctx.invoker) + scroller = Scroller(ctx, paginator) + await scroller.start_scrolling() if isinstance(err, commands.CommandInvokeError): pass @@ -94,8 +91,6 @@ class Bot(commands.Bot): # Replace aliases with commands ctx = self.aliaser.get_command(ctx) - if ctx.command and isinstance(ctx.command, commands.GroupMixin): - ctx = self.aliaser.get_subcommand(ctx, ctx.command, [str(ctx.command)]) # Add the localizer if ctx.command and ctx.command.cog_name: diff --git a/cogs/cogs.py b/cogs/cogs.py index 1a73679..65e8640 100644 --- a/cogs/cogs.py +++ b/cogs/cogs.py @@ -10,7 +10,7 @@ class Cogs(commands.Cog): self.bot = bot self.settings = self.bot.settings - @commands.group(name='cogs') + @commands.group(name='cogs', hidden=True) @checks.is_owner() async def _cogs(self, ctx): if ctx.invoked_subcommand is None: diff --git a/cogs/helpformatter.py b/cogs/helpformatter.py new file mode 100644 index 0000000..99904ca --- /dev/null +++ b/cogs/helpformatter.py @@ -0,0 +1,111 @@ +# NOTICE: Before you copy this example, be sure you understand what all this does. Remember: This is a gist, not a github file meaning +# you can't pip install this, you would need to put this into a new file and add it to your cog list. + +import pprint + +import discord +import asyncio +from discord.ext import commands + +from cogs.utils.paginator import HelpPaginator, Scroller + + +def get_cmd_dict(ctx, qualified_name): + split = qualified_name.split() + cmd_dict = ctx.bot.aliaser.get_cmd_help(ctx.locale, split[-1], split[:-1]) + return cmd_dict + + +def helper(ctx): + bot = ctx.bot + # Display music commands first + music = bot.get_cog('Music') + paginator = coghelper(ctx, music) + + for cog in bot.cogs.copy().values(): + if cog.__cog_name__ is 'Music': + continue + cogpaignator = coghelper(ctx, cog) + paginator.append_paginator(cogpaignator) + + paginator.add_page_indicator(ctx.localizer, "{pageindicator}", ctx.prefix) + return paginator + + +def coghelper(ctx, cog, ignore_subcommands=True): + paginator = HelpPaginator(max_size=5000, max_fields=5, color=ctx.me.color, title=cog.__cog_name__) + for cmd in cog.walk_commands(): + if ignore_subcommands: + if len(cmd.qualified_name.split()) > 1: + continue + if cmd.hidden: + continue + cmd_dict = get_cmd_dict(ctx, cmd.qualified_name) + paginator.add_command_field(cmd_dict) + paginator.add_page_indicator(ctx.localizer, "{paginator}", ctx.prefix) + return paginator + + +def commandhelper(ctx, command, invoker, include_subcmd=True): + cmd_dict = get_cmd_dict(ctx, command.qualified_name) + prefix = ctx.prefix + if invoker.split()[:-1]: + prefix += ' '.join(invoker.split()[:-1]) + ' ' + + aliases = cmd_dict.get('aliases', []) + args = cmd_dict.get('args', '') + description = cmd_dict.get('description', '') + sub_commands = cmd_dict.get('sub_commands', []) + + if len(aliases) > 1: + cmd = prefix + '[' + '|'.join([str(a) for a in aliases]) + '] ' + else: + cmd = prefix + '|'.join([str(a) for a in aliases]) + ' ' + + cmd += args + if not sub_commands: + paginator = HelpPaginator(max_size=5000, max_fields=5, color=ctx.me.color, title=cmd, description=description) + paginator.force_close_page() + return paginator + + description = f"```{description}```" + paginator = HelpPaginator(max_size=5000, max_fields=5, color=ctx.me.color, title=cmd, description=description) + + for sub_command, sub_cmd_dict in sub_commands.items(): + paginator.add_command_field(sub_cmd_dict) + paginator.add_page_indicator(ctx.localizer, "{pageindicator}", ctx.prefix) + return paginator + + +class Help(commands.Cog): + """Help command""" + def __init__(self, bot): + self.bot = bot + + @commands.command(hidden=True) + async def help(self, ctx): # Takes no args because reasons(using the view directly) + ctx.view.skip_ws() + v = ctx.view + invoker = v.buffer[v.index:v.end] + ctx = self.bot.aliaser.get_subcommand(ctx, group=None, parents=[]) + command = v.buffer[v.index:v.end] + + if not command: + scroller = Scroller(ctx, helper(ctx)) + await scroller.start_scrolling() + + if command: + thing = ctx.bot.get_cog(command) or ctx.bot.get_command(command) + if not thing: + return await ctx.send(f'Looks like "{command}" is not a command or category.') + if isinstance(thing, commands.Command): + paginator = commandhelper(ctx, thing, invoker) + scroller = Scroller(ctx, paginator) + await scroller.start_scrolling() + else: + scroller = Scroller(ctx, coghelper(ctx, thing)) + await scroller.start_scrolling() + +def setup(bot): + bot.remove_command("help") + bot.add_cog(Help(bot)) \ No newline at end of file diff --git a/cogs/misc.py b/cogs/misc.py index 6a414b3..564855b 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -9,6 +9,7 @@ from discord.ext import commands from cogs.utils import checks from lavalink import __version__ as LavalinkVersion + class Misc(commands.Cog): def __init__(self, bot): self.bot = bot diff --git a/cogs/music.py b/cogs/music.py index 5cf7f99..971026c 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -15,7 +15,7 @@ from typing import Optional from .utils import checks, RoxUtils, timeformatter from .utils.mixplayer import MixPlayer -from .utils.embedscroller import QueueScroller +from .utils.paginator import QueuePaginator, Scroller time_rx = re.compile('[0-9]+') @@ -220,7 +220,8 @@ class Music(commands.Cog): if user is None: queue = player.global_queue() - scroller = QueueScroller(ctx, queue, ctx.localizer, lines_per_page=10) + pagified_queue = QueuePaginator(ctx.localizer, queue, color=ctx.me.color) + scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() else: @@ -228,7 +229,8 @@ class Music(commands.Cog): if not user_queue: return await ctx.send(ctx.localizer.format_str("{queue.empty}", _user=user.name)) - scroller = QueueScroller(ctx, user_queue, ctx.localizer, lines_per_page=10, user_name=user.name) + pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, user_name=ctx.author.name) + scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() @commands.command(name='myqueue') @@ -240,7 +242,8 @@ class Music(commands.Cog): if not user_queue: return await ctx.send(ctx.localizer.format_str("{my_queue}")) - scroller = QueueScroller(ctx, user_queue, ctx.localizer, lines_per_page=10, user_name=ctx.author.name) + pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, user_name=ctx.author.name) + scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() @commands.command(name='pause') diff --git a/cogs/settings.py b/cogs/settings.py index 4d00d64..0344694 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -105,7 +105,7 @@ class Settings(commands.Cog): await ctx.send(embed=embed) @commands.guild_only() - @_set.command(name='listenchannels') + @_set.command(name='listenchannels', hidden=True) async def set_listen_voice(self, ctx, *channels: discord.VoiceChannel): if channels: channel_ids = [channel.id for channel in channels] diff --git a/cogs/utils/alias.py b/cogs/utils/alias.py index 8593d81..a79f787 100644 --- a/cogs/utils/alias.py +++ b/cogs/utils/alias.py @@ -6,6 +6,12 @@ from os import path from discord.ext import commands +""" +Not the prettiest this, works by replacing any found aliases in a command string with the actual command names. +Allows per guild aliases of subcommands without doing anything to discord.py itself. Suggestions of better/correct +ways of doing this appreciated. +""" + class Aliaser: def __init__(self, localization_folder, default_lang): self.localization_folder = path.realpath(localization_folder) @@ -39,7 +45,7 @@ class Aliaser: def load_localizations(self): for lang in self.localization_table.keys(): - with open(path.join(self.localization_folder, lang, "aliases.yaml"), "r", encoding='utf-8') as f: + with open(path.join(self.localization_folder, lang, "commands.yaml"), "r", encoding='utf-8') as f: data = yaml.load(f) self.localization_table[lang] = {'aliases': data, 'commands': self._gen_alias_dict(data)} @@ -66,9 +72,27 @@ class Aliaser: return command return default - def get_alias(self, locale, command): + def get_cmd_help(self, locale, command=None, parents=[]): + """ Fetches the command info dictionary """ locale = self.localization_table[locale] - return locale['aliases'].get(command, []) + + def traverse(command_tree, parents, command): + # Return the command when no more parents exist + if not parents: + return command_tree.get(command, []) + + parent = command_tree.get(parents.pop(0), None) + if not parent: + return [] + command_tree = parent.get('sub_commands', []) + if not command_tree: + return None + return traverse(command_tree, parents, command) + + command_tree = locale['aliases'] + if command: + return traverse(command_tree, parents, command) + return command_tree def get_command(self, ctx): """ Get a top level command. """ @@ -76,13 +100,22 @@ class Aliaser: ctx.command = None return ctx ctx.view.undo() + ctx.invoker = invoker = ctx.view.buffer[ctx.view.index:ctx.view.end] alias = ctx.view.get_word() command = self.convert_alias(ctx.locale, alias, alias) ctx.invoked_with = command ctx.command = ctx.bot.all_commands.get(command) + if ctx.command and isinstance(ctx.command, commands.GroupMixin): + ctx = self.get_subcommand(ctx, ctx.command, [str(ctx.command)]) return ctx - def get_subcommand(self, ctx, group, parents=[]): + def _replace_command(self, view, index, alias, command): + buf = view.buffer + changed = buf[index:] + view.buffer = buf[:index] + changed.replace(alias, command, 1) + return view + + def get_subcommand(self, ctx, group=None, parents=[]): """ Recursicely replaces all subcommand aliases with subcommands.""" view = ctx.view prev = view.previous @@ -96,7 +129,7 @@ class Aliaser: parents.append(subcmd) # Replace the alias in the command - view.buffer = view.buffer.replace(alias, subcmd) + self._replace_command(view, idx, alias, subcmd) # Update the view to account for difference in length strdiff = len(subcmd) - len(alias) @@ -104,7 +137,10 @@ class Aliaser: view.index += strdiff # Get the subcommand - sub = group.all_commands.get(subcmd, None) + if group: + sub = group.all_commands.get(subcmd, None) + else: + sub = ctx.bot.all_commands.get(subcmd, None) # Translate any subsubcommands if sub and isinstance(sub, commands.GroupMixin): diff --git a/cogs/utils/embedscroller.py b/cogs/utils/embedscroller.py deleted file mode 100644 index 536eb99..0000000 --- a/cogs/utils/embedscroller.py +++ /dev/null @@ -1,220 +0,0 @@ -import asyncio -import discord -import math -import lavalink - - -class CantScroll(Exception): - pass - -# Maybe slightly very copied from rdanny -# Thanks danny <3 - - -class EmbedScroller: - def __init__(self, ctx, pages): - - self.bot = ctx.bot - self.pages = pages - self.cmdmsg = ctx.message - self.message = ctx.message - self.channel = ctx.channel - self.author = ctx.author - - if len(self.pages) > 1: - self.scrolling = True - else: - self.scrolling = False - self.reaction_emojis = [ - ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page), - ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page), - ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page), - ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page), - ('❌', self.stop_scrolling), - ] - - if ctx.guild is not None: - self.permissions = self.channel.permissions_for(ctx.guild.me) - else: - self.permissions = self.channel.permissions_for(ctx.bot.user) - - if not self.permissions.embed_links: - raise CantScroll('Bot does not have embed links permission.') - - if not self.permissions.send_messages: - raise CantScroll('Bot cannot send messages.') - - if self.scrolling: - # verify we can actually use the pagination session - if not self.permissions.add_reactions: - raise CantScroll('Bot does not have add reactions permission.') - - if not self.permissions.read_message_history: - raise CantScroll('Bot does not have Read Message History permission.') - - async def send(self): - self.current_page = 0 - # No embeds to scroll through - if not self.pages: - return - - if not self.scrolling: - return await self.channel.send(embed=self.pages[0]) - - self.message = await self.channel.send(embed=self.pages[0]) - for (reaction, _) in self.reaction_emojis: - if len(self.pages) == 2 and reaction in ('\u23ed', '\u23ee'): - continue - - await self.message.add_reaction(reaction) - - async def scroll(self, page): - if page < 0 or page >= len(self.pages): - return - self.current_page = page - await self.message.edit(embed=self.pages[page]) - - async def first_page(self): - await self.scroll(0) - - async def last_page(self): - await self.scroll(len(self.pages) - 1) - - async def next_page(self): - await self.scroll(self.current_page + 1) - - async def previous_page(self): - await self.scroll(self.current_page - 1) - - async def stop_scrolling(self): - self.scrolling = False - await self.message.delete() - await self.cmdmsg.delete() - - def react_check(self, reaction, user): - if user is None or user.id != self.author.id: - return False - - if reaction.message.id != self.message.id: - return False - - for (emoji, func) in self.reaction_emojis: - if reaction.emoji == emoji: - self.match = func - return True - return False - - async def start_scrolling(self): - if not self.scrolling: - await self.send() - else: - self.bot.loop.create_task(self.send()) - - while self.scrolling: - try: - reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0) - except asyncio.TimeoutError: - self.scrolling = False - try: - await self.message.clear_reactions() - except: - pass - finally: - break - - try: - await self.message.remove_reaction(reaction, user) - except: - pass - - await self.match() - - -class ScrollerFromLines(EmbedScroller): - def __init__(self, ctx, description, lines, lines_per_page=10): - pagecount = math.ceil(len(lines) / lines_per_page) - pages = [] - page = '' - for index, line in enumerate(lines): - page += line + '\n' - if index % lines_per_page == lines_per_page - 1: - embed = discord.Embed(color=0xEFD26C, - title=description, - description=page) - embed.set_footer(text=localizer.format_str("{queue.pageindicator}", - _current=index//lines_per_page + 1, - _total=pagecount)) - pages.append(embed) - page = '' - - if page != '': - embed = discord.Embed(color=0xEFD26C, - description=page) - - embed.set_footer(text=localizer.format_str("{queue.pageindicator}", - _current=pagecount, - _total=pagecount)) - pages.append(embed) - - super().__init__(ctx=ctx, pages=pages) - - -class QueueScroller(EmbedScroller): - """ Fugly, but works for now """ - def __init__(self, ctx, queue, localizer, lines_per_page=10, user_name: str=None): - - pagecount = math.ceil(len(queue) / lines_per_page) - self.queue = queue - - if user_name is None: - title = localizer.format_str("{queue.length}", - _length=len(queue)) - else: - title = localizer.format_str("{queue.userqueue}", - _user=user_name, - _length=len(queue)) - pages = [] - page = '' - for index, temp in enumerate(queue): - if user_name is None: - track = temp - page += localizer.format_str("{queue.globaltrack}", - _index=index+1, - _title=track.title, - _uri=track.uri, - _user_id=track.requester) - else: - track, globpos = temp - page += localizer.format_str("{queue.usertrack}", - _index=index+1, - _globalindex=globpos+1, - _title=track.title, - _uri=track.uri) - - if index % lines_per_page == lines_per_page - 1: - embed = discord.Embed(color=0xEFD26C, - title=title, - description=page) - embed.set_footer(text=localizer.format_str("{queue.pageindicator}", - _current=index//lines_per_page + 1, - _total=pagecount)) - pages.append(embed) - page = '' - - if page != '': - embed = discord.Embed(color=0xEFD26C, - title=title, - description=page) - embed.set_footer(text=localizer.format_str("{queue.pageindicator}", - _current=pagecount, - _total=pagecount)) - pages.append(embed) - - super().__init__(ctx=ctx, pages=pages) - - async def scroll(self, page): - # overwrite to add ability to update queue length/add scrubber. - if page < 0 or page >= len(self.pages): - return - self.current_page = page - await self.message.edit(embed=self.pages[page]) diff --git a/cogs/utils/localizer.py b/cogs/utils/localizer.py index 578aa67..2ec06ea 100644 --- a/cogs/utils/localizer.py +++ b/cogs/utils/localizer.py @@ -52,7 +52,7 @@ class Localizer: self.localization_table[lang] = {} l_table = self.localization_table[lang] for file in glob(path.join(self.localization_folder, lang, "*.yaml")): - if 'aliases' in file: + if 'aliases' in file or 'commands' in file: continue file_base = path.basename(file).split(".")[0] with open(file, "r", encoding='utf-8') as f: diff --git a/cogs/utils/paginator.py b/cogs/utils/paginator.py new file mode 100644 index 0000000..41e10e7 --- /dev/null +++ b/cogs/utils/paginator.py @@ -0,0 +1,285 @@ +import discord +import asyncio + + +class CantScroll(Exception): + pass + + +class BasePaginator: + def __init__(self, max_size=2000, max_units=10): + self._pages = [] + self._current_page_size = 0 + self._current_units = 0 + self._max_size = max_size + self._max_units = max_units + + def close_page(self): + pass + + def add_page_indicator(self, localizer, localizer_str=None, prefix=None): + self.close_page() + if localizer_str: + for i, page in enumerate(self._pages, start=1): + page.set_footer(text=localizer.format_str(localizer_str, + _current=i, + _total=len(self._pages), + _prefix=prefix)) + else: + for i, page in enumerate(self._pages, start=1): + page.set_footer(text=f"{i}/{len(self._pages)}") + + def append_paginator(self, paginator): + if not isinstance(paginator, BasePaginator): + raise TypeError('Pageinator needs to be a subclass of BasePaginator.') + self.close_page() + for page in paginator.pages: + self.pages.append(page) + + @property + def pages(self): + if self._current_page_size > 0: + self.close_page() + return self._pages + + +class TextPaginator(BasePaginator): + def __init__(self, max_size=2000, max_lines=10, **embed_base): + super().__init__(max_size=max_size, max_units=max_lines) + self._current_page = [] + self.embed_base = embed_base + + def close_page(self): + if self._current_page_size > 0: + embed = discord.Embed(**self.embed_base) + embed.description = '\n'.join(self._current_page) + self._pages.append(embed) + + self._current_page_size = 0 + self._current_units = 0 + self._current_page = [] + + def add_line(self, line='', *, empty=False): + if len(line) > self._max_size: + raise RuntimeError(f'Line exceeds maximum page size {self._max_size}') + + if self._current_page_size + len(line) + 1 > self._max_size: + self.close_page() + + if self._current_units >= self._max_units: + self.close_page() + + self._current_page_size += len(line) + 1 + self._current_units += 1 + self._current_page.append(line) + + if empty: + self._current_page.append('') + self._current_page_size += 1 + + +class FieldPaginator(BasePaginator): + def __init__(self, max_size=5000, max_fields=8, **embed_base): + super().__init__(max_size=max_size, max_units=max_fields) + self._embed_base = embed_base + self._current_page = discord.Embed(**self._embed_base) + + def close_page(self): + if self._current_page_size > 0: + self._pages.append(self._current_page) + self._current_page_size = 0 + self._current_units = 0 + self._current_page = discord.Embed(**self._embed_base) + + def add_field(self, name, value, inline=False): + field = {"name": name, "value": value} + fieldsize = sum([len(val) for _, val in field.items()]) + if len(name) > 256 or len(value) > 1024: + raise RuntimeError(f'Maximum field size exceeded, max name length: 256. max value length: 1024.') + + if self._current_page_size + fieldsize > self._max_size: + self.close_page() + + if self._current_units >= self._max_units: + self.close_page() + + field["inline"] = inline + self._current_page_size += fieldsize + self._current_units += 1 + self._current_page.add_field(**field) + + +class QueuePaginator(TextPaginator): + def __init__(self, localizer, queue, color, user_name: str=None): + self.localizer = localizer + self.queue = queue + + if user_name is None: + title = localizer.format_str("{queue.length}", + _length=len(queue)) + else: + title = localizer.format_str("{queue.userqueue}", + _user=user_name, + _length=len(queue)) + + super().__init__(max_lines=10, **{"color": color, "title": title}) + + for index, temp in enumerate(queue): + if user_name is None: + track = temp + queued_track = localizer.format_str("{queue.globaltrack}", _index=index+1, + _title=track.title, _uri=track.uri, + _user_id=track.requester) + else: + track, globpos = temp + queued_track = localizer.format_str("{queue.usertrack}", _index=index+1, + _globalindex=globpos+1, _title=track.title, + _uri=track.uri) + + self.add_line(queued_track) + self.add_page_indicator(self.localizer, "{queue.pageindicator}") + + +class HelpPaginator(FieldPaginator): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def force_close_page(self): + self._pages.append(self._current_page) + self._current_page_size = 0 + self._current_units = 0 + self._current_page = discord.Embed(**self._embed_base) + + def add_command_field(self, cmd_dict): + if not isinstance(cmd_dict, dict): + return + aliases = cmd_dict.get('aliases', []) + args = cmd_dict.get('args', '') + description = cmd_dict.get('description', '') + + cmd = str(aliases[0]) + + cmd += ' ' + args + self.add_field(name=cmd, value=description, inline=False) + + +# Maybe slightly very copied from rdanny +# Thanks danny <3 + +class Scroller: + def __init__(self, ctx, paginator): + + self.bot = ctx.bot + self.pages = paginator.pages + self.cmdmsg = ctx.message + self.message = ctx.message + self.channel = ctx.channel + self.author = ctx.author + + if len(self.pages) > 1: + self.scrolling = True + else: + self.scrolling = False + self.reaction_emojis = [ + ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page), + ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page), + ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page), + ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page), + ('❌', self.stop_scrolling), + ] + + if ctx.guild is not None: + self.permissions = self.channel.permissions_for(ctx.guild.me) + else: + self.permissions = self.channel.permissions_for(ctx.bot.user) + + if not self.permissions.embed_links: + raise CantScroll('Bot does not have embed links permission.') + + if not self.permissions.send_messages: + raise CantScroll('Bot cannot send messages.') + + if self.scrolling: + # verify we can actually use the pagination session + if not self.permissions.add_reactions: + raise CantScroll('Bot does not have add reactions permission.') + + if not self.permissions.read_message_history: + raise CantScroll('Bot does not have Read Message History permission.') + + async def send(self): + self.current_page = 0 + # No embeds to scroll through + if not self.pages: + return + + if not self.scrolling: + return await self.channel.send(embed=self.pages[0]) + + self.message = await self.channel.send(embed=self.pages[0]) + for (reaction, _) in self.reaction_emojis: + if len(self.pages) == 2 and reaction in ('\u23ed', '\u23ee'): + continue + + await self.message.add_reaction(reaction) + + async def scroll(self, page): + if page < 0 or page >= len(self.pages): + return + self.current_page = page + await self.message.edit(embed=self.pages[page]) + + async def first_page(self): + await self.scroll(0) + + async def last_page(self): + await self.scroll(len(self.pages) - 1) + + async def next_page(self): + await self.scroll(self.current_page + 1) + + async def previous_page(self): + await self.scroll(self.current_page - 1) + + async def stop_scrolling(self): + self.scrolling = False + await self.message.delete() + await self.cmdmsg.delete() + + def react_check(self, reaction, user): + if user is None or user.id != self.author.id: + return False + + if reaction.message.id != self.message.id: + return False + + for (emoji, func) in self.reaction_emojis: + if reaction.emoji == emoji: + self.match = func + return True + return False + + async def start_scrolling(self): + if not self.scrolling: + await self.send() + else: + self.bot.loop.create_task(self.send()) + + while self.scrolling: + try: + reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0) + except asyncio.TimeoutError: + self.scrolling = False + try: + await self.message.clear_reactions() + except: + pass + finally: + break + + try: + await self.message.remove_reaction(reaction, user) + except: + pass + + await self.match() \ No newline at end of file diff --git a/localization/en_en/aliases.yaml b/localization/en_en/aliases.yaml deleted file mode 100644 index 08eeb6a..0000000 --- a/localization/en_en/aliases.yaml +++ /dev/null @@ -1,63 +0,0 @@ -# Aliases for each of the commands in the the bot(except the forgotten onces.) -# Aliases without subcommands don't need to have "aliases:", specified. -# For commands with subcommands you need to have one subcategory called aliases and one called subcommands. - - -# Music -play: [p] -now: [np, nowplaying] -queue: [q] -myqueue: [mq] -seek: [] -skip: [] -skipto: [forceskip] -stop: [] -pause: [resume] -shuffle: [] -move: [m] -remove: [rem] -DJremove: [djremove] -removeuser: [] -search: [s] -disconnect: [dc] -volume: [v, vol] -normalize: [normal] -boost: [bassboost] -history: [h] -scrub: [] - -# Misc -info: [] -musicinfo: [] -uptime: [] -ping: [] -guilds: [] -reloadlocale: [] -reloadalias: [] - -# Settings -settings: - aliases: [set] - sub_commands: - current: [show] - serverlocale: [lang] - djroles: [] - dynamicmax: [] - musicchannels: [vc] - listenchannels: [] - textchannels: [tc] - maxduration: [] - modrole: [] - resetprefix: [] - serverprefix: [] - threshold: [] - -# Cogs -cogs: - aliases: [] - sub_commands: - load: [] - unload: [] - reload: [] - reloadall: [] - shutdown: [] \ No newline at end of file diff --git a/localization/en_en/commands.yaml b/localization/en_en/commands.yaml new file mode 100644 index 0000000..f6f4a72 --- /dev/null +++ b/localization/en_en/commands.yaml @@ -0,0 +1,185 @@ +# Aliases and translated versions of help messages and arguments +# for each of the commands in the the bot(except the forgotten onces.) +# Currently no fallback so everything has to be translated. + +# Help +help: + aliases: [help] + args: '' + description: 'Shows a help message.' + + +# Music +play: + aliases: [play, p] + args: '' # This is just a cosmetic representation of the arguments. + description: 'Searches and plays a song from a given query.' +now: + aliases: [now, np, nowplaying] + description: 'Show the song currently playing.' +queue: + aliases: [queue, q] + args: '[user]' + description: 'Shows the queue. Shows a user queue if user is specified.' +myqueue: + aliases: [myqueue, mq] + description: 'Shows your queue.' +seek: + aliases: [seek] + args: '' + description: 'Seek forward or backwards in the song.' +skip: + aliases: [skip] + description: 'Vote to skip the current song.' +skipto: + aliases: [skipto, forceskip] + args: '' + description: 'Skips to the specified position in the queue.' +stop: + aliases: [stop] + description: 'Stops playing and clears the queue.' +pause: + aliases: [pause, resume] + description: 'Pause/resume the current song.' +shuffle: + aliases: [shuffle] + description: 'Shuffles your queue' +move: + aliases: [move, m] + args: ' ' + description: 'Moves a track in your queue.' +remove: + aliases: [rem] + args: '' + description: 'Removes a song from your queue.' +DJremove: + aliases: [DJremove, djremove] + args: '' + description: 'Removes a song from the global queue.' +removeuser: + aliases: [removeuser] + args: '' + description: 'Removes all songs queued by .' +search: + aliases: [search, s] + args: '' + description: 'Returns a list of possible songs.' +disconnect: + aliases: [disconnect, dc] + description: 'Clears the queue, stops playing and leaves the current voice channel.' +volume: + aliases: [volume, v, vol] + args: '' + description: 'Sets the volume of the music.' +normalize: + aliases: [nomralize, normal] + description: 'Resets the volume and the equalizer.' +boost: + aliases: [boost, bassboost] + args: + description: 'Turns on or off bass boost' +history: + aliases: [history, h] + description: 'Shows the last 10 songs played.' +scrub: + aliases: [scrub] + description: 'Shows a temporary controller/scrubber.' + +# Misc +info: + aliases: [info] + description: 'Shows info about the bot.' +musicinfo: + aliases: [musicinfo] + description: 'Shows music related info about the bot.' +uptime: + aliases: [uptime] + description: 'Shows how long the bot has been running.' +ping: + aliases: [ping] + description: 'Get the current latency of the bot.' +guilds: + aliases: [guilds] + description: 'Show the guilds the bot is in' +reloadlocale: + aliases: [reloadlocale] + description: 'Reload all response translations' +reloadalias: + aliases: [reloadalias] + description: 'Reload all aliases and help information.' + +# Settings +settings: + aliases: [settings, set] + description: 'Manage server settings.' + sub_commands: + current: + aliases: [current, show] + description: 'Show current settings.' + serverlocale: + aliases: [serverlocale, lang] + args: '' + description: 'Set the server language.' + djroles: + aliases: [djroles] + args: '[roles...]' + description: 'Set the DJ roles for the server.' + dynamicmax: + aliases: [dynamicmax] + args: '' + description: 'Choose if the max song duration is dynamic.' + musicchannels: + aliases: [musicchannels, vc] + args: '[voicechannels...]' + description: 'Set the channels where the bot will play music.' + listenchannels: + aliases: [listenchannels] + args: '[voicechannels...]' + description: 'Set the channels where the bot will mute people who join.' + textchannels: + aliases: [textchannels, tc] + args: '[textchannels...]' + description: 'Set the channels where the bot will accept music commands.' + maxduration: + aliases: [maxduration] + args: '' + description: 'Max song length that can be queued in minutes.' + modrole: + aliases: [modrole] + args: '[roles...]' + description: 'Set the modroles for the server.' + resetprefix: + aliases: [resetprefix] + description: 'reset the prefixes on the server to the bots default.' + serverprefix: + aliases: [serverprefix] + args: '[prefixes...]' + description: 'Set serverprefixes. Not listing any prefixes will show the current ones.' + threshold: + aliases: [threshold] + args: '' + description: 'Set the vote threshold required to skip a song.' + +# Cogs +cogs: + aliases: [cogs] + description: 'cog management' + sub_commands: + load: + aliases: [load] + args: '' + description: 'Loads a cog' + unload: + aliases: [unload] + args: '' + description: 'Unloads a cog' + reload: + aliases: [reload] + args: '' + description: 'Reloads a cog' + reloadall: + aliases: [reloadall] + description: 'Reload all cogs' + shutdown: + aliases: [shutdown] + description: 'Shuts down the bot' \ No newline at end of file diff --git a/localization/en_en/help.yaml b/localization/en_en/help.yaml new file mode 100644 index 0000000..76adf96 --- /dev/null +++ b/localization/en_en/help.yaml @@ -0,0 +1 @@ +pageindicator: Viewing page {_current}/{_total} | Type "{_prefix}help " for more information \ No newline at end of file diff --git a/localization/en_en/music.yaml b/localization/en_en/music.yaml index 08aa8e8..8c87378 100644 --- a/localization/en_en/music.yaml +++ b/localization/en_en/music.yaml @@ -23,8 +23,8 @@ queue: user_empty: "{_user}'s queue is empty" length: "**Queue** `{_length} songs`" userqueue: "**{_user}\\'s queue** `{_length}`" - globaltrack: "`{_index}.` **[{_title}]({_uri})** _by <@{_user_id}>_\n" - usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**\n" + globaltrack: "`{_index}.` **[{_title}]({_uri})** _by <@{_user_id}>_" + usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**" pageindicator: Viewing page {_current}/{_total} my_queue: Your queue is empty resume: diff --git a/localization/nb_no/aliases.yaml b/localization/nb_no/aliases.yaml deleted file mode 100644 index 994e3b9..0000000 --- a/localization/nb_no/aliases.yaml +++ /dev/null @@ -1,43 +0,0 @@ - -# Music aliases -play: [p, spill, s] -now: [np, nå] -queue: [q, kø, k] -myqueue: [mq, minkø, mk] -seek: [spol] -skip: [hopp] -skipto: [forceskip, hopptil, tvinghopp] -stop: [stopp] -pause: [resume, gjenoppta] -shuffle: [stokk] -move: [m, flytt, f] -remove: [rem, fjern] -DJremove: [DJfjern, djfjern] -removeuser: [fjernbruker] -search: [søk, finn] -disconnect: [dc, kf, koblefra] -volume: [vol, volum, lydstyrke, lyd] -normalize: [normal, nl, normaliser] -boost: [bassboost, bassforsterker] -history: [h, hist, historie] -scrub: [kontroller, konsoll] - -# Other -info: [informasjon] -musicinfo: [musikkinfo, musikkinformasjon] - -settings: - aliases: [innstillinger] - sub_commands: - serverlocale: [språk] - current: [gjeldende] - djroles: [djroller] - dynamicmax: [dynamiskmaks] - listenchannels: [] - maxduration: [maksvarighet] - modrole: [modrolle] - musicchannels: [musikkanaler] - resetprefix: [] - serverprefix: [serverprefiks] - textchannels: [tekstkanaler] - threshold: [grense] \ No newline at end of file diff --git a/localization/nb_no/commands.yaml b/localization/nb_no/commands.yaml new file mode 100644 index 0000000..2613625 --- /dev/null +++ b/localization/nb_no/commands.yaml @@ -0,0 +1,185 @@ +# Aliases for each of the commands in the the bot(except the forgotten onces.) +# Aliases without subcommands don't need to have "aliases:", specified. +# For commands with subcommands you need to have one subcategory called aliases and one called subcommands. + +# Help +help: + aliases: [hjelp] + args: '' + description: 'Viser en hjelp-melding.' + + +# Music +play: + aliases: [spill, s] + args: '' # This is just a cosmetic representation of the arguments. + description: 'Søker opp og spiller av en sang.' +now: + aliases: [nå, spillernå] + description: 'Viser hvilken sang som spilles.' +queue: + aliases: [kø, k] + args: '[bruker]' + description: 'Viser køen. Viser en brukerkø om bruker er spesifisert.' +myqueue: + aliases: [minkø, mk] + description: 'Viser din personlige kø.' +seek: + aliases: [spol] + args: '' + description: 'Spol frem eller tilbake i sangen.' +skip: + aliases: [hopp] + description: 'Stem for å hoppe over sangen som spilles.' +skipto: + aliases: [hopptil, tvinghopp] + args: '' + description: 'Hopper over til sangen i den spesifiserte køposisjonen.' +stop: + aliases: [stopp] + description: 'Stopper avspillingen og tømmer køen.' +pause: + aliases: [pause, fortsett] + description: 'Pauser/fortsetter den nåværende sangen.' +shuffle: + aliases: [omstokk, stokkom, bland] + description: 'Stokker om rekkefølgen til den personlige køen din.' +move: + aliases: [flytt, f] + args: ' ' + description: 'Flytter en sang i den personlige køen din.' +remove: + aliases: [fjern] + args: '' + description: 'Fjerner en sang fra personlige køen din.' +DJremove: + aliases: [DJfjern, djfjern] + args: '' + description: 'Fjerner en sang fra køen.' +removeuser: + aliases: [fjernbruker] + args: '' + description: 'Fjerner alle sangene i køen som er lagt til av en spesifisert bruker.' +search: + aliases: [søk, finn] + args: '' + description: 'Søker opp og viser en liste med mulige sanger.' +disconnect: + aliases: [koblefra, kf] + description: 'Tømmer køen, stopper avspillingen og forlater talekanalen.' +volume: + aliases: [volum, v, vol, lydnivå] + args: '' + description: 'Setter volumet på musikken.' +normalize: + aliases: [normaliser, normal] + description: 'Tilbakestiller volumet og equalizer til standardinnstillinger.' +boost: + aliases: [bassforsterker, forsterkbass] + args: '' + description: 'Skrur av/på bassforsterkeren' +history: + aliases: [historikk, avspillingshistorikk, h] + description: 'Viser en liste over de 10 siste avspilte sangene.' +scrub: + aliases: [kontroller] + description: 'Gir deg en kontroller som du kan styre musikken med.' + +# Misc +info: + aliases: [info, botinfo, båttinfo] + description: 'Gir deg informasjon om båtten.' +musicinfo: + aliases: [musikkinfo] + description: 'Gir deg informasjon om musikkspilleren til båtten.' +uptime: + aliases: [oppetid] + description: 'Viser hvor lenge båtten har kjørt.' +ping: + aliases: [ping] + description: 'Sjekker responstiden til båtten.' +guilds: + aliases: [tenarar] + description: 'Sender en liste over tenarane som båtten er medlem av' +reloadlocale: + aliases: [reloadlocale] + description: 'Laser inn alle oversetningsfilene på nytt' +reloadalias: + aliases: [reloadalias] + description: 'Laster inn aliaser og hjelp på nytt.' + +# Settings +settings: + aliases: [innstillinger, oppsett] + description: 'Behandle serverinnstillinger.' + sub_commands: + current: + aliases: [vis, gjeldende] + description: 'Viser gjeldende innstillinger.' + serverlocale: + aliases: [språk] + args: '' + description: 'Endrer serverspråket.' + djroles: + aliases: [djroller, DJroller] + args: '[roller...]' + description: 'Setter DJ-rollene på serveren.' + dynamicmax: + aliases: [dynamiskmaks] + args: '' + description: 'Velg om maks sanglengde skal være dynamisk eller ikke.' + musicchannels: + aliases: [musikk-kanal] + args: '[talekanaler...]' + description: 'Setter kanalene som båtten kan spille musikk i.' + listenchannels: + aliases: [lyttekanaler, lk] + args: '[talekanaler...]' + description: 'Setter kanalene hvor båtten vil automatisk dempe alle som kobler seg til den.' + textchannels: + aliases: [tekstkanaler, tk] + args: '[tekstkanaler...]' + description: 'Setter kanalene hvor man kan bruke musikk-kommandoer.' + maxduration: + aliases: [makslengde] + args: '' + description: 'Setter makslengden på sanger som kan bli lagt til i køen.' + modrole: + aliases: [modrolle] + args: '[roller...]' + description: 'Setter serverens modrolle.' + resetprefix: + aliases: [resetprefiks] + description: 'Tilbakestiller serverprefiksen til båtten sin standardprefiks.' + serverprefix: + aliases: [serverprefiks] + args: '[prefikser...]' + description: 'Setter serverprefiksen. Om ingen prefiks blir gitt, vil den vise serverens foreløpige satte prefikser.' + threshold: + aliases: [stemmegrense] + args: '' + description: 'Setter grensen for hvor mange prosent som må stemme for å hoppe over en sang.' + +# Cogs +cogs: + aliases: [cogs] + description: 'behandle cogs' + sub_commands: + load: + aliases: [load] + args: '' + description: 'Laster inn en cog' + unload: + aliases: [unload] + args: '' + description: 'Unloads a cog' + reload: + aliases: [reload] + args: '' + description: 'Laster en cog inn på nytt.' + reloadall: + aliases: [reloadall] + description: 'Laser inn alle cogs på nytt.' + shutdown: + aliases: [shutdown] + description: 'Stopper båtten.' \ No newline at end of file diff --git a/localization/nb_no/help.yaml b/localization/nb_no/help.yaml new file mode 100644 index 0000000..00be282 --- /dev/null +++ b/localization/nb_no/help.yaml @@ -0,0 +1 @@ +pageindicator: Viser side {_current}/{_total} | Skriv "{_prefix}hjelp " for mer informasjon \ No newline at end of file diff --git a/localization/nb_no/music.yaml b/localization/nb_no/music.yaml index 0a7cce3..cb837be 100644 --- a/localization/nb_no/music.yaml +++ b/localization/nb_no/music.yaml @@ -23,8 +23,8 @@ queue: user_empty: "{_user} sin kø er tom" length: "**Kø** `{_length} sanger`" userqueue: "**{_user} sin kø** `{_length}` sanger" - globaltrack: "`{_index}.` **[{_title}]({_uri})** _av <@{_user_id}>_\n" - usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**\n" + globaltrack: "`{_index}.` **[{_title}]({_uri})** _av <@{_user_id}>_" + usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**" pageindicator: Viser side {_current}/{_total} my_queue: Din kø er tom resume: diff --git a/localization/nn_no/commands.yaml b/localization/nn_no/commands.yaml new file mode 100644 index 0000000..50d629d --- /dev/null +++ b/localization/nn_no/commands.yaml @@ -0,0 +1,66 @@ +# Aliases for each of the commands in the the bot(except the forgotten onces.) +# Aliases without subcommands don't need to have "aliases:", specified. +# For commands with subcommands you need to have one subcategory called aliases and one called subcommands. + + +# Music +play: + aliases: [play, p] + args: + description: Searches and plays a song from a given query. +now: [now, np, nowplaying] +queue: [queue, q] +myqueue: [myqueue, mq] +seek: [seek] +skip: [skip] +skipto: [skipto, forceskip] +stop: [stop] +pause: [pause, resume] +shuffle: [shuffle] +move: [move, m] +remove: [rem] +DJremove: [DJremove, djremove] +removeuser: [removeuser] +search: [search, s] +disconnect: [disconnect, dc] +volume: [volume, v, vol] +normalize: [nomralize, normal] +boost: [boost, bassboost] +history: [history, h] +scrub: [scrub] + +# Misc +info: [info] +musicinfo: [musicinfo] +uptime: [uptime] +ping: [ping] +guilds: [guilds] +reloadlocale: [reloadlocale] +reloadalias: [reloadalias] + +# Settings +settings: + aliases: [settings, set] + sub_commands: + current: [current, show] + serverlocale: [serverlocale, lang] + djroles: [djroles] + dynamicmax: [dynamicmax] + musicchannels: [musicchannels, vc] + listenchannels: [listenchannels] + textchannels: [textchannels, tc] + maxduration: [maxduration] + modrole: [modrole] + resetprefix: [resetprefix] + serverprefix: [serverprefix] + threshold: [threshold] + +# Cogs +cogs: + aliases: [cogs] + sub_commands: + load: [load] + unload: [unload] + reload: [reload] + reloadall: [reloadall] + shutdown: [shutdown] \ No newline at end of file diff --git a/localization/nn_no/music.yaml b/localization/nn_no/music.yaml index d30191a..b0b5079 100644 --- a/localization/nn_no/music.yaml +++ b/localization/nn_no/music.yaml @@ -23,8 +23,8 @@ queue: user_empty: "{_user} sin kø er tom" length: "**Kø** `{_length} songar`" userqueue: Køen til **{_user}** `{_length}` songar - globaltrack: "`{_index}.` **[{_title}]({_uri})** _av <@{_user_id}>_\n" - usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**\n" + globaltrack: "`{_index}.` **[{_title}]({_uri})** _av <@{_user_id}>_" + usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**" pageindicator: Viser side {_current}/{_total} my_queue: Din kø er tom resume: