From 0b042532fddf4714f7401f91453c6f465c309cf0 Mon Sep 17 00:00:00 2001 From: Draper <27962761+Drapersniper@users.noreply.github.com> Date: Fri, 20 Dec 2019 06:58:08 +0000 Subject: [PATCH] [Audio] Fix Attribute error raised by `is_alone` method when channel was None (#3122) * Fix attribute Fixes #3120 Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> * Chore Signed-off-by: Drapersniper <27962761+drapersniper@users.noreply.github.com> --- changelog.d/3120.bugfix.rst | 1 + redbot/cogs/audio/audio.py | 105 +++++++++++++++--------------------- redbot/cogs/audio/utils.py | 17 +++++- 3 files changed, 58 insertions(+), 65 deletions(-) create mode 100644 changelog.d/3120.bugfix.rst diff --git a/changelog.d/3120.bugfix.rst b/changelog.d/3120.bugfix.rst new file mode 100644 index 000000000..4ff7c240a --- /dev/null +++ b/changelog.d/3120.bugfix.rst @@ -0,0 +1 @@ +Fixed an issue when calling audio commands when not in a voice channel could result in a crash. diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py index 209a571bb..6bb3229df 100644 --- a/redbot/cogs/audio/audio.py +++ b/redbot/cogs/audio/audio.py @@ -5,7 +5,7 @@ import datetime import heapq import json import logging -import os +import math import random import re import time @@ -17,7 +17,6 @@ from typing import List, Optional, Tuple, Union, cast import aiohttp import discord import lavalink -import math from fuzzywuzzy import process import redbot.core @@ -34,8 +33,9 @@ from redbot.core.utils.menus import ( start_adding_reactions, ) from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate + from . import audio_dataclasses -from .apis import MusicCache, HAS_SQL, _ERROR +from .apis import _ERROR, HAS_SQL, MusicCache from .checks import can_have_caching from .converters import ComplexScopeParser, ScopeParser, get_lazy_converter, get_playlist_converter from .equalizer import Equalizer @@ -57,8 +57,26 @@ from .playlists import ( get_playlist, humanize_scope, ) -from .utils import * - +from .utils import ( + CacheLevel, + Notifier, + clear_react, + draw_time, + dynamic_time, + get_description, + is_allowed, + match_url, + match_yt_playlist, + pass_config_to_dependencies, + queue_duration, + remove_react, + rgetattr, + time_convert, + track_creator, + track_limit, + url_check, + userlimit, +) _ = Translator("Audio", __file__) @@ -1669,9 +1687,7 @@ class Audio(commands.Cog): if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg(ctx, _("You need the DJ role to disconnect.")) - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): return await self._embed_msg(ctx, _("There are other people listening to music.")) else: await self._embed_msg(ctx, _("Disconnecting...")) @@ -2270,9 +2286,7 @@ class Audio(commands.Cog): dj_enabled = await self.config.guild(ctx.guild).dj_enabled() vote_enabled = await self.config.guild(ctx.guild).vote_enabled() if dj_enabled or vote_enabled: - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): return if player.current: @@ -2322,9 +2336,7 @@ class Audio(commands.Cog): ctx, _("You must be in the voice channel pause or resume.") ) if dj_enabled: - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): return await self._embed_msg( ctx, _("You need the DJ role to pause or resume tracks.") ) @@ -5202,9 +5214,7 @@ class Audio(commands.Cog): dj_enabled = await self.config.guild(ctx.guild).dj_enabled() player = lavalink.get_player(ctx.guild.id) if dj_enabled: - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): return await self._embed_msg(ctx, _("You need the DJ role to skip tracks.")) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel @@ -5522,10 +5532,9 @@ class Audio(commands.Cog): dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if not self._player_check(ctx) or not player.queue: return await self._embed_msg(ctx, _("There's nothing in the queue.")) + if dj_enabled: - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): return await self._embed_msg(ctx, _("You need the DJ role to clear the queue.")) player.queue.clear() await self._embed_msg(ctx, _("The queue has been cleared.")) @@ -5542,9 +5551,7 @@ class Audio(commands.Cog): if not self._player_check(ctx) or not player.queue: return await self._embed_msg(ctx, _("There's nothing in the queue.")) if dj_enabled: - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): return await self._embed_msg(ctx, _("You need the DJ role to clean the queue.")) clean_tracks = [] removed_tracks = 0 @@ -5624,10 +5631,8 @@ class Audio(commands.Cog): """Shuffles the queue.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if dj_enabled: - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): - return await self._embed_msg(ctx, _("You need the DJ role to clean the queue.")) + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): + return await self._embed_msg(ctx, _("You need the DJ role to shuffle the queue.")) if not self._player_check(ctx): return await self._embed_msg(ctx, _("There's nothing in the queue.")) try: @@ -6090,7 +6095,7 @@ class Audio(commands.Cog): Accepts seconds or a value formatted like 00:00:00 (`hh:mm:ss`) or 00:00 (`mm:ss`).""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() vote_enabled = await self.config.guild(ctx.guild).vote_enabled() - is_alone = await self._is_alone(ctx, ctx.author) + is_alone = await self._is_alone(ctx) is_requester = await self.is_requester(ctx, ctx.author) can_skip = await self._can_instaskip(ctx, ctx.author) @@ -6212,10 +6217,9 @@ class Audio(commands.Cog): return await self._embed_msg(ctx, _("Nothing playing.")) dj_enabled = await self.config.guild(ctx.guild).dj_enabled() vote_enabled = await self.config.guild(ctx.guild).vote_enabled() - is_alone = await self._is_alone(ctx, ctx.author) + is_alone = await self._is_alone(ctx) is_requester = await self.is_requester(ctx, ctx.author) can_skip = await self._can_instaskip(ctx, ctx.author) - if dj_enabled and not vote_enabled: if not (can_skip or is_requester) and not is_alone: return await self._embed_msg( @@ -6228,7 +6232,6 @@ class Audio(commands.Cog): and skip_to_track > 1 ): return await self._embed_msg(ctx, _("You can only skip the current track.")) - if vote_enabled: if not can_skip: if skip_to_track is not None: @@ -6296,40 +6299,21 @@ class Audio(commands.Cog): return False - async def _is_alone(self, ctx: commands.Context, member: discord.Member): - try: - user_voice = ctx.guild.get_member(member.id).voice - bot_voice = ctx.guild.get_member(self.bot.user.id).voice - nonbots = sum(not m.bot for m in user_voice.channel.members) - if user_voice.channel != bot_voice.channel: - nonbots = nonbots + 1 - except AttributeError: - if ctx.guild.get_member(self.bot.user.id).voice is not None: - nonbots = sum( - not m.bot for m in ctx.guild.get_member(self.bot.user.id).voice.channel.members - ) - if nonbots == 1: - nonbots = 2 - elif ctx.guild.get_member(member.id).voice.channel.members == 1: - nonbots = 1 - else: - nonbots = 0 - return nonbots <= 1 + async def _is_alone(self, ctx: commands.Context): + channel_members = rgetattr(ctx, "guild.me.voice.channel.members", []) + nonbots = sum(m.id != ctx.author.id for m in channel_members if not m.bot) + return nonbots < 1 async def _has_dj_role(self, ctx: commands.Context, member: discord.Member): dj_role_obj = ctx.guild.get_role(await self.config.guild(ctx.guild).dj_role()) - if dj_role_obj in ctx.guild.get_member(member.id).roles: - return True - return False + return dj_role_obj in ctx.guild.get_member(member.id).roles @staticmethod async def is_requester(ctx: commands.Context, member: discord.Member): try: player = lavalink.get_player(ctx.guild.id) log.debug(f"Current requester is {player.current}") - if player.current.requester.id == member.id: - return True - return False + return player.current.requester.id == member.id except Exception as e: log.error(e) return False @@ -6422,9 +6406,7 @@ class Audio(commands.Cog): ctx, _("You must be in the voice channel to stop the music.") ) if vote_enabled or vote_enabled and dj_enabled: - if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( - ctx, ctx.author - ): + if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx): return await self._embed_msg( ctx, _("There are other people listening - vote to skip instead.") ) @@ -6754,10 +6736,7 @@ class Audio(commands.Cog): log.error( "Exception raised in Audio's emptypause_timer.", exc_info=True ) - finally: - pause_times.pop(server.id, None) - else: - pause_times.pop(server.id, None) + pause_times.pop(server.id, None) servers = stop_times.copy() servers.update(pause_times) for sid in servers: diff --git a/redbot/cogs/audio/utils.py b/redbot/cogs/audio/utils.py index 682d36d50..b25ec5fe2 100644 --- a/redbot/cogs/audio/utils.py +++ b/redbot/cogs/audio/utils.py @@ -1,5 +1,6 @@ import asyncio import contextlib +import functools import os import re import time @@ -10,10 +11,9 @@ import lavalink from redbot.core import Config, commands from redbot.core.bot import Red + from . import audio_dataclasses - from .converters import _pass_config_to_converters - from .playlists import _pass_config_to_playlist __all__ = [ @@ -32,6 +32,7 @@ __all__ = [ "url_check", "userlimit", "is_allowed", + "rgetattr", "CacheLevel", "Notifier", ] @@ -240,6 +241,18 @@ def userlimit(channel): return True +def rsetattr(obj, attr, val): + pre, _, post = attr.rpartition(".") + return setattr(rgetattr(obj, pre) if pre else obj, post, val) + + +def rgetattr(obj, attr, *args): + def _getattr(obj2, attr2): + return getattr(obj2, attr2, *args) + + return functools.reduce(_getattr, [obj] + attr.split(".")) + + class CacheLevel: __slots__ = ("value",)