def enter(self, string, **kwargs): char, consumed_str = self.emit_consume_char(string) utilities.debug_print("Enter {} '{}' '{}'".format( self.__class__.__name__, consumed_str, char), kwargs, debug_level=4) beat_length = kwargs.get("beat_length", 0.25) duration = kwargs.get("duration", 1) note = kwargs.get("note") assert note is not None sharp = kwargs.get("sharp", False) octave = kwargs.get("octave", kwargs.get("default_octave", 2)) note_obj = Note(beat_length, duration, note, sharp, octave, []) ## Clean up kwargs for next pass kwargs.pop("note_obj", None) kwargs.pop("duration", None) kwargs.pop("note", None) kwargs.pop("sharp", None) kwargs.pop("octave", None) ## Next state return self.exit(char, string, note_obj=note_obj, **kwargs)
def init_phrases(self): phrase_file_paths = self.scan_phrases(self.phrases_folder_path) counter = 0 for phrase_file_path in phrase_file_paths: starting_count = counter phrase_group = self._build_phrase_group(phrase_file_path) for phrase in self.load_phrases(phrase_file_path): try: self.add_phrase(phrase) phrase_group.add_phrase(phrase) except Exception as e: utilities.debug_print(e, "Skipping...", debug_level=2) else: counter += 1 ## Ensure we don't add in empty phrase files into the groupings if(counter > starting_count): self.phrase_groups[phrase_group.key] = phrase_group ## Set up a dummy command for the category, to help with the help interface. See help_formatter.py help_command = commands.Command(phrase_group.key, lambda noop: None, hidden=True, no_pm=True) self.bot.add_command(help_command) self.command_names.append(phrase_group.key) # Keep track of the 'parent' commands for later use print("Loaded {} phrase{}.".format(counter, "s" if counter != 1 else "")) return counter
async def _announce(self, state, message, callback=None): """Internal way to speak text to a specific speech_state """ try: ## Create a .wav file of the message wav_path = await self.save(message, True) 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 _announce():", e, debug_level=0) return False else: ## On successful player creation, build a SpeechEntry and push it into the queue await state.speech_queue.put(SpeechEntry(None, state.voice_client.channel, player, wav_path, callback)) ## 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 join_channel(self, channel): state = self.get_speech_state(channel.server) ## Check if we've already got a voice client if(state.voice_client): ## Check if bot is already in the desired channel if(state.voice_client.channel == channel): return True ## Otherwise, move it into the desired channel try: await state.voice_client.move_to(channel) except Exception as e: utilities.debug_print("Voice client exists", e, debug_level=2) return False else: return True ## Otherwise, create a new one try: await self.create_voice_client(channel) except (discord.ClientException, discord.InvalidArgument) as e: utilities.debug_print("Voice client doesn't exist", e, debug_level=2) return False else: return True
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
def run(self): ## Keep bot going despite any misc service errors try: self.bot.run(utilities.load_json(self.token_file_path)[self.TOKEN_KEY]) except Exception as e: utilities.debug_print("Critical exception when running bot", e, debug_level=0) time.sleep(1) self.run()
def _delete_map_callback(): try: os.remove(path) except OSError as e: utilities.debug_print("Error deleting map at: '{}'.".format(path), e, debug_level=1) return False return True
def enter(self, string, **kwargs): char = self.emit_char(string) utilities.debug_print("Enter {} '{}'".format( self.__class__.__name__, string), kwargs, debug_level=4) return self.exit(char, string, **kwargs)
def load_base_maps(self): maps = {} try: for map_name, map_path in self.map_file_paths.items(): maps[map_name] = Image.open(map_path) except Exception as e: utilities.debug_print("Error opening base_map.", e, debug_level=0) return maps
def _init_dir(self): if(not os.path.exists(self.output_folder_path)): os.makedirs(self.output_folder_path) else: for root, dirs, files in os.walk(self.output_folder_path, topdown=False): for file in files: try: os.remove(os.sep.join([root, file])) except OSError as e: utilities.debug_print("Error removing file: {}, during temp dir cleanup.".format(file), e, debug_level=2)
def save_map(self, pillow_image, file_name=None): file_name = self._generate_unique_file_name(self.map_file_extension) if not file_name else file_name file_path = os.sep.join([self.output_folder_path, file_name]) try: pillow_image.save(file_path, format=self.map_file_extension) except IOError as e: utilities.debug_print("Unable to save image at: '{}'.".format(file_path), e, debug_level=0) return None else: return file_path
def enter(self, string, **kwargs): char, consumed_str = self.emit_consume_char(string) utilities.debug_print("Enter {} '{}' '{}'".format( self.__class__.__name__, consumed_str, char), kwargs, debug_level=4) return self.exit(self.emit_char(consumed_str), consumed_str, octave=int(char), **kwargs)
def put(self, dynamo_item): if (self.enabled): try: return self.table.put_item(Item=dynamo_item.getDict()) except Exception as e: ## Don't let issues with dynamo tank the bot's functionality utilities.debug_print("Exception while performing dynamo put", e, debug_level=1) return None else: return None
async def attempt_leave_channel(self, state): ## Handy closure to preserve leave_channel's argument async def leave_channel_closure(): await self.leave_channel(state.voice_client.channel) ## Attempt to leave the state's channel await asyncio.sleep(self.channel_timeout) if(state.last_speech_time + self.channel_timeout <= state.get_current_time() and state.voice_client): utilities.debug_print("Leaving channel", debug_level=4) if(len(self.channel_timeout_phrases) > 0): await self._announce(state, choice(self.channel_timeout_phrases), leave_channel_closure) else: await leave_channel_closure()
def exit(self, char, string, **kwargs): for regex_string, handler in self.exit_dict.items(): match = re.match(regex_string, char) utilities.debug_print("MATCHING", regex_string, char, handler.__self__.__class__.__name__, debug_level=4) if (match): return handler(string, **kwargs) if (self.error_handler): self.error_handler(char, string) return None
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))
def enter(self, string, **kwargs): utilities.debug_print("Enter {} '{}'".format( self.__class__.__name__, string), kwargs, debug_level=4) note_obj = kwargs.get("note_obj") sub_notes = kwargs.get("sub_notes", []) beat_length = kwargs.get("beat_length", 0.25) / (len(sub_notes) + 1) if (note_obj): for note in sub_notes: note.beat_length = beat_length note_obj.beat_length = beat_length note_obj.sub_notes = sub_notes return note_obj
def enter(self, string, **kwargs): char, consumed_str = self.emit_consume_char(string) utilities.debug_print("Enter {} '{}' '{}'".format( self.__class__.__name__, consumed_str, char), kwargs, debug_level=4) note_obj = kwargs.get("note_obj") assert note_obj is not None sub_notes = kwargs.get("sub_notes", []) sub_notes.append(note_obj) ## Clean up kwargs for next pass kwargs.pop("note_obj", None) kwargs.pop("sub_notes", None) return self.exit(self.emit_char(consumed_str), consumed_str, sub_notes=sub_notes, **kwargs)
async def upload_file(self, file_path, channel, content=None, callback=None): ## Pythonically open and upload the image to the given channel with open(file_path, "rb") as fd: try: await self.bot.send_file(channel, fd, content=content) except errors.HTTPException as e: utilities.debug_print("Error uploading file at: '{}'", e, debug_level=0) await self.failed_upload_feedback(e) return False ## Call the callback function, provided it exists if (callback): return callback() else: return True
def load_phrases(self, path): ## Insert source[key] (if it exists) into target[key], else insert a default string def insert_if_exists(target, source, key, default=None): if(key in source): target[key] = source[key] return target phrases = [] with open(path) as fd: for phrase_raw in json.load(fd)[self.PHRASES_KEY]: try: message = phrase_raw[self.MESSAGE_KEY] ## Todo: make this less ugly kwargs = {} help_value = phrase_raw.get(self.HELP_KEY) # fallback for the help submenus kwargs = insert_if_exists(kwargs, phrase_raw, self.HELP_KEY) kwargs = insert_if_exists(kwargs, phrase_raw, self.BRIEF_KEY, help_value) ## Attempt to populate the description kwarg, but if it isn't available, then try and parse the ## message down into something usable instead. if (self.DESCRIPTION_KEY in phrase_raw): kwargs[self.DESCRIPTION_KEY] = phrase_raw[self.DESCRIPTION_KEY] else: kwargs[self.DESCRIPTION_KEY] = self.process_string_into_searchable(message) phrase_name = phrase_raw[self.NAME_KEY] phrase = Phrase( phrase_name, message, phrase_raw.get(self.IS_MUSIC_KEY, False), **kwargs ) phrases.append(phrase) self.command_names.append(phrase_name) except Exception as e: utilities.debug_print("Error loading {} from {}. Skipping...".format(phrase_raw, fd), e, debug_level=3) ## Todo: This doesn't actually result in the phrases in the help menu being sorted? return sorted(phrases, key=lambda phrase: phrase.name)
def delete(self, file_path): ## Basically, windows spits out a 'file in use' error when speeches are deleted after ## being skipped, probably because of the file being loaded into the ffmpeg player. So ## if the deletion fails, just pop it into a list of paths to delete on the next go around. if(os.path.isfile(file_path)): self.paths_to_delete.append(file_path) to_delete = [] for index, path in enumerate(self.paths_to_delete): try: os.remove(path) except FileNotFoundError: ## The goal was to remove the file, and as long as it doesn't exist then we're good. continue except Exception as e: utilities.debug_print("Error deleting file:", path, type(e).__name__, e, debug_level=1) to_delete.append(path) self.paths_to_delete = to_delete[:] return True
async def save(self, message, ignore_char_limit=False): ## Validate output directory if(not self.output_dir_path): utilities.debug_print("Unable to save without output_dir_path set. See {}.__init__".format(self.__name__), debug_level=0) return None ## Check message size if(not self.check_length(message) and not ignore_char_limit): return None ## Generate and validate filename output_file_path = os.sep.join([self.output_dir_path, self._generate_unique_file_name(self.output_extension)]) ## Parse options and message save_option = '-w "{}"'.format(output_file_path) message = self._parse_message(message) ## Format and invoke args = '{} {} "{}"'.format( self.exe_path, save_option, message ) ## Prepend the windows emulator if using linux (I'm aware of what WINE means) if(utilities.is_linux()): args = "{} {}".format(self.wine, args) ## Prepend the fake display created with Xvfb if running headless if(self.is_headless): args = "{} {}".format(self.xvfb_prepend, args) retval = os.system(args) if(retval == 0): return output_file_path else: return None
def discover(self): ## Assumes that the modules folder is inside the root modules_folder_path = os.path.abspath( os.path.sep.join(["..", self.modules_folder])) ## Expose the modules folder to the interpreter, so modules can be loaded sys.path.append(modules_folder_path) ## Build a list of potential module paths and iterate through it... candidate_modules = os.listdir(modules_folder_path) for candidate in candidate_modules: ## If the file could be a python file... if (candidate[-3:] == ".py"): name = candidate[:-3] ## Attempt to import the module (akin to 'import [name]') and register it normally ## NOTE: Modules MUST have a 'main()' function that essentially returns a list containing all the args ## needed by the 'register()' method of this ModuleManager class. At a minimum this list MUST ## contain a reference to the class that serves as an entry point to the module. You should also ## specify whether or not a given module is a cog (for discord.py) or not. try: module = importlib.import_module(name) declarations = module.main() ## Validate the shape of the main() method's data, and attempt to tolerate poor formatting if (not isinstance(declarations, list)): declarations = [declarations] elif (len(declarations) == 0): raise RuntimeError( "Module '{}' main() returned empty list. Needs a class object at minimum." .format(module.__name__)) self.register(*declarations) except Exception as e: utilities.debug_print( "Unable to import module: {},".format(name), e, debug_level=2) del module
async def attempt_delete_command_message(self, message): if(self.delete_commands): try: await self.bot.delete_message(message) except errors.Forbidden: utilities.debug_print("Bot doesn't have permission to delete the message", debug_level=3)
def enter(self, char, string): utilities.debug_print("Error", char, string, debug_level=4) return None