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
This commit is contained in:
parent
e2ebaa80b4
commit
84a93440fd
Notes:
blender-bot
2023-02-14 19:45:27 +01:00
Referenced by issue #49008, Blender-ID add-on for inclusion as OFFICIAL
|
@ -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()
|
|
@ -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()
|
|
@ -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']
|
|
@ -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)
|
Loading…
Reference in New Issue