Esempio n. 1
0
    def onStop(self):
        super().onStop()
        if not self._recording:
            return

        try:
            online = requests.get(
                'https://api.projectalice.io/generate_204').status_code == 204
        except:
            online = False

        if not online:
            self.logInfo('We are currently offline, cannot send log reports')
            os.remove(self._flagFile)
            return

        repo = Repository(directory=self.Commons.rootDir())
        if not repo.isUpToDate():
            self.logInfo(
                'Alice is not up to date. Please first update to latest version and retry before trying to submit a bug report again.'
            )
            os.remove(self._flagFile)
            return

        if not self._history or not self._title:
            self.logInfo('Nothing to report')
        elif not self.ConfigManager.githubAuth:
            self.logWarning(
                'Cannot report bugs if Github user and token are not set in configs'
            )
        else:
            title = f'[AUTO BUG REPORT] {self._title}'
            body = '\n'.join(self._history)
            data = {'title': title, 'body': f'```\n{body}\n```'}

            request = requests.post(
                url=f'{constants.GITHUB_API_URL}/ProjectAlice/issues',
                data=json.dumps(data),
                auth=self.ConfigManager.githubAuth)
            if request.status_code != 201:
                self.logError(
                    f'Something went wrong reporting a bug, status: {request.status_code}, error: {request.json()}'
                )
            else:
                self.logInfo(
                    f'Created new issue: {request.json()["html_url"]}')

        os.remove(self._flagFile)
 def __init__(self, installer: dict):
     self._installer = installer
     self._updateAvailable = False
     self._name = installer['name']
     self._icon = self._installer.get('icon', 'fas fa-biohazard')
     self._aliceMinVersion = Version.fromString(
         self._installer.get('aliceMinVersion', '1.0.0-b4'))
     self._maintainers = self._installer.get('maintainers', list())
     self._description = self._installer.get('desc', '')
     self._category = self._installer.get('category', constants.UNKNOWN)
     self._conditions = self._installer.get('conditions', dict())
     self._skillPath = Path('skills') / self._name
     self._repository = Repository(directory=self._skillPath,
                                   init=True,
                                   raiseIfExisting=False)
     super().__init__()
Esempio n. 3
0
	def getSkillRepository(self, skillName: str, directory: str = None) -> Optional[Repository]:
		"""
		Returns a Git object for the given skill
		:param skillName:
		:param directory: where to look for that skill, if not standard directory
		:return:
		"""

		if not directory:
			directory = self.getSkillDirectory(skillName=skillName)

		try:
			return Repository(directory=directory)
		except:
			raise
