From d81d5e5ef72fd03af9bee701329aa18df34b9e97 Mon Sep 17 00:00:00 2001 From: brandons209 Date: Fri, 26 Jun 2020 00:21:16 -0400 Subject: [PATCH] inital release of sfx cog --- rolemanagement/core.py | 8 +- sfx/__init__.py | 5 + sfx/info.json | 15 +++ sfx/injection.py | 210 +++++++++++++++++++++++++++++ sfx/sfx.py | 290 +++++++++++++++++++++++++++++++++++++++++ sfx/utils.py | 13 ++ 6 files changed, 537 insertions(+), 4 deletions(-) create mode 100644 sfx/__init__.py create mode 100644 sfx/info.json create mode 100644 sfx/injection.py create mode 100644 sfx/sfx.py create mode 100644 sfx/utils.py diff --git a/rolemanagement/core.py b/rolemanagement/core.py index ab2f42c..322604c 100644 --- a/rolemanagement/core.py +++ b/rolemanagement/core.py @@ -391,8 +391,7 @@ class RoleManagement( Roles with spaces in the name should be put in quotes """ current = await self.config.role(add_role).add_with() - current = [discord.utils.get(ctx.guild.roles, id=r) - for r in current] + current = [discord.utils.get(ctx.guild.roles, id=r) for r in current] await self.config.role(add_role).add_with.set([r.id for r in roles]) @@ -401,8 +400,9 @@ class RoleManagement( elif not roles and not current: await ctx.send("No roles originally defined.") else: - await ctx.send(f"Add with roles set to `{humanize_list([r.name for r in roles])}` from `{humanize_list(current) if current else None}`") - + await ctx.send( + f"Add with roles set to `{humanize_list([r.name for r in roles])}` from `{humanize_list(current) if current else None}`" + ) @rgroup.command(name="viewreactions") async def rg_view_reactions(self, ctx: GuildContext): diff --git a/sfx/__init__.py b/sfx/__init__.py new file mode 100644 index 0000000..b581e42 --- /dev/null +++ b/sfx/__init__.py @@ -0,0 +1,5 @@ +from .sfx import SFX + + +def setup(bot): + bot.add_cog(SFX(bot)) diff --git a/sfx/info.json b/sfx/info.json new file mode 100644 index 0000000..926ae69 --- /dev/null +++ b/sfx/info.json @@ -0,0 +1,15 @@ +{ + "author": [ + "Brandons209" + ], + "install_msg": "Thanks for install.", + "short": "Play sound effects!", + "description": "Play sound effects, either via links or files. Integrates with economy and cost manager cog.", + "min_bot_version": "3.5.0", + "requirements": ["tabulate"], + "tags": [ + "sfx", + "saysound", + "audio" + ] +} diff --git a/sfx/injection.py b/sfx/injection.py new file mode 100644 index 0000000..504d460 --- /dev/null +++ b/sfx/injection.py @@ -0,0 +1,210 @@ + #### INJECTED BY SFX COG #### + async def sfx_play(self, ctx: commands.Context, query: str, volume = 100): + ### From play command, process query and check if can play in this context ### + query = Query.process_input(query, self.local_folder_current_path) + import datetime + + if not self._player_check(ctx): + if self.lavalink_connection_aborted: + msg = _("Connection to Lavalink has failed") + desc = EmptyEmbed + if await self.bot.is_owner(ctx.author): + desc = _("Please check your console or logs for details.") + return await self.send_embed_msg(ctx, title=msg, description=desc) + try: + if ( + not ctx.author.voice.channel.permissions_for(ctx.me).connect + or not ctx.author.voice.channel.permissions_for(ctx.me).move_members + and self.is_vc_full(ctx.author.voice.channel) + ): + return await self.send_embed_msg( + ctx, + title=_("Unable To Play Tracks"), + description=_("I don't have permission to connect to your channel."), + ) + await lavalink.connect(ctx.author.voice.channel) + player = lavalink.get_player(ctx.guild.id) + player.store("connect", datetime.datetime.utcnow()) + except AttributeError: + return await self.send_embed_msg( + ctx, + title=_("Unable To Play Tracks"), + description=_("Connect to a voice channel first."), + ) + except IndexError: + return await self.send_embed_msg( + ctx, + title=_("Unable To Play Tracks"), + description=_("Connection to Lavalink has not yet been established."), + ) + + ### PLAYER SETTINGS ### + player = lavalink.get_player(ctx.guild.id) + + player.store("channel", ctx.channel.id) + player.store("guild", ctx.guild.id) + await self._eq_check(ctx, player) + + player.repeat = False + player.shuffle = False + player.shuffle_bumped = True + if player.volume != volume: + await player.set_volume(volume) + + if len(player.queue) >= 10000: + return await self.send_embed_msg( + ctx, title=_("Unable To Play Tracks"), description=_("Queue size limit reached.") + ) + + try: + await self.sfx_enqueue_tracks(ctx, query) + except QueryUnauthorized as err: + return await self.send_embed_msg( + ctx, title=_("Unable To Play Tracks"), description=err.message + ) + + async def sfx_enqueue_tracks( + self, ctx: commands.Context, query: Query, enqueue: bool = True + ) -> Union[discord.Message, List[lavalink.Track], lavalink.Track]: + player = lavalink.get_player(ctx.guild.id) + try: + if self.play_lock[ctx.message.guild.id]: + return await self.send_embed_msg( + ctx, + title=_("Unable To Get Tracks"), + description=_("Wait until the playlist has finished loading."), + ) + except KeyError: + self.update_player_lock(ctx, True) + guild_data = await self.config.guild(ctx.guild).all() + first_track_only = False + single_track = None + index = None + playlist_data = None + playlist_url = None + seek = 0 + + if not await self.is_query_allowed( + self.config, ctx.guild, f"{query}", query_obj=query + ): + raise QueryUnauthorized( + _("{query} is not an allowed query.").format(query=query.to_string_user()) + ) + if query.single_track: + first_track_only = True + index = query.track_index + if query.start_time: + seek = query.start_time + try: + result, called_api = await self.api_interface.fetch_track(ctx, player, query) + except TrackEnqueueError: + self.update_player_lock(ctx, False) + return await self.send_embed_msg( + ctx, + title=_("Unable to Get Track"), + description=_( + "I'm unable get a track from Lavalink at the moment, " + "try again in a few minutes." + ), + ) + tracks = result.tracks + playlist_data = result.playlist_info + if not enqueue: + return tracks + if not tracks: + self.update_player_lock(ctx, False) + title = _("Nothing found.") + embed = discord.Embed(title=title) + if result.exception_message: + if "Status Code" in result.exception_message: + embed.set_footer(text=result.exception_message[:2000]) + else: + embed.set_footer(text=result.exception_message[:2000].replace("\n", "")) + if await self.config.use_external_lavalink() and query.is_local: + embed.description = _( + "Local tracks will not work " + "if the `Lavalink.jar` cannot see the track.\n" + "This may be due to permissions or because Lavalink.jar is being run " + "in a different machine than the local tracks." + ) + elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT: + title = _("Track is not playable.") + embed = discord.Embed(title=title) + embed.description = _( + "**{suffix}** is not a fully supported format and some " + "tracks may not play." + ).format(suffix=query.suffix) + return await self.send_embed_msg(ctx, embed=embed) + + queue_dur = await self.queue_duration(ctx) + queue_total_duration = self.format_time(queue_dur) + before_queue_length = len(player.queue) + + + single_track = None + # a ytsearch: prefixed item where we only need the first Track returned + # this is in the case of [p]play , a single Spotify url/code + # or this is a localtrack item + try: + if len(player.queue) >= 10000: + return await self.send_embed_msg(ctx, title=_("Queue size limit reached.")) + + single_track = ( + tracks + if isinstance(tracks, lavalink.rest_api.Track) + else tracks[index] + if index + else tracks[0] + ) + if seek and seek > 0: + single_track.start_timestamp = seek * 1000 + if not await self.is_query_allowed( + self.config, + ctx.guild, + ( + f"{single_track.title} {single_track.author} {single_track.uri} " + f"{str(Query.process_input(single_track, self.local_folder_current_path))}" + ), + ): + if IS_DEBUG: + log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})") + self.update_player_lock(ctx, False) + return await self.send_embed_msg( + ctx, title=_("This track is not allowed in this server.") + ) + elif guild_data["maxlength"] > 0: + if self.is_track_too_long(single_track, guild_data["maxlength"]): + player.add(ctx.author, single_track) + player.maybe_shuffle() + self.bot.dispatch( + "red_audio_track_enqueue", + player.channel.guild, + single_track, + ctx.author, + ) + else: + self.update_player_lock(ctx, False) + return await self.send_embed_msg( + ctx, title=_("Track exceeds maximum length.") + ) + + else: + player.add(ctx.author, single_track) + player.maybe_shuffle() + self.bot.dispatch( + "red_audio_track_enqueue", player.channel.guild, single_track, ctx.author + ) + except IndexError: + self.update_player_lock(ctx, False) + title = _("Nothing found") + desc = EmptyEmbed + if await self.bot.is_owner(ctx.author): + desc = _("Please check your console or logs for details.") + return await self.send_embed_msg(ctx, title=title, description=desc) + + if not player.current: + await player.play() + self.update_player_lock(ctx, False) + return single_track or message + + ### END INJECTION BY SFX ### diff --git a/sfx/sfx.py b/sfx/sfx.py new file mode 100644 index 0000000..8044042 --- /dev/null +++ b/sfx/sfx.py @@ -0,0 +1,290 @@ +from redbot.core.utils.chat_formatting import * +from redbot.core import Config, checks, commands, bank +from redbot.core.data_manager import cog_data_path +import discord + +import os +import glob +import asyncio +from difflib import get_close_matches +import tabulate + +from .utils import saysound, code_path + +EXT = ("mp3", "flac", "ogg", "wav") + + +class SFX(commands.Cog): + """ Play saysounds in VC's in your guild + Supports costs, files, and links. + """ + + def __init__(self, bot): + self.bot = bot + self.config = Config.get_conf(self, identifier=8495126065166516132, force_registration=True) + + # saysounds maps saysound name (str) -> saysound data (dict) + default_guild = {"saysounds": {}, "FREE_ROLES": []} + default_global = {"attachments": True} + + self.config.register_guild(**default_guild) + self.config.register_global(**default_global) + + global PATH + global AUDIO_CODE_PATH + PATH = str(cog_data_path(cog_instance=self)) + audio_cog = bot.get_cog("Audio") + if audio_cog: + AUDIO_CODE_PATH = str(code_path(cog_instance=audio_cog)) + else: + AUDIO_CODE_PATH = None + + # integrates cost manager free roles! + # if cost_manager isn't loaded, use this cog's free roles + async def get_cost(self, member: discord.Member, sound: dict): + cost = sound["cost"] + cost_manager = self.bot.get_cog("CostManager") + + if cost_manager: + free_roles = await cost_manager.config.guild(member.guild).FREE_ROLES() + else: + free_roles = await self.config.guild(member.guild).FREE_ROLES() + + member_roles = {r.id for r in member.roles if r.name != "@everyone"} + found_roles = set(free_roles) & member_roles + if found_roles: + cost = 0 + + return cost + + # plays sound in vc of author + async def play(self, ctx, sound: dict): + audio_cog = self.bot.get_cog("Audio") + if not audio_cog: + await ctx.send(error("Unable to load audio cog, please contact bot owner!")) + return + + try: + query = sound["url"] if sound["url"] else sound["filepath"] + await audio_cog.sfx_play(ctx, query, volume=sound["volume"]) + except Exception as e: + await ctx.send(error("Unable to play sound, please contact bot owner!")) + print(e) # TODO add logging properly + + @commands.group() + @checks.admin_or_permissions(administrator=True) + async def sfxset(self, ctx): + """ + Manage settings for saysounds + """ + pass + + @sfxset.command(name="add") + @commands.guild_only() + async def sfxset_add(self, ctx, cost: int, vol: int, *, name: str): + """ + Add an sfx sound for the guild. + + Attach the audio file to the message. + """ + can_use = await self.config.attachments() + + if not can_use: + await ctx.send(error("Sorry, I only allow adding say sounds using URLs.")) + return + + if len(ctx.message.attachments) < 1: + await ctx.send(error("Please provide an attachment.")) + return + + file = ctx.message.attachments[0] + ext = file.filename.split(".")[-1] + + if ext not in EXT: + await ctx.send(error("Audio file must one of `mp3, wav, flac, or ogg` formats.")) + return + + save_path = os.path.join(PATH, str(ctx.guild.id)) + if not os.path.exists(save_path): + os.makedirs(save_path) + + save_path = os.path.join(save_path, file.filename) + + try: + await file.save(save_path) + except: + await ctx.send(error("Error saving file, please try again.")) + return + + async with self.config.guild(ctx.guild).saysounds() as saysounds: + author = f"{ctx.author} id: {ctx.author.id}" + new_sound = saysound(name, author, cost=cost, volume=vol, filepath=save_path) + saysounds[name] = new_sound + + await ctx.tick() + + @sfxset.command(name="addurl") + @commands.guild_only() + async def sfxset_addurl(self, ctx, cost: int, vol: int, url: str, *, name: str): + """ + Add an sfx sound for the guild. + + URL must be a direct link to the audio file, a youtube link, spotify link, + soundcloud link etc. + You can test if it will work using the `play` command on the bot. + """ + async with self.config.guild(ctx.guild).saysounds() as saysounds: + author = f"{ctx.author} id: {ctx.author.id}" + new_sound = saysound(name, author, cost=cost, volume=vol, url=url) + saysounds[name] = new_sound + + await ctx.tick() + + @sfxset.command(name="del") + @commands.guild_only() + async def sfxset_del(self, ctx, *, name: str): + """ + Delete an sfx sound for the guild. + """ + async with self.config.guild(ctx.guild).saysounds() as saysounds: + try: + del saysounds[name] + await ctx.tick() + except KeyError: + await ctx.send(error("That say sound does not exist!")) + + @sfxset.command(name="file") + @checks.is_owner() + async def sfxset_file(self, ctx, *, on_off: bool = None): + """ + Enable or disable allowing to save audio files directly for playback + """ + curr = await self.config.attachments() + if on_off is None: + msg = "on" if curr else "off" + await ctx.send(f"Allowing saving of audio files is currently {msg}.") + return + + await self.config.attachments.set(on_off) + await ctx.tick() + + @sfxset.command(name="setup") + @checks.is_owner() + async def sfxset_setup(self, ctx): + """ + Run this first to inject code into audio cog for proper sfx usage. + + After injection, reload audio cog and run this again to confirm injection + was successful. + """ + audio_cog = self.bot.get_cog("Audio") + + if not audio_cog: + await ctx.send(error("Audio cog not loaded, load it first before running this.")) + return + + try: + if callable(audio_cog.sfx_play): + await ctx.send("Injection successful, SFX is ready to use!") + else: + await ctx.send("Function found but not callable, unknown error.") + return + except: + pass + + if not AUDIO_CODE_PATH: + await ctx.send(error("Please reload the cog after the Audio cog has been loaded!")) + return + + inject_path = str(code_path(cog_instance=self) / "injection.py") + with open(inject_path, "r") as f: + injection = f.read() + + inject_path = os.path.join(AUDIO_CODE_PATH, "utilities", "player.py") + with open(inject_path, "a") as f: + f.write(injection) + + await ctx.send("Injection complete, reload audio cog then run this command again to make sure it worked.") + + @commands.command(name="sfx") + @commands.guild_only() + @commands.cooldown(rate=1, per=10, type=commands.BucketType.user) + async def sfx(self, ctx, *, name: str): + """ + Play a say sound! + """ + if not ctx.author.voice or ctx.author.voice.channel is None: + await ctx.send(error("Connect to a voice channel to use this command.")) + return + + saysounds = await self.config.guild(ctx.guild).saysounds() + audio_cog = self.bot.get_cog("Audio") + play = self.bot.get_command("play") + volume = self.bot.get_command("volume") + if not play: + await ctx.send(error("Audio cog not loaded! Please contact bot owner.")) + return + + try: + sound = saysounds[name] + except KeyError: + # name doesn't have to be full name, will find closest match + matches = get_close_matches(name, list(saysounds.keys()), n=1, cutoff=0.7) + if matches: + sound = saysounds[matches[0]] + else: + await ctx.send(error("Say sound could not be found!")) + return + + # found saysound + + # charge user + cost = await self.get_cost(ctx.author, sound) + msg = None + if cost > 0: + currency_name = await bank.get_currency_name(ctx.guild) + try: + await bank.withdraw_credits(ctx.author, cost) + balance = await bank.get_balance(ctx.author) + msg = await ctx.send(f"Charged: {cost}, Balance: {balance}") + except ValueError: + balance = await bank.get_balance(ctx.author) + msg = await ctx.send( + error( + f"Sorry {ctx.author.name}, you do not have enough {currency_name} to use that say sound. (Cost: {cost}, Balance: {balance})" + ) + ) + await asyncio.sleep(10) + await msg.delete() + return + + await self.play(ctx, sound) + + if msg: + await asyncio.sleep(5) + await msg.delete() + + @commands.command(name="sfxlist") + async def sfx_list(self, ctx): + """ + List all say sounds for guild. + """ + + saysounds = await self.config.guild(ctx.guild).saysounds() + msg = [] + + keys = sorted(list(saysounds.keys())) + + for sound_name in keys: + msg.append((sound_name, saysounds[sound_name]["cost"])) + + msg = tabulate.tabulate(msg, ["Sound", "Cost"], tablefmt="github") + + pages = pagify(msg) + + for page in pages: + try: + await ctx.author.send(box(page)) + except: + await ctx.send("Please allow DMs from server members so I can DM you the list!") + return diff --git a/sfx/utils.py b/sfx/utils.py new file mode 100644 index 0000000..7a5638f --- /dev/null +++ b/sfx/utils.py @@ -0,0 +1,13 @@ +import inspect +from pathlib import Path + +# defines a saysound dictionary +def saysound(name: str, added_by: str, cost: int = 0, volume: int = 100, url: str = None, filepath: str = None) -> dict: + saysound = {"name": name, "added_by": added_by, "cost": cost, "volume": volume, "url": url, "filepath": filepath} + + return saysound + + +# gets path to main directory of cog's code +def code_path(cog_instance): + return Path(inspect.getfile(cog_instance.__class__)).parent