Initial selector implementation. Requires d.py 1.3

This commit is contained in:
Even committed 2019-10-02 22:46:36 +02:00
1 parent ab214d7428
commit bf4d20990a
8 files changed
+303 -123

No files matched your search

+6 -6
View File
@@ -9,14 +9,14 @@ class Cogs(commands.Cog):
self.settings = self.bot.settings
@commands.group(name='cogs', hidden=True)
@checks.is_owner()
@commands.is_owner()
async def _cogs(self, ctx):
if ctx.invoked_subcommand is None:
await ctx.invoke(self.bot.get_command('help'),
ctx.command.qualified_name)
@_cogs.command()
@checks.is_owner()
@commands.is_owner()
async def load(self, ctx, *, module):
"""Loads a module."""
try:
@@ -26,7 +26,7 @@ class Cogs(commands.Cog):
await ctx.send(f'```py\n{traceback.format_exc(e)}\n```')
@_cogs.command()
@checks.is_owner()
@commands.is_owner()
async def unload(self, ctx, *, module):
"""Unloads a module."""
try:
@@ -36,7 +36,7 @@ class Cogs(commands.Cog):
await ctx.send(f'```py\n{traceback.format_exc()}\n```')
@_cogs.command(name='reload')
@checks.is_owner()
@commands.is_owner()
async def _reload(self, ctx, *, module):
"""Reloads a module."""
try:
@@ -47,7 +47,7 @@ class Cogs(commands.Cog):
await ctx.send(f'```py\n{traceback.format_exc()}\n```')
@_cogs.command(name='reloadall')
@checks.is_owner()
@commands.is_owner()
async def _relaod_all(self, ctx):
"""Reloads all extensions"""
try:
@@ -61,7 +61,7 @@ class Cogs(commands.Cog):
await ctx.send(f'```py\n{traceback.format_exc()}\n```')
@_cogs.command(name='shutdown')
@checks.is_owner()
@commands.is_owner()
async def _shutdown(self, ctx):
"""Logs out and stops."""
self.bot.lavalink.players.clear()
+5 -5
View File
@@ -31,7 +31,7 @@ class Misc(commands.Cog):
await ctx.send(f'{days}d {hours}h {minutes}m {seconds}s')
@commands.command(name='guilds')
@checks.is_owner()
@commands.is_owner()
async def _guilds(self, ctx):
guilds = f"{self.bot.user.name} is in:\n"
for guild in self.bot.guilds:
@@ -41,7 +41,7 @@ class Misc(commands.Cog):
@commands.command()
async def musicinfo(self, ctx):
"""
Info om musikkspilleren
Info about the music player
"""
embed = discord.Embed(title='{music.title}', color=ctx.me.color)
lavalink = self.bot.lavalink
@@ -56,14 +56,14 @@ class Misc(commands.Cog):
await ctx.send(embed=embed)
@commands.command(name="reloadlocale")
@checks.is_owner()
@commands.is_owner()
async def reload_locale(self, ctx):
self.bot.localizer.index_localizations()
self.bot.localizer.load_localizations()
await ctx.send("Localizations reloaded.")
@commands.command(name="reloadalias")
@checks.is_owner()
@commands.is_owner()
async def reload_alias(self, ctx):
self.bot.aliaser.index_localizations()
self.bot.aliaser.load_localizations()
@@ -72,7 +72,7 @@ class Misc(commands.Cog):
@commands.command()
async def info(self, ctx):
"""
Info om Shite Music Bot
Info about the bot
"""
membercount = []
for guild in self.bot.guilds:
+95 -92
View File
@@ -20,6 +20,7 @@ from .utils import checks, RoxUtils, timeformatter
from .utils.mixplayer import MixPlayer
from .utils.paginator import QueuePaginator, TextPaginator, Scroller
from .utils.selector import Selector
time_rx = re.compile('[0-9]+')
url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+')
@@ -292,42 +293,91 @@ class Music(commands.Cog):
await ctx.send(ctx.localizer.format_str("{shuffle}"))
@commands.command(name='move')
async def _move(self, ctx, from_pos: int, to_pos: int):
async def _move(self, ctx):
""" Moves a song in your queue. """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
user_queue = player.user_queue(ctx.author.id)
if not user_queue:
return await ctx.send(ctx.localizer.format_str("{my_queue}"))
if not all(x in range(1, len(user_queue) + 1) for x in [from_pos, to_pos]):
return await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(user_queue)))
moved = player.move_user_track(ctx.author.id, from_pos - 1, to_pos - 1)
if moved is None:
return await ctx.send(ctx.localizer.format_str("{move.not_in_queue}"))
msg = ctx.localizer.format_str("{move.moved_to}", _title=moved.title, _from=from_pos, _to=to_pos)
await ctx.send(msg)
@commands.command(name='remove')
async def _remove(self, ctx, pos: int):
""" Removes an item from the player's queue with the given pos. """
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
if player.queue.empty:
return
user_queue = player.user_queue(ctx.author.id)
user_queue = player.user_queue(ctx.author.id, dual=True)
if not user_queue:
return await ctx.send(ctx.localizer.format_str("{my_queue}"))
# Setup for selector
identifiers = []
for index, temp in enumerate(user_queue):
track, globpos = temp
identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1,
_globalindex=globpos+1, _title=track.title,
_uri=track.uri))
def ret_first_arg(*args):
return args[0]
functions = [ret_first_arg]*len(user_queue)
arguments = [(i,) for i in range(len(user_queue))]
# Set the different titles for the different selections
round_titles = ["{moved.choose_song}","{moved.choose_pos}"]
round_titles = [ctx.localizer.format_str(i) for i in round_titles]
selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, round_titles=round_titles,
color=ctx.me.color, title=ctx.localizer.format_str("{moved.choose}"))
# Get the initial and final position
message, page, selections = await selector.start_scrolling()
pos_initial, pos_final = selections[0], selections[1]
# Move the track
moved = player.move_user_track(ctx.author.id, pos_initial, pos_final)
# Create a nice embed explaining what happened
song = f'**[{moved.title}]({moved.uri})**'
embed = discord.Embed(color=ctx.me.color, description=song, title='{moved.moved}')
thumbnail_url = await RoxUtils.ThumbNailer.identify(self, moved.identifier, moved.uri)
member = ctx.guild.get_member(moved.requester)
if thumbnail_url:
embed.set_thumbnail(url=thumbnail_url)
if member.nick:
member_identifier = member.nick
else:
member_identifier = member.name
embed.add_field(name="{moved.local}", value=f"`{pos_initial + 1} → {pos_final + 1}`", inline=True)
embed.add_field(name="{moved.global}", value=f"`{player.queue._loc_to_glob(ctx.author.id, pos_initial) + 1}\
→ {player.queue._loc_to_glob(ctx.author.id, pos_final) + 1}`", inline=True)
embed.set_footer(text=f'{{requested_by}} {member_identifier}', icon_url=member.avatar_url)
embed = ctx.localizer.format_embed(embed)
await message.edit(embed=embed)
await message.clear_reactions()
@commands.command(name='remove')
async def _remove(self, ctx):
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
if player.queue.empty:
return
if pos > len(user_queue) or pos < 1:
return await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(user_queue)))
user_queue = player.user_queue(ctx.author.id, dual=True)
if not user_queue:
return await ctx.send(ctx.localizer.format_str("{my_queue}"))
removed = player.remove_user_track(ctx.author.id, pos - 1)
identifiers = []
for index, temp in enumerate(user_queue):
track, globpos = temp
identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1,
_globalindex=globpos+1, _title=track.title,
_uri=track.uri))
await ctx.send(ctx.localizer.format_str("{remove}", _title=removed.title))
functions = [player.remove_user_track]*len(user_queue)
arguments = [(ctx.author.id, i) for i in range(len(user_queue))]
selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, color=0xFF0000,
title='Remove song plz')
message, page, removed = await selector.start_scrolling()
await message.edit(content=ctx.localizer.format_str("{remove}", _title=removed.title), embed=None)
await message.clear_reactions()
@commands.command(name="DJremove")
@checks.DJ_or()
@@ -382,8 +432,7 @@ class Music(commands.Cog):
await ctx.send(embed=embed)
@commands.command(name='search')
async def _search(self, ctx, *, query):
""" Lists the first 10 search results from a given query. """
async def _search(self, ctx, *, query):
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
if not query.startswith('ytsearch:') and not query.startswith('scsearch:'):
@@ -397,80 +446,32 @@ class Music(commands.Cog):
return await ctx.send(embed=embed)
tracks = results['tracks']
result_count = min(len(tracks), 5)
result_count = min(len(tracks), 15)
# Setup for the selector
identifiers = []
functions = [self.enqueue]*result_count
arguments = [(ctx, tracks[i], discord.Embed(color=ctx.me.color)) for i in range(result_count)]
search_results = ''
for index, track in enumerate(tracks, start=1):
track_title = track['info']['title']
track_uri = track['info']['uri']
duration = timeformatter.format(int(track['info']['length']))
search_results += f'`{index}.` [{track_title}]({track_uri}) `{duration}`\n'
identifiers.append(f'`{index}.` [{track_title}]({track_uri}) `{duration}`')
if index == result_count:
break
choices = [
('1\N{combining enclosing keycap}', 1),
('2\N{combining enclosing keycap}', 2),
('3\N{combining enclosing keycap}', 3),
('4\N{combining enclosing keycap}', 4),
('5\N{combining enclosing keycap}', 5),
]
choice = None
search_selector = Selector(ctx, identifiers, functions, arguments, num_selections=5,
color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
# Let the user scroll through results
message, current_page, result = await search_selector.start_scrolling()
def check(reaction, user):
if user is None or user.id != ctx.author.id:
return False
result = ctx.localizer.format_embed(result)
await message.edit(embed=result)
await message.clear_reactions()
if reaction.message.id != result_msg.id:
return False
if reaction.emoji == '❌':
return True
for (emoji, index) in choices[:result_count]:
if emoji == reaction.emoji:
nonlocal choice
choice = index
return True
return False
embed.description = '{search}'
embed = ctx.localizer.format_embed(embed)
result_msg = await ctx.send(embed=embed)
for emoji, index in choices[:result_count]:
await result_msg.add_reaction(emoji)
await result_msg.add_reaction('❌')
embed.description = search_results
embed.title = '{results}'
embed.color = ctx.me.color
embed = ctx.localizer.format_embed(embed)
await result_msg.edit(embed=embed)
try:
reaction, user = await self.bot.wait_for('reaction_add', timeout=15.0, check=check)
except asyncio.TimeoutError:
await result_msg.clear_reactions()
embed.title = ''
embed.description = '{time_expired}'
embed = ctx.localizer.format_embed(embed)
await result_msg.edit(embed=embed)
await asyncio.sleep(5)
await result_msg.delete()
else:
if choice is None:
await result_msg.delete()
else:
await result_msg.clear_reactions()
track = tracks[choice - 1]
await self.enqueue(ctx, track, embed)
embed = ctx.localizer.format_embed(embed)
await result_msg.edit(embed=embed)
if not player.is_playing:
await player.play()
if not player.is_playing:
await player.play()
@commands.command(name='disconnect')
@checks.DJ_or(alone=True)
@@ -718,7 +719,7 @@ class Music(commands.Cog):
# Create returns a player if one exists, otherwise creates.
# Add commands that require joining voice to work.
should_connect = ctx.command.callback.__name__ in ('_play', '_find', '_search')
should_connect = ctx.command.callback.__name__ in ('_play', '_find', '_search', '_sk')
# Add commands that don't require the you being in voice.
without_connect = ctx.command.callback.__name__ in ('_queue', '_history', '_now', '_lyrics')
@@ -793,6 +794,8 @@ class Music(commands.Cog):
duration = timeformatter.format(int(track.duration))
embed.description = f'[{track.title}]({track.uri})\n**{duration}**'
return embed
def max_track_length(self, guild, player):
is_dynamic = self.bot.settings.get(guild, 'duration.is_dynamic', 'default_duration_type')
maxlength = self.bot.settings.get(guild, 'duration.max', None)
+1
View File
@@ -68,6 +68,7 @@ class MusicEvents(commands.Cog):
player.update_listeners(member, after)
await self.check_leave_voice(member.guild)
async def check_leave_voice(self, guild):
""" Checks if the bot should leave the voice channel """
# TODO, disconnect timer?
-1
View File
@@ -184,7 +184,6 @@ class Settings(commands.Cog):
embed.description = f'Static'
await ctx.send(embed=embed)
@checks.is_admin()
@commands.guild_only()
@_set.command(name='current')
async def current_settings(self, ctx):
-16
View File
@@ -28,22 +28,6 @@ def has_guild_permissions(*, check=all, **perms):
return commands.check(pred)
def is_even():
def predicate(ctx):
return ctx.message.author.id == 142212883512557569
return commands.check(predicate)
def is_owner():
def predicate(ctx):
is_owner = (ctx.message.author.id == 120970603556503552 or
ctx.message.author.id == 142212883512557569 or
ctx.message.author.id == 212635519706726410 or
ctx.message.author.id == 170506717140877312)
return is_owner
return commands.check(predicate)
def is_admin():
async def pred(ctx):
return await check_guild_permissions(ctx, {'administrator': True})
+190
View File
@@ -0,0 +1,190 @@
import discord
import asyncio
import itertools
import inspect
from .paginator import *
class Selector(TextPaginator):
def __init__(self, ctx, identifiers, functions, arguments, terminate_on_select=True, num_selections=3,
round_titles=[], max_size=2000, **embed_base):
"""
Selector, tool for making interactive menus.
:param ctx: discord.py context
:param identifiers: A list of strings, one for each menu item
:param functions: List of functions, one for each menu item. Gets run when the corresponding
menu item is selected
:param arguments: List of arguments to pass to the corresponding function in functions.
:param terminate_on_select:
:param num_selections: the number of selectable items per page
:param round_titles: A list of embed titles, updates each time a menu selection is made
:param max_size: The max amount of characters per page.
:param embed_base: Arguments that specify the look of the scroller embeds
"""
self.match = None
self.current_page = 0
self.ctx = ctx
self.bot = ctx.bot
self.cmdmsg = ctx.message
self.message = ctx.message
self.channel = ctx.channel
self.author = ctx.author
self.localizer = ctx.localizer
self.num_selections = num_selections
self.terminate_on_select = terminate_on_select
self.round_titles = round_titles
assert len(identifiers) == len(functions) == len(arguments)
super().__init__(max_size=max_size, max_lines=num_selections, **embed_base)
self.selections = list(zip(identifiers, functions, arguments))
self.max_selections = min(len(identifiers), num_selections)
for i, selection in enumerate(self.selections):
self.add_line(f"{i%num_selections+1}\N{combining enclosing keycap} {selection[0]}")
self.close_page()
if len(self.pages) > 1:
self.multipage = True
else:
self.multipage = False
assert num_selections <= 10
self.select_emojis = [f"{i+1}\N{combining enclosing keycap}" for i in range(num_selections)]
self.control_emojis = [
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
('❌', self.stop_scrolling),
]
self.timeout = 30
self.args = None
self.control_input = False
self.scrolling = True
self.stopped = False
self.add_page_indicator(self.localizer, "{queue.pageindicator}")
if self.round_titles:
self.update_embed_title(self.round_titles.pop(0))
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 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.clear_reactions()
def update_embed_title(self, updated_title):
for page in self.pages:
page.title = updated_title
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 i, emoji in enumerate(self.select_emojis):
if emoji == reaction.emoji:
choice = i + self.current_page * self.num_selections
if choice >= len(self.selections):
return True
self.match = self.selections[choice][1]
self.args = self.selections[choice][2]
return True
for (emoji, func) in self.control_emojis:
if reaction.emoji == emoji:
self.match = func
self.control_input = True
return True
return False
async def send(self):
# No embeds to scroll through
if not self.pages:
return
self.message = await self.channel.send(embed=self.pages[0])
for reaction in self.select_emojis[:self.max_selections]:
if not self.stopped:
await self.message.add_reaction(reaction)
if self.multipage:
for reaction in list(zip(*self.control_emojis))[0]:
if not self.stopped:
await self.message.add_reaction(reaction)
else:
if not self.stopped:
await self.message.add_reaction('❌')
async def start_scrolling(self):
result = None
if not self.scrolling:
await self.send()
else:
self.bot.loop.create_task(self.send())
current_round = 0
results = []
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()
except:
pass
finally:
break
try:
await self.message.remove_reaction(reaction, user)
except:
pass
if self.match:
if inspect.iscoroutinefunction(self.match):
if self.args:
result = await self.match(*self.args)
else:
result = await self.match()
else:
if self.args:
result = self.match(*self.args)
else:
result = self.match()
else:
continue
if self.terminate_on_select and not self.control_input:
current_round += 1
results.append(result)
if not self.round_titles:
self.stopped = True
await self.message.clear_reactions()
break
if self.round_titles:
self.update_embed_title(self.round_titles.pop(0))
await self.scroll(self.current_page)
self.args = None
self.control_input = False
if len(results) == 1:
return self.message, self.pages[self.current_page], results[0]
else:
return self.message, self.pages[self.current_page], results
+6 -3
View File
@@ -32,9 +32,12 @@ resume:
paused: "⏯ | Paused"
shuffle: Your queue has been shuffled
out_of_range: Positions must be between 1 and {_len}
move:
not_in_queue: Check that the positions exist in your queue
moved_to: "{_title} moved from position {_from} to {_to} in your queue"
moved:
choose_song: "**Select the song to move**"
choose_pos: "**Select where to move it**"
moved: "**Song moved**"
local: "User queue:"
global: "Server queue:"
remove: "**{_title}** removed."
dj_removed: "**{_title}** queued by {_user} removed."
dj_remove_user: Removed all songs queued by <@{_id}>.