Esempio n. 4
0
	def downloadSkills(self, skills: Union[str, List[str]]) -> Optional[Dict]:
		"""
		Clones skills. Existence of the skill online is checked
		:param skills:
		:return: Dict: a dict of created repositories
		"""
		if isinstance(skills, str):
			skills = [skills]

		repositories = dict()

		for skillName in skills:
			try:
				tag = self.SkillStoreManager.getSkillUpdateTag(skillName=skillName)
				response = requests.get(f'{constants.GITHUB_RAW_URL}/skill_{skillName}/{tag}/{skillName}.install')
				if response.status_code != 200:
					raise GithubNotFound

				self.logInfo(f'Now downloading **{skillName}** version **{tag}**')

				with suppress(): # Increment download counter
					requests.get(f'https://skills.projectalice.ch/{skillName}')

				installFile = response.json()

				if not self.ConfigManager.getAliceConfigByName('devMode'):
					self.checkSkillConditions(installer=installFile)

				source = self.getGitRemoteSourceUrl(skillName=skillName, doAuth=False)

				try:
					repository = self.getSkillRepository(skillName=skillName)
				except PathNotFoundException:
					repository = Repository.clone(url=source, directory=self.getSkillDirectory(skillName=skillName), makeDir=True)
				except NotGitRepository:
					shutil.rmtree(self.getSkillDirectory(skillName=skillName), ignore_errors=True)
					repository = Repository.clone(url=source, directory=self.getSkillDirectory(skillName=skillName), makeDir=True)
				except:
					raise

				repository.checkout(tag=tag)
				repositories[skillName] = repository
			except GithubNotFound:
				if skillName in self.NEEDED_SKILLS:
					self._busyInstalling.clear()
					self.logFatal(f"Skill **{skillName}** is required but wasn't found in released skills, cannot continue")
					return repositories
				else:
					self.logError(f'Skill "{skillName}" not found in released skills')
					continue
			except SkillNotConditionCompliant as e:
				if self.notCompliantSkill(skillName=skillName, exception=e):
					continue
				else:
					self._busyInstalling.clear()
					return repositories
			except Exception as e:
				if skillName in self.NEEDED_SKILLS:
					self._busyInstalling.clear()
					self.logFatal(f'Error downloading skill **{skillName}** and it is required, cannot continue: {e}')
					return repositories
				else:
					self.logError(f'Error downloading skill "{skillName}": {e}')
					continue
		return repositories
    def __init__(self,
                 supportedIntents: Iterable = None,
                 databaseSchema: dict = None,
                 **kwargs):
        super().__init__(**kwargs)

        try:
            self._skillPath = Path(inspect.getfile(self.__class__)).parent
            self._installFile = Path(inspect.getfile(
                self.__class__)).with_suffix('.install')
            self._installer = json.loads(self._installFile.read_text())
        except FileNotFoundError:
            raise SkillInstanceFailed(
                skillName=constants.UNKNOWN,
                error=f'[{type(self).__name__}] Cannot find install file')
        except Exception as e:
            raise SkillInstanceFailed(
                skillName=constants.UNKNOWN,
                error=f'[{type(self).__name__}] Failed loading skill: {e}')

        instructionsFile = self.getResource(
            f'instructions/{self.LanguageManager.activeLanguage}.md')
        if not instructionsFile.exists():
            instructionsFile = self.getResource(f'instructions/en.md')

        self._instructions = instructionsFile.read_text(
        ) if instructionsFile.exists() else ''

        self._name = self._installer['name']
        self._author = self._installer.get('author', constants.UNKNOWN)
        self._version = self._installer.get('version', '0.0.1')
        self._icon = self._installer.get('icon', 'fas fa-biohazard')
        self._aliceMinVersion = Version.fromString(
            self._installer.get('aliceMinVersion', '1.0.0-b4'))
        self._maintainers = self._installer.get('maintainers', list())
        self._description = self._installer.get('desc', '')
        self._category = self._installer.get('category', constants.UNKNOWN)
        self._conditions = self._installer.get('conditions', dict())
        self._updateAvailable = False
        self._active = False
        self._delayed = False
        self._required = False
        self._failedStarting = False
        self._databaseSchema = databaseSchema
        self._widgets = list()
        self._widgetTemplates = dict()
        self._deviceTypes = list()
        self._intentsDefinitions = dict()
        self._scenarioPackageName = ''
        self._scenarioPackageVersion = Version(mainVersion=0,
                                               updateVersion=0,
                                               hotfix=0)
        self._supportedIntents: Dict[str, Intent] = self.buildIntentList(
            supportedIntents)
        self._repository = Repository(directory=self._skillPath,
                                      init=True,
                                      raiseIfExisting=False)
        self.loadIntentsDefinition()

        self._utteranceSlotCleaner = re.compile('{(.+?):=>.+?}')
        self._myDevicesTemplates = dict()
        self._myDevices: Dict[str, Device] = dict()
