Exemple #1
0
        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)
Exemple #2
0
    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
Exemple #3
0
    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
Exemple #4
0
    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
Exemple #5
0
    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
Exemple #6
0
 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()
Exemple #7
0
        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
Exemple #8
0
        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)
Exemple #9
0
 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
Exemple #10
0
 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)
Exemple #11
0
 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
Exemple #12
0
        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)
Exemple #13
0
 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
Exemple #14
0
    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()
Exemple #15
0
        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
Exemple #16
0
        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))
Exemple #17
0
        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
Exemple #18
0
        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)
Exemple #19
0
    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
Exemple #20
0
    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)
Exemple #21
0
    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
Exemple #22
0
    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
Exemple #23
0
    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
Exemple #24
0
 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)
Exemple #25
0
 def enter(self, char, string):
     utilities.debug_print("Error", char, string, debug_level=4)
     return None