From 2cb03065a38beea13d43f9d404607f33aa1e456a Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Thu, 10 Sep 2020 16:51:00 +0200 Subject: [PATCH] Move to decorators for command requirements Cleans up music.py quite a bit, makes it easier to define the properties you want when creating a new command. Some other small changes to reduce code duplication. --- .vscode/settings.json | 4 +- cogs/errors.py | 6 + cogs/music.py | 281 +++++++++++---------------------------- cogs/utils/checks.py | 3 +- cogs/utils/decorators.py | 124 +++++++++++++++++ cogs/utils/selector.py | 4 + 6 files changed, 219 insertions(+), 203 deletions(-) create mode 100644 cogs/utils/decorators.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f76c37..5978804 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { "python.languageSever": "Pylance", "python.linting.enabled": true, - "python.linting.pylintEnabled": true, + "python.linting.pylintEnabled": false, "python.linting.flake8Enabled": true, - "python.linting.pycodestyleEnabled": true, + "python.linting.pycodestyleEnabled": false, "[python]": { "editor.rulers": [120], }, diff --git a/cogs/errors.py b/cogs/errors.py index 92117d7..a44fcc7 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -57,6 +57,12 @@ class Errors(commands.Cog): elif (err.original == 'You need to be in my voicechannel.'): return await send_error_embed('{errors.my_channel}') + elif (err.original == 'Not playing'): + return await send_error_embed('{not_playing}') + + elif (err.original == 'Not listening'): + return await send_error_embed('{have_to_listen}') + if isinstance(err, commands.CommandOnCooldown): await ctx.send(f"{ctx.message.author.mention} Command is on cooldown. " f"Try again in `{err.retry_after:.1f}` seconds.") diff --git a/cogs/music.py b/cogs/music.py index 79d7370..32ee0f9 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -11,7 +11,6 @@ import json import math import re import urllib -from typing import Optional from bs4 import BeautifulSoup @@ -19,6 +18,9 @@ from .utils import checks, thumbnailer, timeformatter from .utils.paginator import QueuePaginator, Scroller, TextPaginator from .utils.selector import Selector +from .utils.decorators import require_voice_connection, require_playing, require_queue + + time_rx = re.compile('[0-9]+') url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') @@ -31,14 +33,15 @@ class Music(commands.Cog): async def cog_check(self, ctx): if not ctx.guild: raise commands.NoPrivateMessage - textchannels = self.bot.settings.get(ctx.guild, 'channels.text', []) - if textchannels: + if textchannels := self.bot.settings.get(ctx.guild, 'channels.text', []): if ctx.channel.id not in textchannels: return False return True async def cog_before_invoke(self, ctx): - await self.ensure_voice(ctx) + """ Ensures a valic player exists whenever a command is run """ + region = ctx.guild.region if isinstance(ctx.guild.region, str) else ctx.guild.region.value + self.bot.lavalink.player_manager.create(ctx.guild.id, endpoint=region) async def connect_to(self, guild_id: int, channel_id: str): """ Connects to the given voicechannel ID. A channel_id of `None` means disconnect. """ @@ -46,6 +49,7 @@ class Music(commands.Cog): await ws.voice_state(str(guild_id), channel_id) @commands.command(name='play') + @require_voice_connection(should_connect=True) async def _play(self, ctx, *, query: str): """ Searches and plays a song from a given query. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) @@ -64,8 +68,7 @@ class Music(commands.Cog): if results['loadType'] == 'PLAYLIST_LOADED': tracks = results['tracks'] - maxlength = self.max_track_length(ctx.guild, player) - if maxlength: + if maxlength := self.max_track_length(ctx.guild, player): numtracks = 0 for track in tracks: if track['info']['length'] <= maxlength: @@ -97,37 +100,29 @@ class Music(commands.Cog): @commands.command(name='seek') @checks.dj_or(alone=True, track_requester=True) + @require_voice_connection() + @require_playing(ensure_user_listening=True) async def _seek(self, ctx, *, time: str): """ Seeks to a given position in a track. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) + if seconds := time_rx.search(time): + # Convert to milliseconds, include sign + milliseconds = int(seconds.group())*1000 * (-1 if time.startswith('-1') else 1) - if not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - - if ctx.author not in player.listeners: - return await ctx.send(ctx.localizer.format_str("{have_to_listen}")) - - seconds = time_rx.search(time) - if not seconds: - return await ctx.send(ctx.localizer.format_str("{seek.missing_amount}")) - - seconds = int(seconds.group()) * 1000 - if time.startswith('-'): - seconds *= -1 - - track_time = player.position + seconds - await player.seek(track_time) - msg = ctx.localizer.format_str("{seek.track_moved}", _position=timeformatter.format_ms(track_time)) - await ctx.send(msg) + track_time = player.position + milliseconds + await player.seek(track_time) + msg = ctx.localizer.format_str("{seek.track_moved}", _position=timeformatter.format_ms(track_time)) + await ctx.send(msg) + else: + await ctx.send(ctx.localizer.format_str("{seek.missing_amount}")) @commands.command(name='skip') + @require_voice_connection() + @require_playing() async def _skip(self, ctx): """ Skips the current track. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - player.add_skipper(ctx.author) total = len(player.listeners) skips = len(player.skip_voters) @@ -160,46 +155,39 @@ class Music(commands.Cog): @commands.command(name='skipto') @checks.dj_or(alone=True) + @require_playing(ensure_user_listening=True) async def _skip_to(self, ctx, pos: int = 1): """ Plays the queue from a specific point. Disregards tracks before the pos. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if ctx.author not in player.listeners: - return await ctx.send(ctx.localizer.format_str("{have_to_listen}")) - if pos < 1: return await ctx.send(ctx.localizer.format_str("{skip_to.invalid_pos}")) if len(player.queue) < pos: return await ctx.send(ctx.localizer.format_str("{skip_to.exceeds_queue}")) + await player.skip(pos - 1) msg = ctx.localizer.format_str("{skip_to.skipped_to}", _title=player.current.title, _pos=pos) await ctx.send(msg) @commands.command(name='stop') @checks.dj_or(alone=True) + @require_voice_connection() + @require_playing(ensure_user_listening=True) async def _stop(self, ctx): """ Stops the player and clears its queue. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - if ctx.author not in player.listeners: - return await ctx.send(ctx.localizer.format_str("{have_to_listen}")) - - if not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - player.queue.clear() await player.stop() + embed = discord.Embed(color=ctx.me.color, title='{stop}') await ctx.send(embed=ctx.localizer.format_embed(embed)) @commands.command(name='now') + @require_playing() async def _now(self, ctx): """ Shows some stats about the currently playing song. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if not player.current: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - position = timeformatter.format_ms(player.position) if player.current.stream: duration = '{live}' @@ -223,51 +211,43 @@ class Music(commands.Cog): await ctx.send(embed=embed) @commands.command(name='queue') - async def _queue(self, ctx, member: discord.Member = None): + @require_queue(require_member_queue=True) + async def _queue(self, ctx, *, member: discord.Member = None): """ Shows the player's queue. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - if player.queue.empty: - embed = discord.Embed(description='{queue.empty}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - return await ctx.send(embed=embed) - if member is None: queue = player.global_queue() pagified_queue = QueuePaginator(ctx.localizer, queue, color=ctx.me.color) else: - user_queue = player.user_queue(member.id, dual=True) - if not user_queue: - return await ctx.send(ctx.localizer.format_str("{queue.empty}", _user=member.display_name)) - pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, + member_queue = player.user_queue(member.id, dual=True) + pagified_queue = QueuePaginator(ctx.localizer, member_queue, color=ctx.me.color, user_name=member.display_name) scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() @commands.command(name='myqueue') + @require_queue(require_author_queue=True) async def _myqueue(self, ctx): """ Shows your queue. """ player = self.bot.lavalink.player_manager.get(ctx.guild.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}")) - - pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, user_name=ctx.author.name) + pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, + user_name=ctx.author.display_name) scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() + @commands.command(name='pause') @checks.dj_or(alone=True) + @require_voice_connection() + @require_playing() async def _pause(self, ctx): """ Pauses/Resumes the current track. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - if player.paused: await player.set_pause(False) await ctx.send(ctx.localizer.format_str("{resume.resumed}")) @@ -276,29 +256,23 @@ class Music(commands.Cog): await ctx.send(ctx.localizer.format_str("{resume.paused}")) @commands.command(name='shuffle') + @require_voice_connection() + @require_queue(require_author_queue=True) async def _shuffle(self, ctx): """ Shuffles 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}")) - player.shuffle_user_queue(ctx.author.id) await ctx.send(ctx.localizer.format_str("{shuffle}")) @commands.command(name='move') + @require_voice_connection() + @require_queue(require_author_queue=True) async def _move(self, ctx): """ Moves a song in your queue. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if player.queue.empty: - return - 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): @@ -346,19 +320,16 @@ class Music(commands.Cog): embed = ctx.localizer.format_embed(embed) await message.edit(embed=embed) - await message.clear_reactions() @commands.command(name='remove') + @require_voice_connection() + @require_queue(require_author_queue=True) async def _remove(self, ctx): player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if player.queue.empty: - return - user_queue = player.user_queue(ctx.author.id, dual=True) - if not user_queue: - return await ctx.send(ctx.localizer.format_str("{my_queue}")) - + functions = [player.remove_user_track]*len(user_queue) + arguments = [(ctx.author.id, i) for i in range(len(user_queue))] identifiers = [] for index, temp in enumerate(user_queue): track, globpos = temp @@ -366,68 +337,49 @@ class Music(commands.Cog): _globalindex=globpos+1, _title=track.title, _uri=track.uri)) - 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() + message, _, 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() - async def _djremove(self, ctx, pos: int, user: discord.Member = None): + @require_voice_connection() + @require_queue() + async def _djremove(self, ctx, pos: int, member: discord.Member = None): """ Remove a song from either the global queue or a users queue""" player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if user is None: - if player.queue.empty: - return + # TODO: add way to allow "member" as regular arg in require_queue() instead of only kwargs + queue = player.user_queue(member.id) if member else player.queue - if pos > len(player.queue) or pos < 1: - return await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(player.queue))) + if pos <= len(queue) and pos >= 1: + if member is None: + removed = player.remove_global_track(pos - 1) + else: + removed = player.remove_user_track(member.id, pos - 1) - removed = player.remove_global_track(pos - 1) requester = self.bot.get_user(removed.requester) await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.name)) - else: - if player.queue.empty: - return - - user_queue = player.user_queue(user.id) - if not user_queue: - return - - if pos > len(user_queue) or pos < 1: - return await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(user_queue))) - - removed = player.remove_user_track(user.id, pos - 1) - await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=removed.requester.name)) + await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue))) @commands.command(name='removeuser') @checks.dj_or(alone=True) - async def _user_queue_remove(self, ctx, user: discord.Member): + @require_voice_connection() + @require_queue(require_member_queue=True) + async def _user_queue_remove(self, ctx, *, member: discord.Member): """ Remove a song from either the global queue or a users queue""" player = self.bot.lavalink.player_manager.get(ctx.guild.id) + player.remove_user_queue(member.id) - if player.queue.empty: - return - - user_queue = player.user_queue(user.id) - if not user_queue: - return - - player.remove_user_queue(user.id) - - embed = discord.Embed(color=ctx.me.color) - embed.description = "{dj_remove_user}" - embed = ctx.localizer.format_embed(embed, _id=user.id) + embed = discord.Embed(description="{dj_remove_user}", color=ctx.me.color) + embed = ctx.localizer.format_embed(embed, _id=member.id) await ctx.send(embed=embed) @commands.command(name='search') + @require_voice_connection(should_connect=True) async def _search(self, ctx, *, query): player = self.bot.lavalink.player_manager.get(ctx.guild.id) @@ -464,23 +416,17 @@ class Music(commands.Cog): result = ctx.localizer.format_embed(result) await message.edit(embed=result) - await message.clear_reactions() if not player.is_playing: await player.play() @commands.command(name='disconnect') @checks.dj_or(alone=True) + @require_voice_connection() async def _disconnect(self, ctx): """ Disconnects the player from the voice channel and clears its queue. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if not player.is_connected: - return await ctx.send(ctx.localizer.format_str("{not_connected}")) - - if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): - return await ctx.send(ctx.localizer.format_str("{disconnect.not_in_voice}")) - player.queue.clear() await player.stop() await self.connect_to(ctx.guild.id, None) @@ -490,24 +436,21 @@ class Music(commands.Cog): @commands.command(name='reconnect') @checks.dj_or(alone=True) + @require_voice_connection() async def _reconnect(self, ctx): """ Tries to disconnect then reconnect the player in case the bot gets stuck on a song """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if not player.is_connected: - return await ctx.send(ctx.localizer.format_str("{not_connected}")) - - if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): - return await ctx.send(ctx.localizer.format_str("{disconnect.not_in_voice}")) - current_channel = player.channel_id + await player.stop() await self.connect_to(ctx.guild.id, None) - await asyncio.sleep(1) # Pretend to do stuff? + await asyncio.sleep(1) # Pretend stuff is happening/give everything some time to reset. await self.connect_to(ctx.guild.id, current_channel) await player.play() @commands.command(name='volume') @checks.dj_or(alone=True, track_requester=True) + @require_playing(require_user_listening=True) async def _volume(self, ctx, volume: int = None): """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) @@ -517,11 +460,11 @@ class Music(commands.Cog): is_dj = checks.is_dj(ctx) is_admin = getattr(ctx.author.guild_permissions, 'administrator', None) is True - if int(player.current.requester) == ctx.author.id and not is_dj and not is_admin: + if player.current and (int(player.current.requester) == ctx.author.id and not is_dj and not is_admin): if not 50 <= volume <= 125: return await ctx.send(ctx.localizer.format_str("{volume.out_of_range}")) - await player.set_volume(volume) + embed = discord.Embed(color=ctx.me.color) embed.description = "{volume.set_to}" embed = ctx.localizer.format_embed(embed, _volume=player.volume) @@ -529,17 +472,16 @@ class Music(commands.Cog): @commands.command(name='normalize') @checks.dj_or(alone=True) + @require_voice_connection() async def _normalize(self, ctx): - """ Reset the equalizer and """ + """ Reset the equalizer and volume """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) await player.set_volume(100) await player.bassboost(False) - embed = discord.Embed(color=ctx.me.color) - embed.description = '{volume.reset}' + embed = discord.Embed(description='{volume.reset}' , color=ctx.me.color) embed = ctx.localizer.format_embed(embed) - await ctx.send(embed=embed) @commands.command(name='boost') @@ -548,15 +490,11 @@ class Music(commands.Cog): """ Set the equalizer to bass boost the music """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if boost is not None: + if boost: await player.bassboost(boost) embed = discord.Embed(color=ctx.me.color) - - if player.boosted: - embed.description = '{boost.on}' - else: - embed.description = '{boost.off}' + embed.description = '{boost.on}' if player.boosted else '{boost.off}' embed = ctx.localizer.format_embed(embed) await ctx.send(embed=embed) @@ -653,16 +591,13 @@ class Music(commands.Cog): @commands.command(name='scrub') @checks.dj_or(alone=True) + @require_voice_connection() + @require_playing(ensure_user_listening=True) async def _scrub(self, ctx): """ Lists the first 10 search results from a given query. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) controls = '{scrub.controls}' - embed = discord.Embed(description='{nothing_playing}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - - if player.current is None: - return await ctx.send(embed=embed) scrubber = [ ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, -1000), @@ -692,7 +627,7 @@ class Music(commands.Cog): return True return False - embed.description = '{scrub.add}' + embed = discord.Embed(description='{scrub.add}', color=ctx.me.color) embed = ctx.localizer.format_embed(embed) msg = await ctx.send(embed=embed) @@ -727,60 +662,6 @@ class Music(commands.Cog): arg = player.position + arg * 1000 await selection(arg) - async def ensure_voice(self, ctx, do_connect: Optional[bool] = None): - """ This check ensures that the bot and command author are in the same voicechannel. """ - - # If region doesn't return a known server location - try: - region = ctx.guild.region.value - except AttributeError: - region = ctx.guild.region - - player = self.bot.lavalink.player_manager.create(ctx.guild.id, endpoint=region) - # 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', '_sk') - - # Add commands that don't require you being in voice. - without_connect = ctx.command.callback.__name__ in ('_queue', '_history', '_now', '_lyrics') - - if without_connect: - return - - if not ctx.author.voice or not ctx.author.voice.channel: - raise commands.CommandInvokeError('Join a voicechannel first.') - - if not player.is_connected: - if not should_connect: - raise commands.CommandInvokeError('Not connected.') - - voice_channel = ctx.author.voice.channel - - permissions = voice_channel.permissions_for(ctx.me) - - if not permissions.connect or not permissions.speak: # Check user limit too? - raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.') - - voice_channels = self.bot.settings.get(ctx.guild, 'channels.music', []) - - if voice_channels: - if voice_channel.id not in voice_channels: - response = ctx.localizer.format_str('{settings_check.voicechannel}') - for channel_id in voice_channels: - channel = ctx.guild.get_channel(channel_id) - if channel is not None: - response += f'{channel.name}, ' - await ctx.send(response[:-2]) - raise commands.CommandInvokeError('You need to be in the right voice channel') - - player.store('channel', ctx.channel.id) - await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) - - else: - if int(player.channel_id) != ctx.author.voice.channel.id: - raise commands.CommandInvokeError('You need to be in my voicechannel.') - async def enqueue(self, ctx, track, embed): player = self.bot.lavalink.player_manager.get(ctx.guild.id) diff --git a/cogs/utils/checks.py b/cogs/utils/checks.py index 75e22db..96a1a16 100644 --- a/cogs/utils/checks.py +++ b/cogs/utils/checks.py @@ -62,6 +62,7 @@ def dj_or(alone: bool = False, track_requester: bool = False): async def predicate(ctx): try: player = ctx.bot.lavalink.player_manager.get(ctx.guild.id) + # Make this false if no player is_alone = (ctx.author in player.listeners and len(player.listeners) == 1) and alone requester = (player.current.requester == ctx.author.id) and track_requester @@ -70,6 +71,6 @@ def dj_or(alone: bool = False, track_requester: bool = False): is_alone = False dj = is_dj(ctx) - + print(dj, is_alone, requester) return dj or is_alone or requester return commands.check(predicate) diff --git a/cogs/utils/decorators.py b/cogs/utils/decorators.py new file mode 100644 index 0000000..b5c613a --- /dev/null +++ b/cogs/utils/decorators.py @@ -0,0 +1,124 @@ +import functools +import discord +from discord.ext import commands +import inspect + + +def ensure_voice_connection(should_connect=False): + """ + Checks if the bot is in a valid voice channel for the command + should_connect indicates whether the bot should try to join a channel + """ + def ensure_voice_proper(func): + @functools.wraps(func) + async def ensure_voice_inner(self, ctx, *command_args, **kwargs): + """ This check ensures that the bot and command author are in the same voicechannel. """ + + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + if not ctx.author.voice or not ctx.author.voice.channel: + raise commands.CommandInvokeError('Join a voicechannel first.') + + if not player.is_connected: + if not should_connect: + raise commands.CommandInvokeError('Not connected.') + + user_voice_channel = ctx.author.voice.channel + permissions = user_voice_channel.permissions_for(ctx.me) + + if not permissions.connect or not permissions.speak: # Check user limit too? + raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.') + + # Check against music channel restrictions in bot settings + if voice_channels := self.bot.settings.get(ctx.guild, 'channels.music', []): + if user_voice_channel.id not in voice_channels: + response = ctx.localizer.format_str('{settings_check.voicechannel}') + for channel_id in voice_channels: + channel = ctx.guild.get_channel(channel_id) + if channel is not None: + response += f'{channel.name}, ' + # TODO: Make this send the channels in question to the error, if not it is not that useful of + # an error message + await ctx.send(response[:-2]) + raise commands.CommandInvokeError('You need to be in the right voice channel') + + player.store('channel', ctx.channel.id) + await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) + + elif int(player.channel_id) != ctx.author.voice.channel.id: + raise commands.CommandInvokeError('You need to be in my voicechannel.') + + await func(self, ctx, *command_args, **kwargs) + + return ensure_voice_inner + return ensure_voice_proper + + +def require_playing(ensure_user_listening=False): + """ + Checks if the bot is currently playing a track + ensure_user_listening: also checks if the user is listening to the bot + """ + def ensure_play(func): + @functools.wraps(func) + async def ensure_play_inner(self, ctx, *command_args, **kwargs): + + if player := self.bot.lavalink.player_manager.get(ctx.guild.id): + + if not player.is_playing: + raise commands.CommandInvokeError('Not playing') + + if ensure_user_listening and (ctx.author not in player.listeners): + raise commands.CommandInvokeError('Not listening') + + else: + raise Exception('Player object does not yet exist') + + await func(self, ctx, *command_args, **kwargs) + + return ensure_play_inner + return ensure_play + + +def require_queue(require_member_queue=False, require_author_queue=False): + """ + Checks if there is something queued + require_member_queue also checks if the queue of a member is empty, only works when member is an argument + """ + def ensure_queue(func): + if require_member_queue: + inspection = inspect.getfullargspec(func) + assert 'member' in inspection.kwonlyargs, """require_member_error can only be used + on commands with a member keyword argument""" + # assert str(inspection.annotations['member']) == "", """ + # Member must be a discord member type""" + + @functools.wraps(func) + async def ensure_queue_inner(self, ctx, *command_args, **kwargs): + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + if require_member_queue: + try: + if member := kwargs['member']: # Ignore if member is None + user_queue = player.user_queue(member.id) + if not user_queue: + embed = discord.Embed(description='{queue.user_empty}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed, _user=member.display_name) + return await ctx.send(embed=embed) + except KeyError: + raise Exception("require_member_error can only be used on commands with a member keyword argument") + + if require_author_queue: + user_queue = player.user_queue(ctx.author.id) + if not user_queue: + embed = discord.Embed(description='{my_queue}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + return await ctx.send(embed=embed) + + if player.queue.empty: + embed = discord.Embed(description='{queue.empty}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + return await ctx.send(embed=embed) + + await func(self, ctx, *command_args, **kwargs) + return ensure_queue_inner + return ensure_queue diff --git a/cogs/utils/selector.py b/cogs/utils/selector.py index 3e93e60..db54d1c 100644 --- a/cogs/utils/selector.py +++ b/cogs/utils/selector.py @@ -191,6 +191,10 @@ class Selector(TextPaginator): self.args = None self.control_input = False if len(results) == 1: + self.stopped = True + await self.message.clear_reactions() return self.message, self.pages[self.current_page], results[0] else: + self.stopped = True + await self.message.clear_reactions() return self.message, self.pages[self.current_page], results