class AliceSkill(ProjectAliceObject):
    def __init__(self,
                 supportedIntents: Iterable = None,
                 databaseSchema: dict = None,
                 **kwargs):
        super().__init__(**kwargs)

        try:
            self._skillPath = Path(inspect.getfile(self.__class__)).parent
            self._installFile = Path(inspect.getfile(
                self.__class__)).with_suffix('.install')
            self._installer = json.loads(self._installFile.read_text())
        except FileNotFoundError:
            raise SkillInstanceFailed(
                skillName=constants.UNKNOWN,
                error=f'[{type(self).__name__}] Cannot find install file')
        except Exception as e:
            raise SkillInstanceFailed(
                skillName=constants.UNKNOWN,
                error=f'[{type(self).__name__}] Failed loading skill: {e}')

        instructionsFile = self.getResource(
            f'instructions/{self.LanguageManager.activeLanguage}.md')
        if not instructionsFile.exists():
            instructionsFile = self.getResource(f'instructions/en.md')

        self._instructions = instructionsFile.read_text(
        ) if instructionsFile.exists() else ''

        self._name = self._installer['name']
        self._author = self._installer.get('author', constants.UNKNOWN)
        self._version = self._installer.get('version', '0.0.1')
        self._icon = self._installer.get('icon', 'fas fa-biohazard')
        self._aliceMinVersion = Version.fromString(
            self._installer.get('aliceMinVersion', '1.0.0-b4'))
        self._maintainers = self._installer.get('maintainers', list())
        self._description = self._installer.get('desc', '')
        self._category = self._installer.get('category', constants.UNKNOWN)
        self._conditions = self._installer.get('conditions', dict())
        self._updateAvailable = False
        self._active = False
        self._delayed = False
        self._required = False
        self._failedStarting = False
        self._databaseSchema = databaseSchema
        self._widgets = list()
        self._widgetTemplates = dict()
        self._deviceTypes = list()
        self._intentsDefinitions = dict()
        self._scenarioPackageName = ''
        self._scenarioPackageVersion = Version(mainVersion=0,
                                               updateVersion=0,
                                               hotfix=0)
        self._supportedIntents: Dict[str, Intent] = self.buildIntentList(
            supportedIntents)
        self._repository = Repository(directory=self._skillPath,
                                      init=True,
                                      raiseIfExisting=False)
        self.loadIntentsDefinition()

        self._utteranceSlotCleaner = re.compile('{(.+?):=>.+?}')
        self._myDevicesTemplates = dict()
        self._myDevices: Dict[str, Device] = dict()

    @property
    def modified(self) -> bool:
        return self._repository.isDirty()

    @property
    def repository(self) -> Repository:
        return self._repository

    @property
    def failedStarting(self) -> bool:
        return self._failedStarting

    @property
    def myDevices(self) -> Dict[str, Device]:
        return self._myDevices

    @failedStarting.setter
    def failedStarting(self, value: bool):
        self._failedStarting = value

    def registerDeviceInstance(self, device: Device):
        if device.paired:
            self._myDevices[device.uid] = device

    def unregisterDeviceInstance(self, device: Device):
        self._myDevices.pop(device.uid, None)

    def getHtmlInstructions(self) -> flask.Markup:
        return flask.Markup(markdown(self._instructions))

    def addUtterance(self,
                     text: str,
                     intent: str,
                     language: str = None) -> bool:
        """
		Add the supplied utterance for a given skill to the dialogTemplate extending file of the
		current active language if no specific language is supplied.
		:param text:
		:param intent:
		:param language: default None will load the active Language from language manager
		:return:
		"""
        lang = language if language is not None else self.activeLanguage()
        file = self.getResource(f'dialogTemplate/{lang}.ext.json')
        file.touch()

        data: Dict = json.loads(file.read_text())
        data.setdefault('intents', dict())
        data['intents'].setdefault(intent, dict())
        data['intents'][intent].setdefault('utterances', list())

        utterances = data['intents'][intent]['utterances']
        if not text in utterances:
            utterances.append(text)
            data['intents'][intent]['utterances'] = utterances
            file.write_text(json.dumps(data, ensure_ascii=False, indent='\t'))
            return True

        return False

    def loadScenarioNodes(self) -> None:
        """
		Load the scenario nodes (folder scenarioNodes) for Node-Red and store them in _scenarioPackageName
		:return:
		"""
        path = self.getResource('scenarioNodes/package.json')
        if not path.exists():
            return

        try:
            with path.open('r') as fp:
                data = json.load(fp)
                self._scenarioPackageName = data['name']
                self._scenarioPackageVersion = Version.fromString(
                    data['version'])
        except Exception as e:
            self.logWarning(f'Failed to load scenario nodes: {e}')

    def loadIntentsDefinition(self):
        dialogTemplate = self.getResource('dialogTemplate')

        for lang in self.LanguageManager.supportedLanguages:
            try:
                path = dialogTemplate / f'{lang}.json'
                if not path.exists():
                    continue

                with path.open('r') as fp:
                    data = json.load(fp)

                    if 'intents' not in data:
                        continue

                    self._intentsDefinitions[lang] = dict()
                    for intent in data['intents']:
                        self._intentsDefinitions[lang][
                            intent['name']] = intent['utterances']
            except Exception as e:
                self.logWarning(
                    f'Something went wrong loading intent definition for skill **{self._name}**, language **{lang}**: {e}'
                )

    def buildIntentList(self, supportedIntents) -> dict:
        supportedIntents = supportedIntents or list()
        intents: Dict[str, Intent] = self.findDecoratedIntents()
        for item in supportedIntents:
            if isinstance(item, tuple):
                intent = item[0]
                if not isinstance(intent, Intent):
                    intent = Intent(intent, userIntent=False)
                intent.fallbackFunction = item[1]
                item = intent
            elif not isinstance(item, Intent):
                item = Intent(item, userIntent=False)

            if str(item) in intents:
                intents[str(item)].addDialogMapping(item.dialogMapping,
                                                    skillName=self.name)

                if item.fallbackFunction:
                    intents[str(item)].fallbackFunction = item.fallbackFunction

                # always use the highest auth level specified (low values mean a higher auth level)
                if item.authLevel < intents[str(item)].authLevel:
                    intents[str(item)].authLevel = item.authLevel
            else:
                intents[str(item)] = item

        return intents

    def findDecoratedIntents(self) -> dict:
        intentMappings = dict()
        functionNames = [
            name for name, func in self.__class__.__dict__.items()
            if callable(func)
        ]
        for name in functionNames:
            function = getattr(self, name)
            intents = getattr(function, 'intents', list())
            for intentMapping in intents:
                intent = intentMapping['intent']
                requiredState = intentMapping['requiredState']
                if str(intent) not in intentMappings:
                    intentMappings[str(intent)] = intent

                if requiredState:
                    intentMappings[str(intent)].addDialogMapping(
                        {requiredState: function}, skillName=self.name)
                else:
                    intentMappings[str(intent)].fallbackFunction = function

                # always use the highest auth level specified (low values mean a higher auth level)
                if intent.authLevel < intentMappings[str(intent)].authLevel:
                    intentMappings[str(intent)].authLevel = intent.authLevel

        return intentMappings

    def loadWidgets(self) -> None:
        """
		Load all .py files in the widgets folder and load them as instances of Widget.
		Loaded widget types are added to self._widgets
		:return:
		"""
        fp = self.getResource('widgets')
        if fp.exists():
            self.logInfo(f"Found **{len(list(fp.glob('*.py'))) - 1}** widget",
                         plural='widget')
            for file in fp.glob('*.py'):
                if file.name.startswith('__'):
                    continue

                self._widgets.append(Path(file).stem)
                self.loadWidgetConfigTemplate(Path(file).stem)

    def loadWidgetConfigTemplate(self, widgetType):
        """
		Load the config file of the current widget type.
		The config has to be in the default alice json format containing the config value names, default value and value type.
		:param widgetType:
		:return:
		"""
        try:
            filepath = Path(
                f'skills/{self._name}/widgets/{widgetType}.config.template')
            if not filepath.exists():
                self.logInfo(
                    f'![green](No widget config template for widget type {widgetType} found)'
                )
                return

            data = json.loads(filepath.read_text())

            self._widgetTemplates[widgetType] = data
        except Exception as e:
            self.logError(
                f'Error loading widget config template for widget type **{widgetType}** {e}'
            )

    def getWidgetTemplate(self, name: str) -> dict:
        """
		Get the config template for the given widget type in the current skill instance.
		:param name:
		:return:
		"""
        if name in self._widgetTemplates:
            return self._widgetTemplates[name]
        else:
            return dict()

    def loadDeviceTypes(self) -> None:
        """
		Load all .py files in the devices folder and load them as instances of DeviceType.
		Loaded devices types are added to self._deviceTypes
		:return:
		"""
        fp = self.getResource('devices')
        if fp.exists():
            self.logInfo(
                f"Found **{len(list(fp.glob('*.py'))) - 1}** device type",
                plural='type')
            for file in fp.glob('*.py'):
                if file.name.startswith('__'):
                    continue

                self._deviceTypes.append(Path(file).stem)

                try:
                    deviceImport = importlib.import_module(
                        f'skills.{self.name}.devices.{file.stem}')
                    klass: Device = getattr(deviceImport, file.stem)
                    self.DeviceManager.registerDeviceType(
                        self.name, klass.getDeviceTypeDefinition())
                except Exception as e:
                    self.logError(
                        f"Failed retrieving device type definition for device **{file.stem}** {e}"
                    )

    def getUtterancesByIntent(self,
                              intent: Union[Intent, tuple, str],
                              forceLowerCase: bool = True,
                              cleanSlots: bool = False) -> list:
        lang = self.LanguageManager.activeLanguage
        if lang not in self._intentsDefinitions:
            return list()

        if isinstance(intent, tuple):
            check = intent[0].action
        elif isinstance(intent, Intent):
            check = intent.action
        else:
            check = str(intent).split('/')[-1].split(':')[-1]

        if check not in self._intentsDefinitions[lang]:
            return list()

        if not cleanSlots:
            return list(self._intentsDefinitions[lang][check])

        return [
            re.sub(self._utteranceSlotCleaner, '\\1',
                   utterance.lower() if forceLowerCase else utterance)
            for utterance in self._intentsDefinitions[lang][check]
        ]

    def supportedIntentsWithUtterances(self) -> dict:
        return {
            str(intent): self.getUtterancesByIntent(intent, True, True)
            for intent in self._supportedIntents
        }

    @property
    def widgets(self) -> list:
        return self._widgets

    @property
    def deviceTypes(self) -> list:
        return self._deviceTypes

    @property
    def active(self) -> bool:
        return self._active

    @active.setter
    def active(self, value: bool):
        self._active = value

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str):
        self._name = value

    @property
    def author(self) -> str:
        return self._author

    @author.setter
    def author(self, value: str):
        self._author = value

    @property
    def description(self) -> str:
        return self._description

    @description.setter
    def description(self, value: str):
        self._description = value

    @property
    def version(self) -> str:
        return self._version

    @version.setter
    def version(self, value: str):
        self._version = value

    @property
    def updateAvailable(self) -> bool:
        return self._updateAvailable

    @updateAvailable.setter
    def updateAvailable(self, value: bool):
        self._updateAvailable = value

    @property
    def required(self) -> bool:
        return self._required

    @required.setter
    def required(self, value: bool):
        self._required = value

    @property
    def supportedIntents(self) -> dict:
        return self._supportedIntents

    @supportedIntents.setter
    def supportedIntents(self, value: list):
        self._supportedIntents = value

    @property
    def delayed(self) -> bool:
        return self._delayed

    @delayed.setter
    def delayed(self, value: bool):
        self._delayed = value

    @property
    def scenarioNodeName(self) -> str:
        return self._scenarioPackageName

    @property
    def scenarioNodeVersion(self) -> Version:
        return self._scenarioPackageVersion

    @property
    def icon(self) -> str:
        return self._icon

    @property
    def installFile(self) -> Path:
        return self._installFile

    @property
    def installer(self) -> Dict:
        return self._installer

    @property
    def skillPath(self) -> Path:
        return self._skillPath

    @property
    def instructions(self) -> str:
        return self._instructions

    def hasScenarioNodes(self) -> bool:
        return self._scenarioPackageName != ''

    def subscribeIntents(self):
        self.MqttManager.subscribeSkillIntents(self._supportedIntents)

    def unsubscribeIntents(self):
        self.MqttManager.unsubscribeSkillIntents(self._supportedIntents)

    def notifyDevice(self, topic: str, deviceUid: str = ''):
        self.MqttManager.publish(topic=topic, payload={'uid': deviceUid})

    def authenticateIntent(self, session: DialogSession):
        intent = self._supportedIntents[session.message.topic]
        # Return if intent is for auth users only but the user is unknown
        if session.user == constants.UNKNOWN_USER:
            self.endDialog(sessionId=session.sessionId,
                           text=self.TalkManager.randomTalk(talk='unknownUser',
                                                            skill='system'))
            raise AccessLevelTooLow()
        # Return if intent is for auth users only and the user doesn't have the access level for it
        if not self.UserManager.hasAccessLevel(session.user, intent.authLevel):
            self.endDialog(sessionId=session.sessionId,
                           text=self.TalkManager.randomTalk(talk='noAccess',
                                                            skill='system'))
            raise AccessLevelTooLow()

    @staticmethod
    def intentNameMoreSpecific(intentName: str, oldIntentName: str) -> bool:
        cleanedIntentName = intentName.rstrip('#').split('+')[0]
        cleanedOldIntentName = oldIntentName.rstrip('#').split('+')[0]
        return cleanedIntentName > cleanedOldIntentName

    def filterIntent(self, session: DialogSession) -> Optional[Intent]:
        # Return if the skill isn't active
        if not self.active:
            return None

        # search for intent that has a matching mqtt topic
        matchingIntent = None
        oldIntentName = None
        for intentName, intent in self._supportedIntents.items():
            if MQTTClient.topic_matches_sub(
                    intentName, session.message.topic) and (
                        not matchingIntent or self.intentNameMoreSpecific(
                            intentName, oldIntentName)):
                matchingIntent = intent
                oldIntentName = intentName

        return matchingIntent

    def onMessageDispatch(self, session: DialogSession) -> bool:
        intent = self.filterIntent(session)
        if not intent:
            return False

        if intent.authLevel != AccessLevel.ZERO:
            try:
                self.authenticateIntent(session)
            except AccessLevelTooLow:
                raise

        function = intent.getMapping(session) or self.onMessage
        ret = function(session=session)
        return True if ret is None or ret == True else False

    def getResource(self, resourcePathFile: str = '') -> Path:
        return self.skillPath / resourcePathFile

    def _initDB(self) -> bool:
        if self._databaseSchema:
            return self.DatabaseManager.initDB(schema=self._databaseSchema,
                                               callerName=self.name)
        return True

    def onStart(self):
        self.logInfo(f'Starting')
        self._active = True

        self._initDB()
        self.SkillManager.configureSkillIntents(self._name, True)
        self.LanguageManager.loadSkillStrings(self.name)
        self.TalkManager.loadSkillTalks(self.name)

        self.loadDeviceTypes()
        self.loadWidgets()
        self.loadScenarioNodes()

        self._failedStarting = False
        self.logInfo(f'![green](Started!)')

    def onStop(self):
        self._active = False
        self.SkillManager.configureSkillIntents(self._name, False)
        self.logInfo(f'![green](Stopped)')

    def onBooted(self) -> bool:
        if self.delayed:
            self.logInfo('Delayed start')
            self.ThreadManager.doLater(interval=5, func=self.onStart)

        return True

    def onSkillStopped(self, skill):
        if skill == self._name:
            self.onStop()

    def onSkillInstalled(self, **kwargs):
        self.onSkillUpdated(**kwargs)

    def onSkillUpdated(self, skill: str):
        if skill != self.name:
            return

        self._updateAvailable = False
        self.MqttManager.subscribeSkillIntents(self._supportedIntents)

    def onSkillDeleted(self, skill: str):
        if skill != self.name or not self._databaseSchema:
            return

        for tableName in self._databaseSchema:
            self.DatabaseManager.dropTable(tableName=tableName,
                                           callerName=self.name)

    # HELPERS
    def getConfig(self, key: str) -> Any:
        return self.ConfigManager.getSkillConfigByName(skillName=self.name,
                                                       configName=key)

    def getSkillConfigs(self) -> dict:
        ret = copy(self.ConfigManager.getSkillConfigs(self.name))
        ret.pop('active', None)
        return ret

    def getSkillConfigsTemplate(self) -> dict:
        return self.ConfigManager.getSkillConfigsTemplate(self.name)

    def updateConfig(self, key: str, value: Any):
        self.ConfigManager.updateSkillConfigurationFile(skillName=self.name,
                                                        key=key,
                                                        value=value)

    def getAliceConfig(self, key: str) -> Any:
        return self.ConfigManager.getAliceConfigByName(configName=key)

    def updateAliceConfig(self, key: str, value: Any):
        self.ConfigManager.updateAliceConfiguration(key=key, value=value)

    def activeLanguage(self) -> str:
        return self.LanguageManager.activeLanguage

    def defaultLanguage(self) -> str:
        return self.LanguageManager.defaultLanguage

    def databaseFetch(self,
                      tableName: str,
                      query: str,
                      values: dict = None) -> List:
        return self.DatabaseManager.fetch(tableName=tableName,
                                          query=query,
                                          values=values,
                                          callerName=self.name)

    def databaseInsert(self,
                       tableName: str,
                       query: str = None,
                       values: dict = None) -> int:
        return self.DatabaseManager.insert(tableName=tableName,
                                           query=query,
                                           values=values,
                                           callerName=self.name)

    def randomTalk(self,
                   text: str,
                   replace: Union[str, List] = None,
                   skill: str = None) -> str:
        if not isinstance(replace, list):
            replace = [replace]

        talk = self.TalkManager.randomTalk(talk=text, skill=skill or self.name)

        if replace:
            talk = talk.format(*replace)
        return talk

    def getSkillInstance(self, skillName: str) -> AliceSkill:
        return self.SkillManager.getSkillInstance(skillName=skillName)

    def say(self,
            text: str,
            deviceUid: str = None,
            customData: dict = None,
            canBeEnqueued: bool = True):
        self.MqttManager.say(text=text,
                             deviceUid=deviceUid,
                             customData=customData,
                             canBeEnqueued=canBeEnqueued)

    def ask(self,
            text: str,
            deviceUid: str = None,
            intentFilter: list = None,
            customData: dict = None,
            canBeEnqueued: bool = True,
            currentDialogState: str = '',
            probabilityThreshold: float = None):
        if currentDialogState:
            currentDialogState = f'{self.name}:{currentDialogState}'
        self.MqttManager.ask(text=text,
                             deviceUid=deviceUid,
                             intentFilter=intentFilter,
                             customData=customData,
                             canBeEnqueued=canBeEnqueued,
                             currentDialogState=currentDialogState,
                             probabilityThreshold=probabilityThreshold)

    def continueDialog(self,
                       sessionId: str,
                       text: str,
                       customData: dict = None,
                       intentFilter: list = None,
                       slot: str = '',
                       currentDialogState: str = '',
                       probabilityThreshold: float = None):
        if currentDialogState:
            currentDialogState = f'{self.name}:{currentDialogState}'
        self.MqttManager.continueDialog(
            sessionId=sessionId,
            text=text,
            customData=customData,
            intentFilter=intentFilter,
            slot=slot,
            currentDialogState=currentDialogState,
            probabilityThreshold=probabilityThreshold)

    def endDialog(self,
                  sessionId: str = '',
                  text: str = '',
                  deviceUid: str = ''):
        self.MqttManager.endDialog(sessionId=sessionId,
                                   text=text,
                                   deviceUid=deviceUid)

    def endSession(self, sessionId):
        self.MqttManager.endSession(sessionId=sessionId)

    def playSound(self,
                  soundFilename: str,
                  location: Path = None,
                  sessionId: str = '',
                  deviceUid: Union[str, List[Union[str, Device]]] = None):
        """
		Sends audio chunks from the audio file over Mqtt. Note that instead of using a random "requestId"
		at the end of the topic, we	use the session id if available.
		:param soundFilename:
		:param location:
		:param sessionId:
		:param deviceUid:
		:return:
		"""
        session = self.DialogManager.getSession(sessionId=sessionId)
        if session:
            session.lastWasSoundPlayOnly = True

        self.MqttManager.playSound(soundFilename=soundFilename,
                                   location=location,
                                   sessionId=sessionId,
                                   deviceUid=deviceUid)

    def publish(self,
                topic: str,
                payload: dict = None,
                stringPayload: str = None,
                qos: int = 0,
                retain: bool = False):
        self.MqttManager.publish(topic=topic,
                                 payload=payload,
                                 stringPayload=stringPayload,
                                 qos=qos,
                                 retain=retain)

    def __repr__(self) -> str:
        return json.dumps(self.toDict())

    def __str__(self) -> str:
        return self.__repr__()

    def toDict(self) -> dict:
        return {
            'name': self._name,
            'author': self._author,
            'version': self._version,
            'modified': self.modified,
            'updateAvailable': self._updateAvailable,
            'active': self._active,
            'delayed': self._delayed,
            'required': self._required,
            'databaseSchema': self._databaseSchema,
            'icon': self._icon,
            'instructions': self._instructions,
            'settings': self.ConfigManager.getSkillConfigs(self.name),
            'settingsTemplate': self.getSkillConfigsTemplate(),
            'description': self._description,
            'category': self._category,
            'aliceMinVersion': str(self._aliceMinVersion),
            'maintainers': self._maintainers,
            'intents': self.supportedIntentsWithUtterances()
        }
