diff --git a/bot.py b/bot.py index cf2b8ac..dc586dc 100644 --- a/bot.py +++ b/bot.py @@ -13,9 +13,7 @@ import aiohttp import yaml # Bot Utilities -from cogs.utils.alias import Aliaser -from cogs.utils.context import Context -from cogs.utils.localizer import Localizer, LocalizerWrapper +from cogs.utils.localisation import Aliaser, LocalizedContext, Localizer, LocalizerWrapper from cogs.utils.logger import BotLogger from cogs.utils.settingsmanager import Settings @@ -73,7 +71,7 @@ class Bot(commands.Bot): await self.process_commands(message) async def process_commands(self, message): - ctx = await self.get_context(message, cls=Context) + ctx = await self.get_context(message, cls=LocalizedContext) # Replace aliases with commands ctx = self.aliaser.get_command(ctx) diff --git a/cogs/cogmanager.py b/cogs/cogmanager.py index f811fa0..b20eb17 100644 --- a/cogs/cogmanager.py +++ b/cogs/cogmanager.py @@ -5,7 +5,7 @@ import traceback # Bot Utilities from cogs.helpformatter import commandhelper -from cogs.utils.paginator import Scroller +from cogs.utils.userinteraction import Scroller class CogManager(commands.Cog): diff --git a/cogs/errors.py b/cogs/errors.py index 5f69570..126f395 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -8,7 +8,7 @@ from cogs.music.music_errors import WrongTextChannelError, WrongVoiceChannelErro # Bot Utilities from cogs.helpformatter import commandhelper -from cogs.utils.paginator import Scroller +from cogs.utils.userinteraction import Scroller class Errors(commands.Cog): diff --git a/cogs/helpformatter.py b/cogs/helpformatter.py index 8e42960..875e34b 100644 --- a/cogs/helpformatter.py +++ b/cogs/helpformatter.py @@ -4,7 +4,7 @@ from discord.ext import commands import re # Bot Utilities -from cogs.utils.paginator import HelpPaginator, Scroller +from .utils.userinteraction import HelpPaginator, Scroller usermention = r"<@!?\d{17,19}>" diff --git a/cogs/music/basic_commands.py b/cogs/music/basic_commands.py index ff5761a..5ec061a 100644 --- a/cogs/music/basic_commands.py +++ b/cogs/music/basic_commands.py @@ -10,8 +10,7 @@ import asyncio import re from ..utils import checks, timeformatter -from ..utils.paginator import QueuePaginator, Scroller -from ..utils.selector import Selector +from ..utils.userinteraction import QueuePaginator, Scroller, Selector from .decorators import require_playing, require_queue, require_voice_connection, voteable time_rx = re.compile('[0-9]+') diff --git a/cogs/music/extra_commands.py b/cogs/music/extra_commands.py index b4caebb..335c122 100644 --- a/cogs/music/extra_commands.py +++ b/cogs/music/extra_commands.py @@ -15,7 +15,7 @@ import urllib from bs4 import BeautifulSoup from ..utils import checks -from ..utils.paginator import Scroller, TextPaginator +from ..utils.userinteraction import Scroller, TextPaginator from .decorators import require_playing, require_voice_connection, voteable diff --git a/cogs/nodemanager.py b/cogs/nodemanager.py index d6184ab..075b371 100644 --- a/cogs/nodemanager.py +++ b/cogs/nodemanager.py @@ -9,7 +9,7 @@ import yaml # Bot Utilities from cogs.helpformatter import commandhelper -from cogs.utils.paginator import Scroller +from cogs.utils.userinteraction import Scroller from .utils.mixplayer import MixPlayer diff --git a/cogs/settings.py b/cogs/settings.py index 6e87c66..414e962 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -5,7 +5,7 @@ from discord.ext import commands # Bot Utilities from cogs.helpformatter import commandhelper from cogs.utils import checks -from cogs.utils.paginator import Scroller +from cogs.utils.userinteraction import Scroller class Settings(commands.Cog): diff --git a/cogs/utils/localisation/__init__.py b/cogs/utils/localisation/__init__.py new file mode 100644 index 0000000..696da4c --- /dev/null +++ b/cogs/utils/localisation/__init__.py @@ -0,0 +1,5 @@ +from .alias import Aliaser +from .localizer import LocalizerWrapper, Localizer +from .localizedcontext import LocalizedContext + +__all__ = ['Aliaser', 'LocalizerWrapper', 'Localizer', 'LocalizedContext'] diff --git a/cogs/utils/alias.py b/cogs/utils/localisation/alias.py similarity index 100% rename from cogs/utils/alias.py rename to cogs/utils/localisation/alias.py diff --git a/cogs/utils/context.py b/cogs/utils/localisation/localizedcontext.py similarity index 88% rename from cogs/utils/context.py rename to cogs/utils/localisation/localizedcontext.py index 3dbc92b..0071838 100644 --- a/cogs/utils/context.py +++ b/cogs/utils/localisation/localizedcontext.py @@ -2,7 +2,7 @@ from discord.ext import commands -class Context(commands.Context): +class LocalizedContext(commands.Context): def __init__(self, **kwargs): super().__init__(**kwargs) if self.message.guild: diff --git a/cogs/utils/localisation/localizer/__init__.py b/cogs/utils/localisation/localizer/__init__.py new file mode 100644 index 0000000..221b371 --- /dev/null +++ b/cogs/utils/localisation/localizer/__init__.py @@ -0,0 +1,4 @@ +from .localizer import Localizer +from .localizerwrapper import LocalizerWrapper + +__all__ = ['LocalizerWrapper', 'Localizer'] diff --git a/cogs/utils/dict_utils.py b/cogs/utils/localisation/localizer/dict_utils.py similarity index 100% rename from cogs/utils/dict_utils.py rename to cogs/utils/localisation/localizer/dict_utils.py diff --git a/cogs/utils/localizer.py b/cogs/utils/localisation/localizer/localizer.py similarity index 88% rename from cogs/utils/localizer.py rename to cogs/utils/localisation/localizer/localizer.py index c3a444e..a61beb2 100644 --- a/cogs/utils/localizer.py +++ b/cogs/utils/localisation/localizer/localizer.py @@ -9,7 +9,7 @@ from os import path import yaml # Bot Utilities -from cogs.utils.dict_utils import SafeDict, flatten +from .dict_utils import SafeDict, flatten """ Localizer for bot @@ -152,19 +152,3 @@ class Localizer: def format_embed(self, embed, lang=None, prefix=None, **kvpairs): raw = embed.to_dict() return Embed.from_dict(self.format_dict(raw, lang, prefix, **kvpairs)) - - -class LocalizerWrapper: - def __init__(self, localizer, lang=None, prefix=None): - self.localizer = localizer - self.lang = lang - self.prefix = prefix - - def format_str(self, s, **kvpairs): - return self.localizer.format_str(s, self.lang, self.prefix, **kvpairs) - - def format_dict(self, d, **kvpairs): - return self.localizer.format_dict(d, self.lang, self.prefix, **kvpairs) - - def format_embed(self, embed, **kvpairs): - return self.localizer.format_embed(embed, self.lang, self.prefix, **kvpairs) diff --git a/cogs/utils/localisation/localizer/localizerwrapper.py b/cogs/utils/localisation/localizer/localizerwrapper.py new file mode 100644 index 0000000..f1c769d --- /dev/null +++ b/cogs/utils/localisation/localizer/localizerwrapper.py @@ -0,0 +1,14 @@ +class LocalizerWrapper: + def __init__(self, localizer, lang=None, prefix=None): + self.localizer = localizer + self.lang = lang + self.prefix = prefix + + def format_str(self, s, **kvpairs): + return self.localizer.format_str(s, self.lang, self.prefix, **kvpairs) + + def format_dict(self, d, **kvpairs): + return self.localizer.format_dict(d, self.lang, self.prefix, **kvpairs) + + def format_embed(self, embed, **kvpairs): + return self.localizer.format_embed(embed, self.lang, self.prefix, **kvpairs) diff --git a/cogs/utils/paginator.py b/cogs/utils/paginator.py deleted file mode 100644 index 9872093..0000000 --- a/cogs/utils/paginator.py +++ /dev/null @@ -1,297 +0,0 @@ -# Discord Packages -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, **kvpairs): - 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), **kvpairs)) - 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('Paginator 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('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, player, color, member: discord.Member = None, include_current: bool = False): - self.localizer = localizer - - queue = player.global_queue() if not member else player.user_queue(member.id, dual=True) - duration = player.queue_duration(member=member, include_current=include_current) - - if member: - title = localizer.format_str("{queue.userqueue}", _user=member.display_name, _length=len(queue), - _duration=duration) - else: - title = localizer.format_str("{queue.length}", _length=len(queue), _duration=duration) - - super().__init__(max_lines=10, **{"color": color, "title": title}) - - for index, temp in enumerate(queue): - if member 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, timeout=120.0, clear_on_timeout=False, clear_on_exit=True, clear_command=True): - - if not isinstance(paginator, BasePaginator): - raise TypeError('Paginator needs to be a subclass of BasePaginator.') - - self.bot = ctx.bot - self.pages = paginator.pages - self.cmdmsg = ctx.message - self.message = ctx.message - self.channel = ctx.channel - self.author = ctx.author - - self.timeout = timeout - self.timeout_delete = clear_on_timeout - self.exit_delete = clear_on_exit - self.clear_command = clear_command - - 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 - if self.exit_delete: - await self.message.delete() - if self.clear_command: - 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=self.timeout) - except asyncio.TimeoutError: - self.scrolling = False - try: - await self.message.clear_reactions() - if self.timeout_delete: - await self.message.delete() - if self.clear_command: - await self.cmdmsg.delete() - except discord.Forbidden: - pass - finally: - break - - try: - await self.message.remove_reaction(reaction, user) - except discord.Forbidden: - pass - - await self.match() diff --git a/cogs/utils/settingsmanager/__init__.py b/cogs/utils/settingsmanager/__init__.py new file mode 100644 index 0000000..fba219e --- /dev/null +++ b/cogs/utils/settingsmanager/__init__.py @@ -0,0 +1,3 @@ +from .settingsmanager import Settings + +__all__ = ['Settings'] diff --git a/cogs/utils/settingsmanager/dictmapper.py b/cogs/utils/settingsmanager/dictmapper.py new file mode 100644 index 0000000..7832c3b --- /dev/null +++ b/cogs/utils/settingsmanager/dictmapper.py @@ -0,0 +1,37 @@ + +class DictMapper: + """ + Base class for interacting with nested dictionaries through lists of keys + E.g. mapping ["a", "b", "c"] to example_dictionary["a"]["b"]["c"] + """ + + @staticmethod + def set(d, keys, val): + key = keys[0] + if len(keys) == 1: + if val is None: + try: + d.pop(key) + except KeyError: + pass + else: + d[key] = val + return + if key in d.keys(): + if not isinstance(d[key], dict): + d[key] = {} + DictMapper.set(d[key], keys[1:], val) + else: + d[key] = {} + DictMapper.set(d[key], keys[1:], val) + + @staticmethod + def get(d, keys): + key = keys[0] + try: + if len(keys) > 1 and isinstance(d[key], dict): + return DictMapper.get(d[key], keys[1:]) + else: + return d[key] + except KeyError: + return None diff --git a/cogs/utils/settingsmanager.py b/cogs/utils/settingsmanager/settingsmanager.py similarity index 72% rename from cogs/utils/settingsmanager.py rename to cogs/utils/settingsmanager/settingsmanager.py index 2b6b9f5..ddbc29a 100644 --- a/cogs/utils/settingsmanager.py +++ b/cogs/utils/settingsmanager/settingsmanager.py @@ -6,9 +6,10 @@ import locale as localee import os import yaml +from .dictmapper import DictMapper -class Settings: +class Settings(): def __init__(self, datadir, **default_settings): self._DATA_PATH = f"{datadir}/bot/" self._SETTINGS_PATH = self._DATA_PATH + 'settings.yaml' @@ -34,35 +35,6 @@ class Settings: with codecs.open(self._SETTINGS_PATH, "r", encoding='utf8') as f: self.settings = yaml.load(f, Loader=yaml.SafeLoader) - def _set(self, d, keys, val): - key = keys[0] - if len(keys) == 1: - if val is None: - try: - d.pop(key) - except KeyError: - pass - else: - d[key] = val - return - if key in d.keys(): - if not isinstance(d[key], dict): - d[key] = {} - self._set(d[key], keys[1:], val) - else: - d[key] = {} - self._set(d[key], keys[1:], val) - - def _get(self, d, keys): - key = keys[0] - try: - if len(keys) > 1 and isinstance(d[key], dict): - return self._get(d[key], keys[1:]) - else: - return d[key] - except KeyError: - return None - def set(self, identifier, setting, value): """ Set value in settings, will overwrite any existing values. """ guild_name = None @@ -75,7 +47,7 @@ class Settings: if guild_name: self.settings[identifier]["_servername"] = guild_name - self._set(self.settings[identifier], setting.split('.'), value) + DictMapper.set(self.settings[identifier], setting.split('.'), value) with codecs.open(self._SETTINGS_PATH, 'w', encoding='utf8') as f: yaml.dump(self.settings, f, indent=2) @@ -95,7 +67,7 @@ class Settings: if identifier not in self.settings.keys(): return default - value = self._get(self.settings[identifier], setting.split('.')) + value = DictMapper.get(self.settings[identifier], setting.split('.')) if value is not None: return value else: diff --git a/cogs/utils/userinteraction/__init__.py b/cogs/utils/userinteraction/__init__.py new file mode 100644 index 0000000..5397e1b --- /dev/null +++ b/cogs/utils/userinteraction/__init__.py @@ -0,0 +1,6 @@ +from .scroller import Scroller +from .selector import Selector +from .paginators import BasePaginator, TextPaginator, FieldPaginator, HelpPaginator, QueuePaginator, CantScrollException + +__all__ = ['Scroller', 'Selector', 'QueuePaginator', 'HelpPaginator', + 'TextPaginator', 'FieldPaginator', 'CantScrollException', 'BasePaginator'] diff --git a/cogs/utils/userinteraction/paginators/__init__.py b/cogs/utils/userinteraction/paginators/__init__.py new file mode 100644 index 0000000..a335ea8 --- /dev/null +++ b/cogs/utils/userinteraction/paginators/__init__.py @@ -0,0 +1,7 @@ +from .queuepaginator import QueuePaginator +from .helppaginator import HelpPaginator +from .textpaginator import TextPaginator +from .fieldpaginator import FieldPaginator +from .basepaginator import CantScrollException, BasePaginator + +__all__ = ['QueuePaginator', 'HelpPaginator', 'TextPaginator', 'FieldPaginator', 'CantScrollException', 'BasePaginator'] diff --git a/cogs/utils/userinteraction/paginators/basepaginator.py b/cogs/utils/userinteraction/paginators/basepaginator.py new file mode 100644 index 0000000..adb5fb1 --- /dev/null +++ b/cogs/utils/userinteraction/paginators/basepaginator.py @@ -0,0 +1,37 @@ +class CantScrollException(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, **kvpairs): + 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), **kvpairs)) + 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('Paginator 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 diff --git a/cogs/utils/userinteraction/paginators/fieldpaginator.py b/cogs/utils/userinteraction/paginators/fieldpaginator.py new file mode 100644 index 0000000..979a044 --- /dev/null +++ b/cogs/utils/userinteraction/paginators/fieldpaginator.py @@ -0,0 +1,33 @@ +import discord +from .basepaginator import BasePaginator + + +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('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) diff --git a/cogs/utils/userinteraction/paginators/helppaginator.py b/cogs/utils/userinteraction/paginators/helppaginator.py new file mode 100644 index 0000000..67a4fab --- /dev/null +++ b/cogs/utils/userinteraction/paginators/helppaginator.py @@ -0,0 +1,25 @@ +import discord +from .fieldpaginator import FieldPaginator + + +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) diff --git a/cogs/utils/userinteraction/paginators/queuepaginator.py b/cogs/utils/userinteraction/paginators/queuepaginator.py new file mode 100644 index 0000000..72d1949 --- /dev/null +++ b/cogs/utils/userinteraction/paginators/queuepaginator.py @@ -0,0 +1,31 @@ +import discord +from .textpaginator import TextPaginator + + +class QueuePaginator(TextPaginator): + def __init__(self, localizer, player, color, member: discord.Member = None, include_current: bool = False): + self.localizer = localizer + + queue = player.global_queue() if not member else player.user_queue(member.id, dual=True) + duration = player.queue_duration(member=member, include_current=include_current) + + if member: + title = localizer.format_str("{queue.userqueue}", _user=member.display_name, _length=len(queue), + _duration=duration) + else: + title = localizer.format_str("{queue.length}", _length=len(queue), _duration=duration) + + super().__init__(max_lines=10, **{"color": color, "title": title}) + + for index, temp in enumerate(queue): + if member 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}") diff --git a/cogs/utils/userinteraction/paginators/textpaginator.py b/cogs/utils/userinteraction/paginators/textpaginator.py new file mode 100644 index 0000000..a396e2c --- /dev/null +++ b/cogs/utils/userinteraction/paginators/textpaginator.py @@ -0,0 +1,39 @@ +# Discord Packages +import discord + +from .basepaginator import BasePaginator + + +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 diff --git a/cogs/utils/userinteraction/scroller.py b/cogs/utils/userinteraction/scroller.py new file mode 100644 index 0000000..71ab7d5 --- /dev/null +++ b/cogs/utils/userinteraction/scroller.py @@ -0,0 +1,141 @@ + +# This was at some point *heavliy* inspired by the paginator implementation by Rapptz for r.Danny +# https://github.com/Rapptz/RoboDanny/blob/b0401e046814f42597c6e0280fef2432cc49cc5d/cogs/utils/paginator.py + +import discord +import asyncio + +from .paginators import BasePaginator, CantScrollException + + +class Scroller: + def __init__(self, ctx, paginator, timeout=120.0, clear_on_timeout=False, clear_on_exit=True, clear_command=True): + + if not isinstance(paginator, BasePaginator): + raise TypeError('Paginator needs to be a subclass of BasePaginator.') + + self.bot = ctx.bot + self.pages = paginator.pages + self.cmdmsg = ctx.message + self.message = ctx.message + self.channel = ctx.channel + self.author = ctx.author + + self.timeout = timeout + self.timeout_delete = clear_on_timeout + self.exit_delete = clear_on_exit + self.clear_command = clear_command + + 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 CantScrollException('Bot does not have embed links permission.') + + if not self.permissions.send_messages: + raise CantScrollException('Bot cannot send messages.') + + if self.scrolling: + # verify we can actually use the pagination session + if not self.permissions.add_reactions: + raise CantScrollException('Bot does not have add reactions permission.') + + if not self.permissions.read_message_history: + raise CantScrollException('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 + if self.exit_delete: + await self.message.delete() + if self.clear_command: + 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=self.timeout) + except asyncio.TimeoutError: + self.scrolling = False + try: + await self.message.clear_reactions() + if self.timeout_delete: + await self.message.delete() + if self.clear_command: + await self.cmdmsg.delete() + except discord.Forbidden: + pass + finally: + break + + try: + await self.message.remove_reaction(reaction, user) + except discord.Forbidden: + pass + + await self.match() diff --git a/cogs/utils/selector.py b/cogs/utils/userinteraction/selector.py similarity index 99% rename from cogs/utils/selector.py rename to cogs/utils/userinteraction/selector.py index db54d1c..679b7ad 100644 --- a/cogs/utils/selector.py +++ b/cogs/utils/userinteraction/selector.py @@ -4,7 +4,7 @@ import discord import asyncio import inspect -from .paginator import TextPaginator +from .paginators import TextPaginator class Selector(TextPaginator):