From 6a68fb9dc4fe82fe60f990b9d090b63889d85c32 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Sun, 9 Oct 2022 13:51:15 +0200 Subject: [PATCH] Update error handling for decorators. Add more type hints --- bot.py | 7 +- musicbot/cogs/cogmanager.py | 6 +- musicbot/cogs/errors.py | 101 ++++++++------- musicbot/cogs/helpformatter/help.py | 6 +- musicbot/cogs/misc.py | 6 +- musicbot/cogs/music/__init__.py | 122 +++++++++++------- musicbot/cogs/music/decorators.py | 63 +++------ musicbot/cogs/music/music_errors.py | 74 +++++++++-- musicbot/cogs/music/voice_client.py | 38 ++++++ musicbot/cogs/nodemanager.py | 21 ++- musicbot/cogs/settings.py | 6 +- .../utils/settingsmanager/settingsmanager.py | 3 +- musicbot/utils/thumbnailer.py | 6 +- musicbot/utils/userinteraction/scroller.py | 4 +- 14 files changed, 287 insertions(+), 176 deletions(-) create mode 100644 musicbot/cogs/music/voice_client.py diff --git a/bot.py b/bot.py index 4647ddf..01d1c4e 100644 --- a/bot.py +++ b/bot.py @@ -1,5 +1,6 @@ # Discord Packages import discord +import lavalink from discord.ext import commands from discord.flags import MemberCacheFlags @@ -8,6 +9,7 @@ import os import time import traceback from argparse import ArgumentParser, RawTextHelpFormatter +from typing import Optional import aiohttp import yaml @@ -35,7 +37,7 @@ def _get_prefix(bot, message): return commands.when_mentioned_or(*prefixes)(bot, message) -class Bot(commands.Bot): +class MusicBot(commands.Bot): def __init__(self, datadir, debug: bool = False): intents = discord.Intents.all() super().__init__(command_prefix=_get_prefix, @@ -55,6 +57,7 @@ class Bot(commands.Bot): self.main_logger = logger self.logger = self.main_logger.bot_logger.getChild("Bot") self.logger.debug("Debug: %s" % debug) + self.lavalink: Optional[lavalink.Client] = None async def on_message(self, message): if message.author.bot: @@ -110,7 +113,7 @@ class Bot(commands.Bot): def run_bot(datadir, debug: bool = False): - bot = Bot(datadir, debug=debug) + bot = MusicBot(datadir, debug=debug) bot.run() diff --git a/musicbot/cogs/cogmanager.py b/musicbot/cogs/cogmanager.py index cd3e0fc..1a7ece2 100644 --- a/musicbot/cogs/cogmanager.py +++ b/musicbot/cogs/cogmanager.py @@ -3,13 +3,15 @@ from discord.ext import commands import traceback +from bot import MusicBot + from ..utils.userinteraction import ClearOn, Scroller from .helpformatter import commandhelper class CogManager(commands.Cog): - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot self.settings = self.bot.settings @commands.group(name='cogmanager', hidden=True) diff --git a/musicbot/cogs/errors.py b/musicbot/cogs/errors.py index 453e56a..4098d04 100644 --- a/musicbot/cogs/errors.py +++ b/musicbot/cogs/errors.py @@ -4,14 +4,16 @@ from discord.ext import commands import sys +from bot import MusicBot + from ..utils.userinteraction import ClearOn, Scroller from .helpformatter import commandhelper -from .music.music_errors import WrongTextChannelError, WrongVoiceChannelError +from .music import music_errors class Errors(commands.Cog): - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot self.logger = self.bot.main_logger.bot_logger.getChild("Errors") async def base_msg(self, ctx, state: int = 0xFCBA03): @@ -42,60 +44,61 @@ class Errors(commands.Cog): embed = ctx.localizer.format_embed(embed) return await ctx.send(embed=embed) - # Music related errors - if isinstance(err, commands.CommandInvokeError): - if (err.original == 'Join a voicechannel first.'): - return await send_error_embed('{errors.join_voice_first}') - - elif (err.original == 'Not connected.'): - return await send_error_embed('{errors.not_connected}') - - elif (err.original == 'I need the `CONNECT` and `SPEAK` permissions.'): - return await send_error_embed('{errors.need_permission}') - - 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}') - - elif (err.original == 'The channel is currently full'): - return await send_error_embed('{errors.full_channel}') - - # Regular command invoke errors + # User input errors if isinstance(err, commands.UserInputError): if self.bot.debug: self.logger.debug("User input error") self.logger.debug(err) return - if isinstance(err, WrongVoiceChannelError): - if (err.original.startswith("You need to be in my voice channel")): - return await send_error_embed(err.channels[0].name, title='{errors.my_channel}') + # Music related errors + if isinstance(err, music_errors.MusicError): + if isinstance(err, music_errors.UserNotConnectedError): + return await send_error_embed('{errors.join_voice_first}') - elif (err.original.startswith('You need to be in the right voice channel')): - changed = False - response = ctx.localizer.format_str('{settings_check.voicechannel}') - for channel_id in err.channels: - channel = ctx.guild.get_channel(channel_id) - if channel is not None: - response += f'{channel.name}, ' - changed = True - if changed: - return await send_error_embed(response[:-2], title='{errors.right_channel}') - else: - return await send_error_embed('{errors.right_channel}') + elif isinstance(err, music_errors.BotNotConnectedError): + return await send_error_embed('{errors.not_connected}') - if isinstance(err, WrongTextChannelError): - try: - await ctx.message.delete() - except Exception as e: - self.logger.debug("Error deleting message: %s\nTraceback: %s" % (e, err)) - response = ctx.localizer.format_str('{settings_check.textchannel}') - for channel_id in err.channels: - response += f'<#{channel_id}>, ' - msg = await send_error_embed(response[:-2], title='{errors.right_channel}') - return await msg.delete(delay=5) + elif isinstance(err, music_errors.MissingPermissionsError): + return await send_error_embed('{errors.need_permission}') + + elif isinstance(err, music_errors.PlayBackStatusError): + if isinstance(err, music_errors.RequirePlayingError): + return await send_error_embed('{not_playing}') + if isinstance(err, music_errors.RequireListeningError): + return await send_error_embed('{have_to_listen}') + + elif isinstance(err, music_errors.ChannelError): + if isinstance(err, music_errors.VoiceChannelFullError): + return await send_error_embed('{errors.full_channel}') + + elif isinstance(err, music_errors.UserInDifferentVoiceChannelError): + return await send_error_embed(err.channels[0].name, title='{errors.my_channel}') + + elif isinstance(err, music_errors.ConfiguredChannelsError): + if isinstance(err, music_errors.WrongVoiceChannelError): + changed = False + response = ctx.localizer.format_str('{settings_check.voicechannel}') + for channel_id in err.channels: + channel = ctx.guild.get_channel(channel_id) + if channel is not None: + response += f'{channel.name}, ' + changed = True + if changed: + return await send_error_embed(response[:-2], title='{errors.right_channel}') + else: + return await send_error_embed('{errors.right_channel}') + + if isinstance(err, music_errors.WrongTextChannelError): + try: + await ctx.message.delete() + except Exception as e: + self.logger.debug("Error deleting message: %s\nTraceback: %s" % (e, err)) + response = ctx.localizer.format_str('{settings_check.textchannel}') + for channel_id in err.channels: + response += f'<#{channel_id}>, ' + msg = await send_error_embed(response[:-2], title='{errors.right_channel}') + return await msg.delete(delay=5) if isinstance(err, commands.CheckFailure): pass diff --git a/musicbot/cogs/helpformatter/help.py b/musicbot/cogs/helpformatter/help.py index 11f7c82..328cee7 100644 --- a/musicbot/cogs/helpformatter/help.py +++ b/musicbot/cogs/helpformatter/help.py @@ -1,14 +1,16 @@ # Discord Packages from discord.ext import commands +from bot import MusicBot + from ...utils.userinteraction.scroller import ClearOn, Scroller from .helpformatter import coghelper, commandhelper, helper, prefix_cleaner class Help(commands.Cog): """Help command""" - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot @commands.command(hidden=True) async def help(self, ctx): # Takes no args because reasons(using the view directly) diff --git a/musicbot/cogs/misc.py b/musicbot/cogs/misc.py index b2f867b..e942d02 100644 --- a/musicbot/cogs/misc.py +++ b/musicbot/cogs/misc.py @@ -6,12 +6,14 @@ from lavalink import __version__ as LavalinkVersion import platform import time +from bot import MusicBot + from ..utils import bot_version class Misc(commands.Cog): - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot def get_uptime(self): now = time.time() diff --git a/musicbot/cogs/music/__init__.py b/musicbot/cogs/music/__init__.py index 0a06ef4..08f709b 100644 --- a/musicbot/cogs/music/__init__.py +++ b/musicbot/cogs/music/__init__.py @@ -1,5 +1,7 @@ # Discord Packages import discord +import lavalink +from discord import VoiceChannel from discord.ext import commands, tasks from lavalink.events import ( NodeChangedEvent, NodeConnectedEvent, NodeDisconnectedEvent, PlayerUpdateEvent, QueueEndEvent, TrackEndEvent, @@ -14,6 +16,8 @@ from typing import List, Optional from bs4 import BeautifulSoup +from bot import MusicBot + # Bot Utilities from musicbot.utils.mixplayer.player import MixPlayer from ...utils import checks, timeformatter @@ -21,22 +25,26 @@ from ...utils.thumbnailer import Thumbnailer from ...utils.userinteraction.paginators import QueuePaginator, TextPaginator from ...utils.userinteraction.scroller import ClearOn, Scroller from ...utils.userinteraction.selector import SelectMode, Selector, SelectorButton, SelectorItem -from .decorators import BasicVoiceClient, require_playing, require_queue, require_voice_connection, voteable -from .music_errors import WrongTextChannelError +from .decorators import require_playing, require_queue, require_voice_connection, voteable +from .music_errors import MusicError, WrongTextChannelError +from .voice_client import BasicVoiceClient time_rx = re.compile('[0-9]+') url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') class Music(commands.Cog): - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot self.logger = self.bot.main_logger.bot_logger.getChild("Music") self.thumbnailer = Thumbnailer(bot=self.bot) self.leave_timer.start() - self.bot.lavalink.add_event_hook(self.track_hook) + if self.bot.lavalink is None: + raise MusicError("Lavalink is not yet initialized") + self.lavalink: lavalink.Client = self.bot.lavalink + self.lavalink.add_event_hook(self.track_hook) async def cog_check(self, ctx): if not ctx.guild: @@ -50,10 +58,17 @@ class Music(commands.Cog): """ Ensures a valid player exists whenever a command is run """ # Creates a new only if one doesn't exist, ensures a valid player for all checks. if ctx.guild: - self.bot.lavalink.player_manager.create(ctx.guild.id) + self.lavalink.player_manager.create(ctx.guild.id) + + def get_player(self, guild: discord.Guild) -> MixPlayer: + player: Optional[MixPlayer] = self.lavalink.player_manager.get(guild.id) + if player is None: + self.logger.error(f"Tried to get player for guild {guild.id} but got None") + raise MusicError(f"Tried to get player for guild {guild.id} but got None") + return player async def enqueue(self, ctx, track, embed, silent=False, check_max_length=True) -> tuple[discord.Embed, bool]: - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) # Only add tracks that don't exceed the max track length if maxlength := self.max_track_length(ctx.guild, player): @@ -96,7 +111,7 @@ class Music(commands.Cog): return embed, True def get_current_song_embed(self, ctx, include_time=False): - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) if not player.current: return @@ -138,7 +153,7 @@ class Music(commands.Cog): async def _play(self, ctx, *, query: str): """ Searches and plays a song from a given query. """ self.logger.debug("Query: %s" % query) - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) query = query.strip('<>') if not url_rx.match(query): @@ -176,7 +191,7 @@ class Music(commands.Cog): @require_playing(require_user_listening=True) async def _seek(self, ctx, *, time: str): """ Seeks to a given position in a track. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) if seconds := time_rx.search(time): # Convert to milliseconds, include sign milliseconds = int(seconds.group())*1000 * (-1 if time.startswith('-1') else 1) @@ -194,7 +209,7 @@ class Music(commands.Cog): @voteable(requester_override=True, react_to_vote=True) async def _skip(self, ctx): """ Skips the current track. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) await player.skip() embed = self.get_current_song_embed(ctx) @@ -205,7 +220,7 @@ class Music(commands.Cog): @require_playing(require_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: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) # TODO: Do all queue out of range messages the same way if pos < 1: @@ -227,7 +242,7 @@ class Music(commands.Cog): @voteable(DJ_override=True, react_to_vote=True) async def _stop(self, ctx): """ Stops the player and clears its queue. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) player.queue.clear() await player.stop() @@ -245,7 +260,7 @@ class Music(commands.Cog): @require_queue(require_member_queue=True) async def _queue(self, ctx, *, member: Optional[discord.Member] = None): """ Shows the player's queue. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) pagified_queue = QueuePaginator(ctx.localizer, player, color=ctx.me.color, member=member) scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling(ClearOn.ManualExit | ClearOn.Timeout) @@ -262,7 +277,7 @@ class Music(commands.Cog): @require_playing() async def _pause(self, ctx): """ Pauses/Resumes the current track. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) await player.set_pause(not player.paused) if player.paused: await ctx.send(ctx.localizer.format_str("{resume.paused}")) @@ -274,7 +289,7 @@ class Music(commands.Cog): @require_queue(require_author_queue=True) async def _shuffle(self, ctx): """ Shuffles your queue. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) player.shuffle_user_queue(ctx.author) await ctx.send(ctx.localizer.format_str("{shuffle}")) @@ -283,7 +298,7 @@ class Music(commands.Cog): @require_queue(require_author_queue=True) async def _move(self, ctx): """ Moves a song in your queue. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) # We create a new selector for each selection def move_selector(ctx, queue: List[AudioTrack], title: str, first: bool): @@ -344,7 +359,7 @@ class Music(commands.Cog): @require_voice_connection() @require_queue(require_author_queue=True) async def _remove(self, ctx): - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) user_queue: List[AudioTrack] = player.user_queue(ctx.author) @@ -406,7 +421,7 @@ class Music(commands.Cog): @require_queue() async def _djremove(self, ctx, pos: int, member: Optional[discord.Member] = None): """ Remove a song from either the global queue or a users queue""" - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) # TODO: add way to allow "member" as regular arg in require_queue() instead of only kwargs queue = player.user_queue(member) if member else player.queue @@ -419,11 +434,13 @@ class Music(commands.Cog): removed = player.remove_user_track(member, pos - 1) if removed: - requester = self.bot.get_user(removed.requester) - await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, - _user=requester.display_name)) + if requester := self.bot.get_user(removed.requester): + await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, + _user=requester.display_name)) + else: + self.logger.error(f"Did not get a valid user for track requester {removed.requester}") else: - self.logger.error("Did not remove a track when trying to remove index {pos}, despite bounds check") + self.logger.error(f"Did not remove a track when trying to remove index {pos}, despite bounds check") else: await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue))) @@ -433,7 +450,7 @@ class Music(commands.Cog): @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: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) player.remove_user_queue(member) embed = discord.Embed(description="{dj_remove_user}", color=ctx.me.color) @@ -444,7 +461,7 @@ class Music(commands.Cog): @commands.command(name='search') @require_voice_connection(should_connect=True) async def _search(self, ctx, *, query): - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) self.logger.debug("Query: %s" % query) if not query.startswith('ytsearch:') and not query.startswith('scsearch:'): @@ -487,7 +504,7 @@ class Music(commands.Cog): @voteable(DJ_override=True, react_to_vote=True) async def _disconnect(self, ctx): """ Disconnects the player from the voice channel and clears its queue. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) player.queue.clear() await player.stop() @@ -501,7 +518,7 @@ class Music(commands.Cog): @voteable(DJ_override=True, react_to_vote=True) async def _reconnect(self, ctx): """ Tries to disconnect then reconnect the player in case the bot gets stuck on a song """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) current_channel = player.channel_id async def recon(): @@ -526,7 +543,7 @@ class Music(commands.Cog): @require_playing(require_user_listening=True) async def _volume(self, ctx, volume: Optional[int] = None): """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) if not volume: return await ctx.send(f'🔈 | {player.volume}%') @@ -547,7 +564,7 @@ class Music(commands.Cog): @voteable(DJ_override=True, react_to_vote=True) async def _forceplay(self, ctx, *, query: str): """ Searches and plays a song from a given query. """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) query = query.strip('<>') if not url_rx.match(query): @@ -584,7 +601,7 @@ class Music(commands.Cog): async def _forcedisconnect(self, ctx): """ Attempts to force disconnect the bot without checking if it is connected initially. """ try: - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) player.queue.clear() await player.stop() except Exception as e: @@ -600,7 +617,7 @@ class Music(commands.Cog): @require_voice_connection() async def _normalize(self, ctx): """ Reset the equalizer and volume """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) await player.set_volume(100) await player.bassboost(False) @@ -613,7 +630,7 @@ class Music(commands.Cog): @checks.dj_or(alone=True) async def _boost(self, ctx, boost: bool = False): """ Set the equalizer to bass boost the music """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) if boost is not None: await player.bassboost(boost) @@ -628,7 +645,7 @@ class Music(commands.Cog): @checks.dj_or(alone=True) async def _nightcore(self, ctx, boost: bool = False): """ Set a filter mimicking nightcore the music """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) await player.nightcoreify(boost) @@ -640,7 +657,7 @@ class Music(commands.Cog): @commands.command(name='history') async def _history(self, ctx): """ Show the last 10 songs played """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) history = player.get_history() if not history: embed = discord.Embed(description='{history.empty}', color=ctx.me.color) @@ -667,7 +684,7 @@ class Music(commands.Cog): async def _lyrics(self, ctx, *query: str): # TODO: - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) genius_access_token = self.bot.APIkeys.get('genius', None) @@ -734,7 +751,7 @@ class Music(commands.Cog): @require_playing(require_user_listening=True) async def _scrub(self, ctx): """ Shows a set of controls which can be used to skip forward or backwards in the song """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) controls = '{scrub.controls}' def seek_callback(player: MixPlayer, seconds): @@ -766,7 +783,7 @@ class Music(commands.Cog): async def _loop(self, ctx): # This is done using subcommands to have separate vote counts for starting and stopping. if ctx.invoked_subcommand is None: - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) embed = discord.Embed(color=ctx.me.color, title='Loop status') if player.looping: @@ -780,7 +797,7 @@ class Music(commands.Cog): @voteable(DJ_override=True, react_to_vote=True) async def _loop_start(self, ctx): """ Set the equalizer to bass boost the music """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) player.enable_looping(True) embed = discord.Embed(color=ctx.me.color) embed.description = 'Looping turned on' @@ -792,7 +809,7 @@ class Music(commands.Cog): @voteable(DJ_override=True, react_to_vote=True) async def _loop_stop(self, ctx): """ Set the equalizer to bass boost the music """ - player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + player = self.get_player(ctx.guild) player.enable_looping(False) embed = discord.Embed(color=ctx.me.color) embed.description = 'Looping turned off' @@ -800,19 +817,21 @@ class Music(commands.Cog): await ctx.send(embed=embed) async def cog_unload(self): - self.bot.lavalink._event_hooks.clear() + self.lavalink._event_hooks.clear() async def track_hook(self, event): if isinstance(event, TrackEndEvent): - channel = self.bot.get_channel(event.player.fetch('channel')) - player: MixPlayer = self.bot.lavalink.player_manager.get(channel.guild.id) - player.skip_voters.clear() + if channel := self.bot.get_channel(event.player.fetch('channel')): + if isinstance(channel, VoiceChannel): + player = self.get_player(channel.guild) + player.skip_voters.clear() if isinstance(event, TrackStartEvent): pass if isinstance(event, QueueEndEvent): - channel = self.bot.get_channel(event.player.fetch('channel')) - await self.check_leave_voice(channel.guild) + if channel := self.bot.get_channel(event.player.fetch('channel')): + if isinstance(channel, VoiceChannel): + await self.check_leave_voice(channel.guild) if isinstance(event, PlayerUpdateEvent): pass if isinstance(event, NodeDisconnectedEvent): @@ -827,16 +846,18 @@ class Music(commands.Cog): @commands.Cog.listener() async def on_voice_state_update(self, member: discord.Member, _: discord.VoiceState, after: discord.VoiceState): """ Updates listeners when the bot or a user changes voice state """ + if self.bot.user is None: + return # Bot not logged in if member.id == self.bot.user.id and after.channel is not None: voice_channel = after.channel - player: MixPlayer = self.bot.lavalink.player_manager.get(member.guild.id) + player = self.get_player(member.guild) player.clear_listeners() for member in voice_channel.members: if not member.bot: player.update_listeners(member, member.voice) if not member.bot: - player: MixPlayer = self.bot.lavalink.player_manager.get(member.guild.id) + player = self.get_player(member.guild) if player is not None: player.update_listeners(member, after) await self.check_leave_voice(member.guild) @@ -844,7 +865,7 @@ class Music(commands.Cog): async def check_leave_voice(self, guild: discord.Guild): """ Checks if the bot should leave the voice channel """ # TODO, disconnect timer? - player: MixPlayer = self.bot.lavalink.player_manager.get(guild.id) + player = self.get_player(guild) if len(player.listeners) == 0 and player.is_connected: if player.queue.empty and player.current is None: await player.stop() @@ -852,8 +873,9 @@ class Music(commands.Cog): await voice_client.disconnect(force=False) async def leave_check(self): - for player_id in self.bot.lavalink.player_manager.players: - await self.check_leave_voice(self.bot.get_guild(player_id)) + for player_id in self.lavalink.player_manager.players: + if guild := self.bot.get_guild(player_id): + await self.check_leave_voice(guild) @tasks.loop(seconds=10.0) async def leave_timer(self): diff --git a/musicbot/cogs/music/decorators.py b/musicbot/cogs/music/decorators.py index 4612d3e..e63fc7f 100644 --- a/musicbot/cogs/music/decorators.py +++ b/musicbot/cogs/music/decorators.py @@ -1,46 +1,16 @@ # Discord Packages import discord -from discord.ext import commands import asyncio import functools import inspect import math +# Bot Utilities +from musicbot.utils.mixplayer.player import MixPlayer from ...utils.checks import is_dj -from .music_errors import WrongVoiceChannelError - - -class BasicVoiceClient(discord.VoiceClient): - def __init__(self, client: discord.Client, channel: discord.abc.Connectable): - self.client = client - self.channel = channel - if hasattr(self.client, 'lavalink'): - self.lavalink = self.client.lavalink - else: - raise Exception("client did not have defined lavalink before connect") - - async def on_voice_server_update(self, data): - await self.lavalink.voice_update_handler({'t': 'VOICE_SERVER_UPDATE', 'd': data}) - - async def on_voice_state_update(self, data): - await self.lavalink.voice_update_handler({'t': 'VOICE_STATE_UPDATE', 'd': data}) - - async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, - self_mute: bool = False) -> None: - await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf) - - async def disconnect(self, *, force: bool = False) -> None: - player = self.lavalink.player_manager.get(self.channel.guild.id) - - # no need to disconnect if we are not connected - if not force and not player.is_connected: - return - - # None means disconnect - await self.channel.guild.change_voice_state(channel=None) - player.channel_id = None - self.cleanup() +from . import music_errors +from .voice_client import BasicVoiceClient def require_voice_connection(should_connect=False): @@ -53,36 +23,39 @@ def require_voice_connection(should_connect=False): 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) + player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + if not player: + raise music_errors.MusicError("ensure voice could not get lavalink player") + if not ctx.author.voice or not ctx.author.voice.channel: - raise commands.CommandInvokeError('Join a voicechannel first.') + raise music_errors.UserNotConnectedError('Join a voicechannel first.') if not player.is_connected: if not should_connect: - raise commands.CommandInvokeError('Not connected.') + raise music_errors.BotNotConnectedError('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.') + raise music_errors.MissingPermissionsError('I need the `CONNECT` and `SPEAK` permissions.') if (len(user_voice_channel.members) == user_voice_channel.user_limit) and not permissions.administrator: - raise commands.CommandInvokeError('The channel is currently full') + raise music_errors.VoiceChannelFullError('The channel is currently full', user_voice_channel) # 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: - raise WrongVoiceChannelError( + raise music_errors.WrongVoiceChannelError( 'You need to be in the right voice channel', channels=voice_channels) player.store('channel', ctx.channel.id) await ctx.author.voice.channel.connect(cls=BasicVoiceClient) - elif int(player.channel_id) != ctx.author.voice.channel.id: + elif player.channel_id and int(player.channel_id) != ctx.author.voice.channel.id: bot_channel = self.bot.get_channel(int(player.channel_id)) - raise WrongVoiceChannelError( - 'You need to be in my voice channel', channels=[bot_channel]) + raise music_errors.UserInDifferentVoiceChannelError('You need to be in my voice channel', + channel=bot_channel) await func(self, ctx, *command_args, **kwargs) @@ -102,10 +75,10 @@ def require_playing(require_user_listening=False): if player := self.bot.lavalink.player_manager.get(ctx.guild.id): if not player.is_playing: - raise commands.CommandInvokeError('Not playing') + raise music_errors.RequirePlayingError('Not playing') if require_user_listening and (ctx.author not in player.listeners): - raise commands.CommandInvokeError('Not listening') + raise music_errors.RequireListeningError('Not listening') else: raise Exception('Player object does not yet exist') diff --git a/musicbot/cogs/music/music_errors.py b/musicbot/cogs/music/music_errors.py index 24a2f41..c0430e5 100644 --- a/musicbot/cogs/music/music_errors.py +++ b/musicbot/cogs/music/music_errors.py @@ -1,20 +1,72 @@ -# define Python user-defined exceptions # Discord Packages from discord.ext.commands import CommandError +from typing import Optional -class WrongChannelError(CommandError): - def __init__(self, e: str, channels=None): - self.original = e + +class MusicError(CommandError): + def __init__(self, message: str, inner_error: Optional[Exception] = None): + self.inner_error = inner_error + super().__init__(message) + + +class MissingPermissionsError(MusicError): + def __init__(self, message: str, inner_error: Optional[Exception] = None): + super().__init__(message, inner_error=inner_error) + + +class BotNotConnectedError(MusicError): + def __init__(self, message: str, inner_error: Optional[Exception] = None): + super().__init__(message, inner_error=inner_error) + + +class UserNotConnectedError(MusicError): + def __init__(self, message: str, inner_error: Optional[Exception] = None): + super().__init__(message, inner_error=inner_error) + + +class PlayBackStatusError(MusicError): + def __init__(self, message: str, inner_error: Optional[Exception] = None): + super().__init__(message, inner_error=inner_error) + + +class RequirePlayingError(PlayBackStatusError): + def __init__(self, message: str, inner_error: Optional[Exception] = None): + super().__init__(message, inner_error=inner_error) + + +class RequireListeningError(PlayBackStatusError): + def __init__(self, message: str, inner_error: Optional[Exception] = None): + super().__init__(message, inner_error=inner_error) + + +# Channel errors +class ChannelError(MusicError): + def __init__(self, message: str, channels, inner_error: Optional[Exception] = None): self.channels = channels - super().__init__() + super().__init__(message=message, inner_error=inner_error) -class WrongVoiceChannelError(WrongChannelError): - def __init__(self, e: str, channels=None): - super().__init__(e, channels=channels) +class VoiceChannelFullError(ChannelError): + def __init__(self, message: str, channel, inner_error: Optional[Exception] = None): + super().__init__(message, channels=[channel], inner_error=inner_error) -class WrongTextChannelError(WrongChannelError): - def __init__(self, e: str, channels=None): - super().__init__(e, channels=channels) +class UserInDifferentVoiceChannelError(ChannelError): + def __init__(self, message: str, channel, inner_error: Optional[Exception] = None): + super().__init__(message, channels=[channel], inner_error=inner_error) + + +class ConfiguredChannelsError(ChannelError): + def __init__(self, message: str, channels, inner_error: Optional[Exception] = None): + super().__init__(message, channels=channels, inner_error=inner_error) + + +class WrongVoiceChannelError(ConfiguredChannelsError): + def __init__(self, message: str, channels, inner_error: Optional[Exception] = None): + super().__init__(message, channels=channels, inner_error=inner_error) + + +class WrongTextChannelError(ConfiguredChannelsError): + def __init__(self, message: str, channels, inner_error: Optional[Exception] = None): + super().__init__(message, channels=channels, inner_error=inner_error) diff --git a/musicbot/cogs/music/voice_client.py b/musicbot/cogs/music/voice_client.py new file mode 100644 index 0000000..cfdaf73 --- /dev/null +++ b/musicbot/cogs/music/voice_client.py @@ -0,0 +1,38 @@ +# Discord Packages +import discord + +from bot import MusicBot +from musicbot.cogs.music.music_errors import MusicError + + +class BasicVoiceClient(discord.VoiceClient): + def __init__(self, client: MusicBot, channel: discord.VoiceChannel): + self.music_bot = client + self.channel = channel + if self.music_bot.lavalink: + self.lavalink = self.music_bot.lavalink + else: + raise MusicError("client did not have defined lavalink before connect") + + async def on_voice_server_update(self, data): + await self.lavalink.voice_update_handler({'t': 'VOICE_SERVER_UPDATE', 'd': data}) + + async def on_voice_state_update(self, data): + await self.lavalink.voice_update_handler({'t': 'VOICE_STATE_UPDATE', 'd': data}) + + async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, + self_mute: bool = False) -> None: + await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf) + + async def disconnect(self, *, force: bool = False) -> None: + player = self.lavalink.player_manager.get(self.channel.guild.id) + + if player: + # no need to disconnect if we are not connected + if not force and not player.is_connected: + return + + # None means disconnect + await self.channel.guild.change_voice_state(channel=None) + player.channel_id = None + self.cleanup() diff --git a/musicbot/cogs/nodemanager.py b/musicbot/cogs/nodemanager.py index 45bb94c..94ca743 100644 --- a/musicbot/cogs/nodemanager.py +++ b/musicbot/cogs/nodemanager.py @@ -1,12 +1,15 @@ # Discord Packages import discord +import lavalink from discord.ext import commands -from lavalink import Client, Node import codecs import yaml +from bot import MusicBot +from musicbot.cogs.music.music_errors import MusicError + # Bot Utilities from musicbot.utils.settingsmanager import Settings from ..utils.mixplayer import MixPlayer @@ -15,8 +18,8 @@ from .helpformatter import commandhelper class NodeManager(commands.Cog): - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot self.settings: Settings = self.bot.settings self.logger = self.bot.main_logger.bot_logger.getChild("NodeManager") @@ -25,8 +28,8 @@ class NodeManager(commands.Cog): 'musicbot.cogs.music', ] - if not hasattr(self.bot, 'lavalink'): - self.bot.lavalink = Client(self.bot.user.id, player=MixPlayer) + if self.bot.lavalink is None and self.bot.user: + self.bot.lavalink = lavalink.Client(self.bot.user.id, player=MixPlayer) self.load_nodes_from_file() @@ -38,6 +41,9 @@ class NodeManager(commands.Cog): await self.bot.load_extension(extension) except Exception: self.logger.exception("Loading of extension %s failed" % extension) + else: + self.logger.error("Failed to initialize lavalink client") + raise MusicError("Failed to initialize lavalink client") def load_nodes_from_file(self): with codecs.open(f"{self.bot.datadir}/config.yaml", 'r', encoding='utf8') as f: @@ -46,6 +52,9 @@ class NodeManager(commands.Cog): name_cache = [] new_nodes = [] + if self.bot.lavalink is None: + return new_nodes + for node in self.bot.lavalink.node_manager.nodes: name_cache.append(node.name) @@ -93,7 +102,7 @@ class NodeManager(commands.Cog): embed.add_field(name=f'{await self._regioner(n.region)} **Name:** {n.name}', value=f'**Host:** {n.host}\n **Port:** {n.port}') - if isinstance(node, Node): + if isinstance(node, lavalink.Node): embed.add_field(name=f'{await self._regioner(node.region)} **Name:** {node.name}', value=f'**Host:** {node.host}\n **Port:** {node.port}') diff --git a/musicbot/cogs/settings.py b/musicbot/cogs/settings.py index 44142af..36ec231 100644 --- a/musicbot/cogs/settings.py +++ b/musicbot/cogs/settings.py @@ -2,14 +2,16 @@ import discord from discord.ext import commands +from bot import MusicBot + from ..utils import checks from ..utils.userinteraction import ClearOn, Scroller from .helpformatter import commandhelper class Settings(commands.Cog): - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot self.settings = self.bot.settings def format_prefixes(self, prefixes): diff --git a/musicbot/utils/settingsmanager/settingsmanager.py b/musicbot/utils/settingsmanager/settingsmanager.py index 5a15165..48a74be 100644 --- a/musicbot/utils/settingsmanager/settingsmanager.py +++ b/musicbot/utils/settingsmanager/settingsmanager.py @@ -4,6 +4,7 @@ import discord import codecs import locale as localee import os +from typing import Any import yaml @@ -53,7 +54,7 @@ class Settings(): with codecs.open(self._SETTINGS_PATH, 'w', encoding='utf8') as f: yaml.dump(self.settings, f, indent=2) - def get(self, identifier, setting, default=''): + def get(self, identifier, setting, default: Any = '') -> Any: """ Gets a value from the settings if a default return value is specified it will return the default if no setting is found. If that default is an attribute of the settings class, the value of the attribute will get returned.""" diff --git a/musicbot/utils/thumbnailer.py b/musicbot/utils/thumbnailer.py index 68d6356..0df7b0f 100644 --- a/musicbot/utils/thumbnailer.py +++ b/musicbot/utils/thumbnailer.py @@ -2,12 +2,12 @@ from time import perf_counter from bs4 import BeautifulSoup as bs4 -from bot import Bot +from bot import MusicBot class Thumbnailer(object): - def __init__(self, bot: Bot): - self.bot = bot + def __init__(self, bot: MusicBot): + self.bot: MusicBot = bot self.logger = self.bot.main_logger.bot_logger.getChild("Thumbnailer") async def get_html(self, url: str): diff --git a/musicbot/utils/userinteraction/scroller.py b/musicbot/utils/userinteraction/scroller.py index dc94660..3d1eec7 100644 --- a/musicbot/utils/userinteraction/scroller.py +++ b/musicbot/utils/userinteraction/scroller.py @@ -7,6 +7,8 @@ import asyncio from enum import Flag, auto from typing import List, Optional +from bot import MusicBot + from .paginators import BasePaginator, CantScrollException @@ -42,7 +44,7 @@ class Scroller: if not self.paginator.pages: raise Exception("Paginator contained no pages to display") # TODO: proper error - self.bot = ctx.bot + self.bot: MusicBot = ctx.bot self.channel = ctx.channel self.message: Optional[discord.Message] = None