class FailedAliceSkill(ProjectAliceObject):
    def __init__(self, installer: dict):
        self._installer = installer
        self._updateAvailable = False
        self._name = installer['name']
        self._icon = self._installer.get('icon', 'fas fa-biohazard')
        self._aliceMinVersion = Version.fromString(
            self._installer.get('aliceMinVersion', '1.0.0-b4'))
        self._maintainers = self._installer.get('maintainers', list())
        self._description = self._installer.get('desc', '')
        self._category = self._installer.get('category', constants.UNKNOWN)
        self._conditions = self._installer.get('conditions', dict())
        self._skillPath = Path('skills') / self._name
        self._repository = Repository(directory=self._skillPath,
                                      init=True,
                                      raiseIfExisting=False)
        super().__init__()

    @staticmethod
    def onMessageDispatch(_session: DialogSession) -> bool:
        return False

    def onStart(self):
        pass  # Is always handled by the sibling

    def onStop(self):
        pass  # Is always handled by the sibling

    def onBooted(self) -> bool:
        return True

    def onSkillInstalled(self, **kwargs):
        self._updateAvailable = False

    def onSkillUpdated(self, **kwargs):
        self._updateAvailable = False

    def __repr__(self) -> str:
        return json.dumps(self.toDict())

    def __str__(self) -> str:
        return self.__repr__()

    @property
    def modified(self) -> bool:
        return self._repository.isDirty()

    @property
    def repository(self) -> Repository:
        return self._repository

    @property
    def skillPath(self) -> Path:
        return self._skillPath

    def getResource(self, resourcePathFile: str = '') -> Path:
        return self.skillPath / resourcePathFile

    def toDict(self) -> dict:
        return {
            'name': self._name,
            'author': self._installer['author'],
            'version': self._installer['version'],
            'modified': self.modified,
            'updateAvailable': self._updateAvailable,
            'maintainers': self._maintainers,
            'settings': self.ConfigManager.getSkillConfigs(self._name),
            'icon': self._icon,
            'description': self._description,
            'category': self._category,
            'aliceMinVersion': str(self._aliceMinVersion)
        }