<commit_before># Copyright 2017 Mycroft AI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
    SkillSettings is a simple extension of a Python dict which enables
    simplified storage of settings.  Values stored into the dict will
    automatically persist locally.  Additionally, it can interact with
    a backend system to provide a GUI interface for some or all of the
    settings.

    The GUI for the setting is described by a file in the skill's root
    directory called settingsmeta.json.  The "name" is associates the
    user-interface field with the setting name in the dictionary.  For
    example, you might have a setting['username'].  In the settingsmeta
    you can describe the interface you want to edit that value with:
        ...
        "fields": [
               {
                   "name": "username",
                   "type": "email",
                   "label": "Email address to associate",
                   "placeholder": "<EMAIL>",
                   "value": ""
               }]
        ...
    When the user changes the setting via the web UI, it will be sent
    down to all the devices and automatically placed into the
    settings['username'].  Any local changes made to the value (e.g.
    via a verbal interaction) will also be synched to the server to show
    on the web interface.

    NOTE: As it stands today, this functions seamlessly with a single
    device.  With multiple devices there are a few hitches that are being
    worked out.  The first device where a skill is installed creates the
    setting and values are sent down to any other devices that install the
    same skill.  However only the original device can make changes locally
    for synching to the web.  This limitation is temporary and will be
    removed soon.


    Usage Example:
        from mycroft.skill.settings import SkillSettings

        s = SkillSettings('./settings.json', 'ImportantSettings')
        s['meaning of life'] = 42
        s['flower pot sayings'] = 'Not again...'
        s.store()  # This happens automagically in a MycroftSkill
