async def reload_clips(self, ctx): """Reloads the list of preset clips.""" ## I don't really like having core modules intertwined with dynamic ones, maybe move the appropriate admin ## modules out into their dynamic module and exposing some admin auth function that they check in with before ## running the command? if (not self.clips_cog): await ctx.send( "Sorry <@{}>, but the clips cog isn't available.".format( ctx.message.author.id)) return False if (not self.is_admin(ctx.message.author)): await ctx.send("<@{}> isn't allowed to do that.".format( ctx.message.author.id)) self.dynamo_db.put( dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False count = self.clips_cog.reload_clips() loaded_clips_string = "Loaded {} clip{}.".format( count, "s" if count != 1 else "") await ctx.send(loaded_clips_string) self.dynamo_db.put( dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, True)) return (count >= 0)
async def skip(self, ctx): """Vote to skip the current speech.""" state = self.get_speech_state(ctx.message.server) if(not state.is_speaking()): await self.bot.say("I'm not speaking at the moment.") self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False else: self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, True)) voter = ctx.message.author if(voter == state.current_speech.requester): await self.bot.say("<@{}> skipped their own speech.".format(voter.id)) await state.skip_speech(voter.voice_channel) ## Attempt to delete the command message await self.attempt_delete_command_message(ctx.message) return False elif(voter.id not in state.skip_votes): state.skip_votes.add(voter) total_votes = len([voter for voter in state.skip_votes if voter.voice_channel == state.voice_client.channel]) total_members = len([member for member in await state.get_members() if not member.bot]) vote_percentage = ceil((total_votes / total_members) * 100) if(total_votes >= self.skip_votes or vote_percentage >= self.skip_percentage): await self.bot.say("Skip vote passed by {}% of members. I'll skip the current speech.".format(vote_percentage)) await state.skip_speech() return True else: raw = "Skip vote added, currently at {}/{} or {}%" await self.bot.say(raw.format(total_votes, total_members, vote_percentage)) else: await self.bot.say("<@{}> has already voted!".format(voter.id))
async def say(self, ctx, *, message, ignore_char_limit=False, target_member=None): """Speaks your text aloud to your channel.""" ## Todo: look into memoization of speech. Phrases.py's speech is a perfect candidate ## Verify that the target/requester is in a channel if (not target_member or not isinstance(target_member, Member)): target_member = ctx.message.author voice_channel = target_member.voice_channel if(voice_channel is None): await self.bot.say("<@{}> isn't in a voice channel.".format(target_member.id)) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False ## Make sure the message isn't too long if(not self.tts_controller.check_length(message) and not ignore_char_limit): await self.bot.say("Keep phrases less than {} characters.".format(self.tts_controller.char_limit)) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False state = self.get_speech_state(ctx.message.server) if(state.voice_client is None): ## Todo: Handle exception if unable to create a voice client await self.create_voice_client(voice_channel) ## Parse down the message before sending it to the TTS service message = self.message_parser.parse_message(message, ctx.message) try: ## Create a .wav file of the message wav_path = await self.save(message, ignore_char_limit) if(wav_path): ## Create a player for the .wav player = state.voice_client.create_ffmpeg_player( wav_path, before_options=self.ffmpeg_before_options, options=self.ffmpeg_options, after=state.next_speech ) else: raise RuntimeError("Unable to save a proper .wav file.") except Exception as e: utilities.debug_print("Exception in say():", e, debug_level=0) await self.bot.say("Unable to say the last message. Sorry, <@{}>.".format(ctx.message.author.id)) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False else: ## On successful player creation, build a SpeechEntry and push it into the queue await state.speech_queue.put(SpeechEntry(ctx.message.author, voice_channel, player, wav_path)) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, True)) ## Attempt to delete the command message await self.attempt_delete_command_message(ctx.message) ## Start a timeout to disconnect the bot if the bot hasn't spoken in a while await self.attempt_leave_channel(state) return True
async def disconnect(self, ctx): """ Disconnect from the current voice channel.""" if(not self.is_admin(ctx.message.author)): await ctx.send("<@{}> isn't allowed to do that.".format(ctx.message.author.id)) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False state = self.audio_player_cog.get_server_state(ctx) await state.ctx.voice_client.disconnect() self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, True)) return True
async def on_command_error(ctx, exception): '''Handles command errors. Attempts to find a similar command and suggests it, otherwise directs the user to the help prompt.''' logger.exception("Unable to process command.", exc_info=exception) self.dynamo_db.put( dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False, str(exception))) ## Attempt to find a command that's similar to the one they wanted. Otherwise just direct them to the help page most_similar_command = self.find_most_similar_command( ctx.message.content) if (most_similar_command[0] == ctx.invoked_with): ## Handle issues where the command is valid, but couldn't be completed for whatever reason. await ctx.send("I'm sorry <@{}>, I'm afraid I can't do that.\n" \ "Discord is having some issues that won't let me speak right now." .format(ctx.message.author.id)) else: help_text_chunks = [ "Sorry <@{}>, **{}{}** isn't a valid command.".format( ctx.message.author.id, ctx.prefix, ctx.invoked_with) ] if (most_similar_command[1] > self.invalid_command_minimum_similarity): help_text_chunks.append("Did you mean **{}{}**?".format( self.activation_string, most_similar_command[0])) else: help_text_chunks.append("Try the **{}help** page.".format( self.activation_string)) ## Dump output to user await ctx.send(" ".join(help_text_chunks)) return
async def reload_cogs(self, ctx): """Reloads the bot's cogs.""" if(not self.is_admin(ctx.message.author)): await ctx.send("<@{}> isn't allowed to do that.".format(ctx.message.author.id)) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False count = self.hawking.module_manager.reload_all() total = len(self.hawking.module_manager.modules) loaded_cogs_string = "Loaded {} of {} cogs.".format(count, total) await ctx.send(loaded_cogs_string) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, True)) return (count >= 0)
async def summon(self, ctx): """Summons the bot to join your voice channel.""" ## Check that the requester is in a voice channel summoned_channel = ctx.message.author.voice_channel if(summoned_channel is None): await self.bot.say("{} isn't in a voice channel.".format(ctx.message.author)) self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False)) return False else: self.dynamo_db.put(dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, True)) ## Attempt to delete the command message await self.attempt_delete_command_message(ctx.message) return await self.join_channel(summoned_channel)
async def _plot(self, ctx, message, map_name): """Plots your given plane's path on the game map.""" ## Parse the user's command try: path_obj = self.path_parser.parse_message(message) except RuntimeError as e: ## Give them some feedback if the command isn't understandable await self.failed_command_feedback(e) ## Put some information about the failed query into the database self.dynamo_db.put( dynamo_helper.DynamoItem(ctx.message.author.id, ctx.message.timestamp.timestamp(), ctx.message.channel.name, ctx.message.server.name, map_name, message, None)) return None else: ## Put some information about the successful query into the database self.dynamo_db.put( dynamo_helper.DynamoItem(ctx.message.author.id, ctx.message.timestamp.timestamp(), ctx.message.channel.name, ctx.message.server.name, map_name, message, str(path_obj))) ## Get the file path for the final map image, and generate a callback to delete the image plotted_map = self.plotter.plot_plane_path(map_name, path_obj) map_path = self.plotter.file_controller.save_map(plotted_map) delete_map_callback = self.plotter.file_controller.create_delete_map_callback( map_path) ## Upload the file to the user's channel in Discord. return await self.upload_file( map_path, ctx.message.channel, content="Here you go, <@{}>. Good luck!".format( ctx.message.author.id), callback=delete_map_callback)
async def on_command_error(ctx, exception): '''Handles command errors. Attempts to find a similar command and suggests it, otherwise directs the user to the help prompt.''' self.dynamo_db.put( dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False, str(exception))) ## Attempt to find a command that's similar to the one they wanted. Otherwise just direct them to the help page most_similar_command = self.find_most_similar_command( ctx.message.content) if (most_similar_command[0] == ctx.invoked_with): logger.exception( "Unable to complete command, with content: {}, for author: {}, in channel {}, in server: {}" .format(ctx.message.content, ctx.message.author.name, ctx.guild.name), exc_info=exception) ## Handle issues where the command is valid, but couldn't be completed for whatever reason. await ctx.send( "I'm sorry <@{}>, I'm afraid I can't do that.\nSomething went wrong, and I couldn't complete the command." .format(ctx.message.author.id)) else: logger.exception( "Received invalid command: '{0}{1}', suggested: '{0}{2}', for author: {3}, in server: {4}" .format(self.activation_str, ctx.invoked_with, most_similar_command[0], ctx.message.author.name, ctx.guild.name), exc_info=exception) help_text_chunks = [ "Sorry <@{}>, **{}{}** isn't a valid command.".format( ctx.message.author.id, ctx.prefix, ctx.invoked_with) ] if (most_similar_command[1] > self.invalid_command_minimum_similarity): help_text_chunks.append("Did you mean **{}{}**?".format( self.activation_str, most_similar_command[0])) else: help_text_chunks.append("Try the **{}help** page.".format( self.activation_str)) ## Dump output to user await ctx.send(" ".join(help_text_chunks)) return
async def on_command_error(exception, ctx): # discord.py uses reflection to set the destination chat channel for whatever reason (sans command ctx) _internal_channel = ctx.message.channel utilities.debug_print(exception, debug_level=2) self.dynamo_db.put(dynamo_helper.DynamoItem( ctx, ctx.message.content, inspect.currentframe().f_code.co_name, False, str(exception))) ## Handy for debugging # import traceback # print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) # traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr) ## Permissions error? if (isinstance(exception, CommandInvokeError) and isinstance(exception.original, TimeoutError)): await self.bot.say("Sorry <@{}>, I'm not able to join the voice channel right now. Discord might be having issues, or I might not have permission to join." .format(ctx.message.author.id)) return ## Poorly handled (for now, until I can get more concrete examples in my database) error messages for users if ("code =" in str(exception)): await self.bot.say("Sorry <@{}>, Discord is having some issues that won't let me speak right now." .format(ctx.message.author.id)) return ## Attempt to find a command that's similar to the one they wanted. Otherwise just direct them to the help page else: most_similar_command = self.find_most_similar_command(ctx.message.content) if (most_similar_command[0] == ctx.invoked_with): ## Handle issues where the command is valid, but couldn't be completed for whatever reason. await self.bot.say("Sorry <@{}>, I can't talk right now. Try again in a little bit.".format(ctx.message.author.id)) else: ## Otherwise, handle other issues involving invalid commands help_text_chunks = [ "Sorry <@{}>, **{}{}** isn't a valid command.".format(ctx.message.author.id, ctx.prefix, ctx.invoked_with) ] ## Build the output to give to the user if (most_similar_command[1] > self.invalid_command_minimum_similarity): help_text_chunks.append("Did you mean **{}{}**?".format(self.activation_str, most_similar_command[0])) else: help_text_chunks.append("Try the **{}help** page.".format(self.activation_str)) await self.bot.say(" ".join(help_text_chunks))
async def play_audio(self, ctx, file_path: str, target_member=None, callback: Callable = None): '''Plays the given audio file aloud to your channel''' ## Verify that the target/requester is in a channel if (not target_member or not isinstance(target_member, Member)): target_member = ctx.message.author voice_channel = None if (target_member.voice): ## Handle users not in a voice channel voice_channel = target_member.voice.channel if (voice_channel is None): await ctx.send("<@{}> isn't in a voice channel.".format( target_member.id)) return False ## Make sure file_path points to an actual file if (not os.path.isfile(file_path)): logger.error( "Unable to play file at: {}, file doesn't exist or isn't a file." .format(file_path)) await ctx.send("Sorry, <@{}>, that couldn't be played.".format( ctx.message.author.id)) return False ## Get/Build a state for this audio, build the player, and add it to the state state = self.get_server_state(ctx) player = self.build_player(file_path) await state.add_play_request( AudioPlayRequest(ctx.message.author, voice_channel, player, file_path, callback)) self.dynamo_db.put( dynamo_helper.DynamoItem(ctx, ctx.message.content, inspect.currentframe().f_code.co_name, True)) return True