Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSybren A. Stüvel <sybren@stuvel.eu>2016-08-07 12:37:23 +0300
committerSybren A. Stüvel <sybren@stuvel.eu>2016-08-07 12:37:23 +0300
commit84a93440fd5c5ecbe80d7bb9743c1747d0bde3eb (patch)
treef16b034a7d309526dfa24ffe8bc61fb9b9f8a5d1 /blender_id
parente2ebaa80b48550cdbb0e47f29e39e59ab57085b1 (diff)
Fix T49008: Blender-ID add-on for inclusion as OFFICIAL
Added Blender-ID add-on version 1.2.0. For more info, see the repository at https://developer.blender.org/diffusion/BIA/ To bundle a new version, run this from the Blender-ID add-on source: python3 setup.py bdist bundle --path /path/to/blender/release/scripts/addons
Diffstat (limited to 'blender_id')
-rw-r--r--blender_id/README.md109
-rw-r--r--blender_id/__init__.py277
-rw-r--r--blender_id/communication.py250
-rw-r--r--blender_id/profiles.py216
4 files changed, 852 insertions, 0 deletions
diff --git a/blender_id/README.md b/blender_id/README.md
new file mode 100644
index 00000000..936e6e3e
--- /dev/null
+++ b/blender_id/README.md
@@ -0,0 +1,109 @@
+Blender ID addon
+================
+
+This addon allows you to authenticate your Blender with your
+[Blender ID](https://www.blender.org/id/) account. This authentication
+can then be used by other addons, such as the
+[Blender Cloud addon](https://developer.blender.org/diffusion/BCA/)
+
+Blender compatibility
+---------------------
+
+Blender ID add-on version 1.2.0 removed some workarounds necessary for
+Blender 2.77a. As such, versions 1.1.x are the last versions compatible with
+Blender 2.77a, and 1.2.0 and newer require at least Blender 2.78.
+
+Building & Bundling
+-------------------
+
+* To build the addon, run `python3 setup.py bdist`
+* To bundle the addon with Blender, run `python3 setup.py bdist bundle --path
+ ../blender-git/blender/release/scripts/addons`.
+* If you don't want to bundle, you can install the addon from Blender
+ (User Preferences → Addons → Install from file...) by pointing it to
+ `dist/blender_id*.addon.zip`.
+
+
+Using the addon
+---------------
+
+* Install the addon as described above.
+* Enable the addon in User Preferences → Addons → System.
+* Sign up for an account at the
+ [Blender ID site](https://www.blender.org/id/) if you don't have an
+ account yet.
+* Log in with your Blender ID and password. You only have to do this
+ once.
+
+Your password is never saved on your machine, just an access token. It
+is stored next to your Blender configuration files, in
+
+* Linux and similar: `$HOME/.config/blender/{version}/config/blender_id`
+* MacOS: `$HOME/Library/Application Support/Blender/{version}/config/blender_id`
+* Windows: `%APPDATA%\Blender Foundation\Blender\{version}\config\blender_id`
+
+where `{version}` is the Blender version.
+
+
+Using the addon from another addon
+----------------------------------
+
+The following functions can be used from other addons to use the Blender
+ID functionality:
+
+**blender_id.get_active_profile()** returns the `BlenderIdProfile` that
+represents the currently logged in user, or `None` when the user isn't
+logged in:
+
+ lang=python
+ class BlenderIdProfile:
+ user_id = '41234'
+ username = 'username@example.com'
+ token = '41344124-auth-token-434134'
+
+
+**blender_id.get_active_user_id()** returns the user ID of the logged
+in user, or `''` when the user isn't logged in.
+
+**blender_id.is_logged_in()** returns `True` if the user is logged
+in, and `False` otherwise.
+
+
+Here is an example of a simple addon that shows your username in its
+preferences panel:
+
+ lang=python,name=demo_blender_id_addon.py
+ # Extend this with your info
+ bl_info = {
+ 'name': 'Demo addon using Blender ID',
+ 'location': 'Add-on preferences',
+ 'category': 'System',
+ 'support': 'TESTING',
+ }
+
+ import bpy
+
+
+ class DemoPreferences(bpy.types.AddonPreferences):
+ bl_idname = __name__
+
+ def draw(self, context):
+ import blender_id
+
+ profile = blender_id.get_active_profile()
+ if profile:
+ self.layout.label('You are logged in as %s' % profile.username)
+ else:
+ self.layout.label('You are not logged in on Blender ID')
+
+
+ def register():
+ bpy.utils.register_module(__name__)
+
+
+ def unregister():
+ bpy.utils.unregister_module(__name__)
+
+
+ if __name__ == '__main__':
+ register()
diff --git a/blender_id/__init__.py b/blender_id/__init__.py
new file mode 100644
index 00000000..ae24af86
--- /dev/null
+++ b/blender_id/__init__.py
@@ -0,0 +1,277 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+bl_info = {
+ 'name': 'Blender ID authentication',
+ 'author': 'Francesco Siddi, Inês Almeida and Sybren A. Stüvel',
+ 'version': (1, 2, 0),
+ 'blender': (2, 77, 0),
+ 'location': 'Add-on preferences',
+ 'description':
+ 'Stores your Blender ID credentials for usage with other add-ons',
+ 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.6/Py/'
+ 'Scripts/System/BlenderID',
+ 'category': 'System',
+ 'support': 'OFFICIAL',
+}
+
+import bpy
+from bpy.types import AddonPreferences, Operator, PropertyGroup
+from bpy.props import PointerProperty, StringProperty
+
+if 'communication' in locals():
+ import importlib
+
+ # noinspection PyUnboundLocalVariable
+ communication = importlib.reload(communication)
+ # noinspection PyUnboundLocalVariable
+ profiles = importlib.reload(profiles)
+else:
+ from . import communication, profiles
+BlenderIdProfile = profiles.BlenderIdProfile
+BlenderIdCommError = communication.BlenderIdCommError
+
+__all__ = ('get_active_profile', 'get_active_user_id', 'is_logged_in', 'create_subclient_token',
+ 'BlenderIdProfile', 'BlenderIdCommError')
+
+
+# Public API functions
+def get_active_user_id() -> str:
+ """Get the id of the currently active profile. If there is no
+ active profile on the file, this function will return an empty string.
+ """
+
+ return BlenderIdProfile.user_id
+
+
+def get_active_profile() -> BlenderIdProfile:
+ """Returns the active Blender ID profile. If there is no
+ active profile on the file, this function will return None.
+
+ :rtype: BlenderIdProfile
+ """
+
+ if not BlenderIdProfile.user_id:
+ return None
+
+ return BlenderIdProfile
+
+
+def is_logged_in() -> bool:
+ """Returns whether the user is logged in on Blender ID or not."""
+
+ return bool(BlenderIdProfile.user_id)
+
+
+def create_subclient_token(subclient_id: str, webservice_endpoint: str) -> dict:
+ """Lets the Blender ID server create a subclient token.
+
+ :param subclient_id: the ID of the subclient
+ :param webservice_endpoint: the URL of the endpoint of the webservice
+ that belongs to this subclient.
+ :returns: the token along with its expiry timestamp, in a {'scst': 'token',
+ 'expiry': datetime.datetime} dict.
+ :raises: blender_id.communication.BlenderIdCommError when the
+ token cannot be created.
+ """
+
+ # Communication between us and Blender ID.
+ profile = get_active_profile()
+ scst_info = communication.subclient_create_token(profile.token, subclient_id)
+ subclient_token = scst_info['token']
+
+ # Send the token to the webservice.
+ user_id = communication.send_token_to_subclient(webservice_endpoint, profile.user_id,
+ subclient_token, subclient_id)
+
+ # Now that everything is okay we can store the token locally.
+ profile.subclients[subclient_id] = {'subclient_user_id': user_id, 'token': subclient_token}
+ profile.save_json()
+
+ return scst_info
+
+
+def get_subclient_user_id(subclient_id: str) -> str:
+ """Returns the user ID at the given subclient.
+
+ Requires that the user has been authenticated at the subclient using
+ a call to create_subclient_token(...)
+
+ :returns: the subclient-local user ID, or None if not logged in.
+ """
+
+ if not BlenderIdProfile.user_id:
+ return None
+
+ return BlenderIdProfile.subclients[subclient_id]['subclient_user_id']
+
+
+class BlenderIdPreferences(AddonPreferences):
+ bl_idname = __name__
+
+ error_message = StringProperty(
+ name='Error Message',
+ default='',
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+ ok_message = StringProperty(
+ name='Message',
+ default='',
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+ blender_id_username = StringProperty(
+ name='E-mail address',
+ default='',
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+ blender_id_password = StringProperty(
+ name='Password',
+ default='',
+ options={'HIDDEN', 'SKIP_SAVE'},
+ subtype='PASSWORD'
+ )
+
+ def reset_messages(self):
+ self.ok_message = ''
+ self.error_message = ''
+
+ def draw(self, context):
+ layout = self.layout
+
+ if self.error_message:
+ sub = layout.row()
+ sub.alert = True # labels don't display in red :(
+ sub.label(self.error_message, icon='ERROR')
+ if self.ok_message:
+ sub = layout.row()
+ sub.label(self.ok_message, icon='FILE_TICK')
+
+ active_profile = get_active_profile()
+ if active_profile:
+ text = 'You are logged in as {0}'.format(active_profile.username)
+ layout.label(text=text, icon='WORLD_DATA')
+ row = layout.row()
+ row.operator('blender_id.logout')
+ if bpy.app.debug:
+ row.operator('blender_id.validate')
+ else:
+ layout.prop(self, 'blender_id_username')
+ layout.prop(self, 'blender_id_password')
+
+ layout.operator('blender_id.login')
+
+
+class BlenderIdMixin:
+ @staticmethod
+ def addon_prefs(context):
+ preferences = context.user_preferences.addons[__name__].preferences
+ preferences.reset_messages()
+ return preferences
+
+
+class BlenderIdLogin(BlenderIdMixin, Operator):
+ bl_idname = 'blender_id.login'
+ bl_label = 'Login'
+
+ def execute(self, context):
+ import random
+ import string
+
+ addon_prefs = self.addon_prefs(context)
+
+ resp = communication.blender_id_server_authenticate(
+ username=addon_prefs.blender_id_username,
+ password=addon_prefs.blender_id_password
+ )
+
+ if resp['status'] == 'success':
+ # Prevent saving the password in user preferences. Overwrite the password with a
+ # random string, as just setting to '' might only replace the first byte with 0.
+ pwlen = len(addon_prefs.blender_id_password)
+ rnd = ''.join(random.choice(string.ascii_uppercase + string.digits)
+ for _ in range(pwlen + 16))
+ addon_prefs.blender_id_password = rnd
+ addon_prefs.blender_id_password = ''
+
+ profiles.save_as_active_profile(
+ resp['user_id'],
+ resp['token'],
+ addon_prefs.blender_id_username,
+ {}
+ )
+ addon_prefs.ok_message = 'Logged in'
+ else:
+ addon_prefs.error_message = resp['error_message']
+ if BlenderIdProfile.user_id:
+ profiles.logout(BlenderIdProfile.user_id)
+
+ BlenderIdProfile.read_json()
+
+ return {'FINISHED'}
+
+
+class BlenderIdValidate(BlenderIdMixin, Operator):
+ bl_idname = 'blender_id.validate'
+ bl_label = 'Validate'
+
+ def execute(self, context):
+ addon_prefs = self.addon_prefs(context)
+
+ resp = communication.blender_id_server_validate(token=BlenderIdProfile.token)
+ if resp is None:
+ addon_prefs.ok_message = 'Authentication token is valid.'
+ else:
+ addon_prefs.error_message = '%s; you probably want to log out and log in again.' % resp
+
+ BlenderIdProfile.read_json()
+
+ return {'FINISHED'}
+
+
+class BlenderIdLogout(BlenderIdMixin, Operator):
+ bl_idname = 'blender_id.logout'
+ bl_label = 'Logout'
+
+ def execute(self, context):
+ communication.blender_id_server_logout(BlenderIdProfile.user_id,
+ BlenderIdProfile.token)
+
+ profiles.logout(BlenderIdProfile.user_id)
+ BlenderIdProfile.read_json()
+
+ return {'FINISHED'}
+
+
+def register():
+ profiles.register()
+ BlenderIdProfile.read_json()
+
+ bpy.utils.register_module(__name__)
+
+ preferences = bpy.context.user_preferences.addons[__name__].preferences
+ preferences.reset_messages()
+
+
+def unregister():
+ bpy.utils.unregister_module(__name__)
+
+
+if __name__ == '__main__':
+ register()
diff --git a/blender_id/communication.py b/blender_id/communication.py
new file mode 100644
index 00000000..ee71c553
--- /dev/null
+++ b/blender_id/communication.py
@@ -0,0 +1,250 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+import functools
+
+
+class BlenderIdCommError(RuntimeError):
+ """Raised when there was an error communicating with Blender ID"""
+
+
+@functools.lru_cache(maxsize=None)
+def host_label():
+ import socket
+
+ return 'Blender running on %r' % socket.gethostname()
+
+
+@functools.lru_cache(maxsize=None)
+def blender_id_endpoint(endpoint_path=None):
+ """Gets the endpoint for the authentication API. If the BLENDER_ID_ENDPOINT env variable
+ is defined, it's possible to override the (default) production address.
+ """
+ import os
+ import urllib.parse
+
+ base_url = os.environ.get('BLENDER_ID_ENDPOINT', 'https://www.blender.org/id/')
+
+ # urljoin() is None-safe for the 2nd parameter.
+ return urllib.parse.urljoin(base_url, endpoint_path)
+
+
+def blender_id_server_authenticate(username, password):
+ """Authenticate the user with the server with a single transaction
+ containing username and password (must happen via HTTPS).
+
+ If the transaction is successful, status will be 'successful' and we
+ return the user's unique blender id and a token (that will be used to
+ represent that username and password combination).
+ If there was a problem, status will be 'fail' and we return an error
+ message. Problems may be with the connection or wrong user/password.
+ """
+
+ import requests
+ import requests.exceptions
+
+ payload = dict(
+ username=username,
+ password=password,
+ host_label=host_label()
+ )
+
+ url = blender_id_endpoint('u/identify')
+ try:
+ r = requests.post(url, data=payload, verify=True)
+ except (requests.exceptions.SSLError,
+ requests.exceptions.HTTPError,
+ requests.exceptions.ConnectionError) as e:
+ print('Exception POSTing to {}: {}'.format(url, e))
+ return dict(
+ status='fail',
+ user_id=None,
+ token=None,
+ error_message=str(e)
+ )
+
+ user_id = None
+ token = None
+ error_message = None
+
+ if r.status_code == 200:
+ resp = r.json()
+ status = resp['status']
+ if status == 'success':
+ user_id = str(resp['data']['user_id'])
+ # We just use the access token for now.
+ token = resp['data']['oauth_token']['access_token']
+ elif status == 'fail':
+ error_message = 'Username and/or password is incorrect'
+ else:
+ status = 'fail'
+ error_message = format('There was a problem communicating with'
+ ' the server. Error code is: %s' % r.status_code)
+
+ return dict(
+ status=status,
+ user_id=user_id,
+ token=token,
+ error_message=error_message
+ )
+
+
+def blender_id_server_validate(token):
+ """Validate the auth token with the server.
+
+ @param token: the authentication token
+ @type token: str
+ @returns: None if the token is valid, or an error message when it's invalid.
+ """
+
+ import requests
+ import requests.exceptions
+
+ try:
+ r = requests.post(blender_id_endpoint('u/validate_token'),
+ data={'token': token}, verify=True)
+ except requests.exceptions.RequestException as e:
+ return str(e)
+
+ if r.status_code == 200:
+ return None
+
+ return 'Authentication token invalid'
+
+
+def blender_id_server_logout(user_id, token):
+ """Logs out of the Blender ID service by removing the token server-side.
+
+ @param user_id: the email address of the user.
+ @type user_id: str
+ @param token: the token to remove
+ @type token: str
+ @return: {'status': 'fail' or 'success', 'error_message': str}
+ @rtype: dict
+ """
+
+ import requests
+ import requests.exceptions
+
+ payload = dict(
+ user_id=user_id,
+ token=token
+ )
+ try:
+ r = requests.post(blender_id_endpoint('u/delete_token'),
+ data=payload, verify=True)
+ except (requests.exceptions.SSLError,
+ requests.exceptions.HTTPError,
+ requests.exceptions.ConnectionError) as e:
+ return dict(
+ status='fail',
+ error_message=format('There was a problem setting up a connection to '
+ 'the server. Error type is: %s' % type(e).__name__)
+ )
+
+ if r.status_code != 200:
+ return dict(
+ status='fail',
+ error_message=format('There was a problem communicating with'
+ ' the server. Error code is: %s' % r.status_code)
+ )
+
+ resp = r.json()
+ return dict(
+ status=resp['status'],
+ error_message=None
+ )
+
+
+def subclient_create_token(auth_token: str, subclient_id: str) -> dict:
+ """Creates a subclient-specific authentication token.
+
+ :returns: the token along with its expiry timestamp, in a {'scst': 'token',
+ 'expiry': datetime.datetime} dict.
+ """
+
+ payload = {'subclient_id': subclient_id,
+ 'host_label': host_label()}
+
+ r = make_authenticated_call('POST', 'subclients/create_token', auth_token, payload)
+ if r.status_code == 401:
+ raise BlenderIdCommError('Your Blender ID login is not valid, try logging in again.')
+
+ if r.status_code != 201:
+ raise BlenderIdCommError('Invalid response, HTTP code %i received' % r.status_code)
+
+ resp = r.json()
+ if resp['status'] != 'success':
+ raise BlenderIdCommError(resp['message'])
+
+ return resp['data']
+
+
+def make_authenticated_call(method, url, auth_token, data):
+ """Makes a HTTP call authenticated with the OAuth token."""
+
+ import requests
+ import requests.exceptions
+
+ try:
+ r = requests.request(method,
+ blender_id_endpoint(url),
+ data=data,
+ headers={'Authorization': 'Bearer %s' % auth_token},
+ verify=True)
+ except (requests.exceptions.HTTPError,
+ requests.exceptions.ConnectionError) as e:
+ raise BlenderIdCommError(str(e))
+
+ return r
+
+
+def send_token_to_subclient(webservice_endpoint: str, user_id: str,
+ subclient_token: str, subclient_id: str) -> str:
+ """Sends the subclient-specific token to the subclient.
+
+ The subclient verifies this token with BlenderID. If it's accepted, the
+ subclient ensures there is a valid user created server-side. The ID of
+ that user is returned.
+
+ :returns: the user ID at the subclient.
+ """
+
+ import requests
+ import urllib.parse
+
+ url = urllib.parse.urljoin(webservice_endpoint, 'blender_id/store_scst')
+ try:
+ r = requests.post(url,
+ data={'user_id': user_id,
+ 'subclient_id': subclient_id,
+ 'token': subclient_token},
+ verify=True)
+ r.raise_for_status()
+ except (requests.exceptions.HTTPError,
+ requests.exceptions.ConnectionError) as e:
+ raise BlenderIdCommError(str(e))
+ resp = r.json()
+
+ if resp['status'] != 'success':
+ raise BlenderIdCommError('Error sending subclient-specific token to %s, error is: %s'
+ % (webservice_endpoint, resp))
+
+ return resp['subclient_user_id']
diff --git a/blender_id/profiles.py b/blender_id/profiles.py
new file mode 100644
index 00000000..7dd6e121
--- /dev/null
+++ b/blender_id/profiles.py
@@ -0,0 +1,216 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+import os
+import bpy
+
+# Set/created upon register.
+profiles_path = ''
+profiles_file = ''
+
+
+class _BIPMeta(type):
+ """Metaclass for BlenderIdProfile."""
+
+ def __str__(self):
+ # noinspection PyUnresolvedReferences
+ return '%s(user_id=%r)' % (self.__qualname__, self.user_id)
+
+
+class BlenderIdProfile(metaclass=_BIPMeta):
+ """Current Blender ID profile.
+
+ This is always stored at class level, as there is only one current
+ profile anyway.
+ """
+
+ user_id = ''
+ username = ''
+ token = ''
+ subclients = {}
+
+ @classmethod
+ def read_json(cls):
+ """Updates the active profile information from the JSON file."""
+
+ active_profile = get_active_profile()
+ if active_profile:
+ cls.user_id = active_profile['user_id']
+ cls.username = active_profile['username']
+ cls.token = active_profile['token']
+ cls.subclients = active_profile.get('subclients', {})
+ else:
+ cls.user_id = ''
+ cls.username = ''
+ cls.token = ''
+ cls.subclients = {} # mapping from subclient-ID to user info dict.
+
+ @classmethod
+ def save_json(cls, make_active_profile=False):
+ """Updates the JSON file with the active profile information."""
+
+ jsonfile = get_profiles_data()
+ jsonfile['profiles'][cls.user_id] = {
+ 'username': cls.username,
+ 'token': cls.token,
+ 'subclients': cls.subclients,
+ }
+
+ if make_active_profile:
+ jsonfile['active_profile'] = cls.user_id
+
+ save_profiles_data(jsonfile)
+
+
+def register():
+ global profiles_path, profiles_file
+
+ profiles_path = bpy.utils.user_resource('CONFIG', 'blender_id', create=True)
+ profiles_file = os.path.join(profiles_path, 'profiles.json')
+
+
+def _create_default_file():
+ """Creates the default profile file, returning its contents."""
+ import json
+
+ profiles_default_data = {
+ 'active_profile': None,
+ 'profiles': {}
+ }
+
+ os.makedirs(profiles_path, exist_ok=True)
+
+ # Populate the file, ensuring that its permissions are restrictive enough.
+ old_umask = os.umask(0o077)
+ try:
+ with open(profiles_file, 'w', encoding='utf8') as outfile:
+ json.dump(profiles_default_data, outfile)
+ finally:
+ os.umask(old_umask)
+
+ return profiles_default_data
+
+
+def get_profiles_data():
+ """Returns the profiles.json content from a blender_id folder in the
+ Blender config directory. If the file does not exist we create one with the
+ basic data structure.
+ """
+ import json
+
+ # if the file does not exist
+ if not os.path.exists(profiles_file):
+ return _create_default_file()
+
+ # try parsing the file
+ with open(profiles_file, 'r', encoding='utf8') as f:
+ try:
+ file_data = json.load(f)
+ file_data['active_profile']
+ file_data['profiles']
+ return file_data
+ except (ValueError, # malformed json data
+ KeyError): # it doesn't have the expected content
+ print('(%s) '
+ 'Warning: profiles.json is either empty or malformed. '
+ 'The file will be reset.' % __name__)
+
+ # overwrite the file
+ return _create_default_file()
+
+
+def get_active_user_id():
+ """Get the id of the currently active profile. If there is no
+ active profile on the file, this function will return None.
+ """
+
+ return get_profiles_data()['active_profile']
+
+
+def get_active_profile():
+ """Pick the active profile from profiles.json. If there is no
+ active profile on the file, this function will return None.
+
+ @returns: dict like {'user_id': 1234, 'username': 'email@blender.org'}
+ """
+ file_content = get_profiles_data()
+ user_id = file_content['active_profile']
+ if not user_id or user_id not in file_content['profiles']:
+ return None
+
+ profile = file_content['profiles'][user_id]
+ profile['user_id'] = user_id
+ return profile
+
+
+def get_profile(user_id):
+ """Loads the profile data for a given user_id if existing
+ else it returns None.
+ """
+
+ file_content = get_profiles_data()
+ if not user_id or user_id not in file_content['profiles']:
+ return None
+
+ profile = file_content['profiles'][user_id]
+ return dict(
+ username=profile['username'],
+ token=profile['token']
+ )
+
+
+def save_profiles_data(all_profiles: dict):
+ """Saves the profiles data to JSON."""
+ import json
+
+ with open(profiles_file, 'w', encoding='utf8') as outfile:
+ json.dump(all_profiles, outfile, sort_keys=True)
+
+
+def save_as_active_profile(user_id, token, username, subclients):
+ """Saves the given info as the active profile."""
+
+ BlenderIdProfile.user_id = user_id
+ BlenderIdProfile.token = token
+ BlenderIdProfile.username = username
+ BlenderIdProfile.subclients = subclients
+
+ BlenderIdProfile.save_json(make_active_profile=True)
+
+
+def logout(user_id):
+ """Invalidates the token and state of active for this user.
+ This is different from switching the active profile, where the active
+ profile is changed but there isn't an explicit logout.
+ """
+ import json
+
+ file_content = get_profiles_data()
+
+ # Remove user from 'active profile'
+ if file_content['active_profile'] == user_id:
+ file_content['active_profile'] = ""
+
+ # Remove both user and token from profiles list
+ if user_id in file_content['profiles']:
+ del file_content['profiles'][user_id]
+
+ with open(profiles_file, 'w', encoding='utf8') as outfile:
+ json.dump(file_content, outfile)