"""

import json
import hashlib
from threading import Timer
from os.path import isfile, join, expanduser

from mycroft.api import DeviceApi, is_paired
from mycroft.util.log import LOG
from mycroft.configuration import ConfigurationManager


class SkillSettings(dict):
    """ Dictionary that can easily be saved to a file, serialized as json. It
        also syncs to the backend for skill settings

    Args:
        directory (str): Path to storage directory
        name (str):      user readable name associated with the settings
    """

    def __init__(self, directory, name):
        super(SkillSettings, self).__init__()
        # when skills try to instantiate settings
        # in __init__, it can erase the settings saved
        # on disk (settings.json). So this prevents that
        # This is set to true in core.py after skill init
        self.allow_overwrite = False

        self.api = DeviceApi()
        self.config = ConfigurationManager.get()
        self.name = name
        # set file paths
        self._settings_path = join(directory, 'settings.json')
        self._meta_path = join(directory, 'settingsmeta.json')
        self.is_alive = True
        self.loaded_hash = hash(json.dumps(self, sort_keys=True))
        self._complete_intialization = False
        self._device_identity = None
        self._api_path = None
        self._user_identity = None
        self.changed_callback = None
        self._poll_timer = None
        self._is_alive = True

        # if settingsmeta exist
        if isfile(self._meta_path):
            self._poll_skill_settings()

    def run_poll(self, _=None):
        """Immediately poll the web for new skill settings"""
        if self._poll_timer:
            self._poll_timer.cancel()
            self._poll_skill_settings()

    def stop_polling(self):
        self._is_alive = False
        if self._poll_timer:
            self._poll_timer.cancel()

    def set_changed_callback(self, callback):
        """ Set callback to perform when server settings have changed.

        Args:
            callback: function/method to call when settings have changed
        """
        self.changed_callback = callback

    # TODO: break this up into two classes
    def initialize_remote_settings(self):
        """ initializes the remote settings to the server """
        # if settingsmeta.json exists (and is valid)
        # this block of code is a control flow for
        # different scenarios that may arises with settingsmeta
        self.load_skill_settings_from_file()  # loads existing settings.json
        settings_meta = self._load_settings_meta()
        if not settings_meta:
            return

        if not is_paired():
            return

        self._device_identity = self.api.identity.uuid
        self._api_path = "/" + self._device_identity + "/skill"
        self._user_identity = self.api.get()['user']['uuid']

        hashed_meta = self._get_meta_hash(settings_meta)
        skill_settings = self._request_other_settings(hashed_meta)
        # if hash is new then there is a diff version of settingsmeta
        if self._is_new_hash(hashed_meta):
            # first look at all other devices on user account to see
            # if the settings exist. if it does then sync with device
            if skill_settings:
                # not_owner flags that this settings is loaded from
                # another device. If a skill settings doesn't have
                # not_owner, then the skill is created from that device
                self['not_owner'] = True
                self.save_skill_settings(skill_settings)
            else:  # upload skill settings if
                uuid = self._load_uuid()
                if uuid is not None:
                    self._delete_metadata(uuid)
                self._upload_meta(settings_meta, hashed_meta)
        else:  # hash is not new
            if skill_settings is not None:
                self['not_owner'] = True
                self.save_skill_settings(skill_settings)
            else:
                settings = self._request_my_settings(hashed_meta)
                if settings is None:
                    # metadata got deleted from Home, send up
                    self._upload_meta(settings_meta, hashed_meta)
                else:
                    self.save_skill_settings(settings)
        self._complete_intialization = True

    @property
    def _is_stored(self):
        return hash(json.dumps(self, sort_keys=True)) == self.loaded_hash

    def __getitem__(self, key):
        """ Get key """
        return super(SkillSettings, self).__getitem__(key)

    def __setitem__(self, key, value):
        """ Add/Update key. """
        if self.allow_overwrite or key not in self:
            return super(SkillSettings, self).__setitem__(key, value)

    def _load_settings_meta(self):
        """ Loads settings metadata from skills path. """
        if isfile(self._meta_path):
            try:
                with open(self._meta_path) as f:
                    data = json.load(f)
                return data
            except Exception as e:
                LOG.error("Failed to load setting file: "+self._meta_path)
                LOG.error(repr(e))
                return None
        else:
            return None

    def _send_settings_meta(self, settings_meta):
        """ Send settingsmeta.json to the server.

        Args:
            settings_meta (dict): dictionary of the current settings meta
        Returns:
            dict: uuid, a unique id for the setting meta data
        """<commit_msg>Load the settingsmeta as utf-8<commit_after># Copyright 2017 Mycroft AI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
    SkillSettings is a simple extension of a Python dict which enables
    simplified storage of settings.  Values stored into the dict will
    automatically persist locally.  Additionally, it can interact with
    a backend system to provide a GUI interface for some or all of the
    settings.

    The GUI for the setting is described by a file in the skill's root
    directory called settingsmeta.json.  The "name" is associates the
    user-interface field with the setting name in the dictionary.  For
    example, you might have a setting['username'].  In the settingsmeta
    you can describe the interface you want to edit that value with:
        ...
        "fields": [
               {
                   "name": "username",
                   "type": "email",
                   "label": "Email address to associate",
                   "placeholder": "<EMAIL>",
                   "value": ""
               }]
        ...
    When the user changes the setting via the web UI, it will be sent
    down to all the devices and automatically placed into the
    settings['username'].  Any local changes made to the value (e.g.
    via a verbal interaction) will also be synched to the server to show
    on the web interface.

    NOTE: As it stands today, this functions seamlessly with a single
    device.  With multiple devices there are a few hitches that are being
    worked out.  The first device where a skill is installed creates the
    setting and values are sent down to any other devices that install the
    same skill.  However only the original device can make changes locally
    for synching to the web.  This limitation is temporary and will be
    removed soon.


    Usage Example:
        from mycroft.skill.settings import SkillSettings

        s = SkillSettings('./settings.json', 'ImportantSettings')
        s['meaning of life'] = 42
        s['flower pot sayings'] = 'Not again...'
        s.store()  # This happens automagically in a MycroftSkill
"""

import json
import hashlib
from threading import Timer
from os.path import isfile, join, expanduser

from mycroft.api import DeviceApi, is_paired
from mycroft.util.log import LOG
from mycroft.configuration import ConfigurationManager


class SkillSettings(dict):
    """ Dictionary that can easily be saved to a file, serialized as json. It
        also syncs to the backend for skill settings

    Args:
        directory (str): Path to storage directory
        name (str):      user readable name associated with the settings
    """

    def __init__(self, directory, name):
        super(SkillSettings, self).__init__()
        # when skills try to instantiate settings
        # in __init__, it can erase the settings saved
        # on disk (settings.json). So this prevents that
        # This is set to true in core.py after skill init
        self.allow_overwrite = False

        self.api = DeviceApi()
        self.config = ConfigurationManager.get()
        self.name = name
        # set file paths
        self._settings_path = join(directory, 'settings.json')
        self._meta_path = join(directory, 'settingsmeta.json')
        self.is_alive = True
        self.loaded_hash = hash(json.dumps(self, sort_keys=True))
        self._complete_intialization = False
        self._device_identity = None
        self._api_path = None
        self._user_identity = None
        self.changed_callback = None
        self._poll_timer = None
        self._is_alive = True

        # if settingsmeta exist
        if isfile(self._meta_path):
            self._poll_skill_settings()

    def run_poll(self, _=None):
        """Immediately poll the web for new skill settings"""
        if self._poll_timer:
            self._poll_timer.cancel()
            self._poll_skill_settings()

    def stop_polling(self):
        self._is_alive = False
        if self._poll_timer:
            self._poll_timer.cancel()

    def set_changed_callback(self, callback):
        """ Set callback to perform when server settings have changed.

        Args:
            callback: function/method to call when settings have changed
        """
        self.changed_callback = callback

    # TODO: break this up into two classes
    def initialize_remote_settings(self):
        """ initializes the remote settings to the server """
        # if settingsmeta.json exists (and is valid)
        # this block of code is a control flow for
        # different scenarios that may arises with settingsmeta
        self.load_skill_settings_from_file()  # loads existing settings.json
        settings_meta = self._load_settings_meta()
        if not settings_meta:
            return

        if not is_paired():
            return

        self._device_identity = self.api.identity.uuid
        self._api_path = "/" + self._device_identity + "/skill"
        self._user_identity = self.api.get()['user']['uuid']

        hashed_meta = self._get_meta_hash(settings_meta)
        skill_settings = self._request_other_settings(hashed_meta)
        # if hash is new then there is a diff version of settingsmeta
        if self._is_new_hash(hashed_meta):
            # first look at all other devices on user account to see
            # if the settings exist. if it does then sync with device
            if skill_settings:
                # not_owner flags that this settings is loaded from
                # another device. If a skill settings doesn't have
                # not_owner, then the skill is created from that device
                self['not_owner'] = True
                self.save_skill_settings(skill_settings)
            else:  # upload skill settings if
                uuid = self._load_uuid()
                if uuid is not None:
                    self._delete_metadata(uuid)
                self._upload_meta(settings_meta, hashed_meta)
        else:  # hash is not new
            if skill_settings is not None:
                self['not_owner'] = True
                self.save_skill_settings(skill_settings)
            else:
                settings = self._request_my_settings(hashed_meta)
                if settings is None:
                    # metadata got deleted from Home, send up
                    self._upload_meta(settings_meta, hashed_meta)
                else:
                    self.save_skill_settings(settings)
        self._complete_intialization = True

    @property
    def _is_stored(self):
        return hash(json.dumps(self, sort_keys=True)) == self.loaded_hash

    def __getitem__(self, key):
        """ Get key """
        return super(SkillSettings, self).__getitem__(key)

    def __setitem__(self, key, value):
        """ Add/Update key. """
        if self.allow_overwrite or key not in self:
            return super(SkillSettings, self).__setitem__(key, value)

    def _load_settings_meta(self):
        """ Loads settings metadata from skills path. """
        if isfile(self._meta_path):
            try:
                with open(self._meta_path, encoding='utf-8') as f:
                    data = json.load(f)
                return data
            except Exception as e:
                LOG.error("Failed to load setting file: "+self._meta_path)
                LOG.error(repr(e))
                return None
        else:
            return None

    def _send_settings_meta(self, settings_meta):
        """ Send settingsmeta.json to the server.

        Args:
            settings_meta (dict): dictionary of the current settings meta
        Returns:
            dict: uuid, a unique id for the setting meta data
        """
{'fraction_non_alphanumeric': 0.06883505217550498, 'fraction_numerical': 0.0013425276133520474, 'mean_word_length': 2.224714679260134, 'pattern_counts': {'":': 12, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4196918', 'n_tokens_mistral': 4222, 'n_tokens_neox': 4081, 'n_words': 1613}