expre-gen/blender/3.4/scripts/addons/blender_id/__init__.py
2025-10-17 09:47:23 +08:00

358 lines
11 KiB
Python

# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2014-2018 Blender Foundation.
bl_info = {
'name': 'Blender ID authentication',
'author': 'Sybren A. Stüvel, Francesco Siddi, and Inês Almeida',
'version': (2, 1, 0),
'blender': (2, 80, 0),
'location': 'Add-on preferences',
'description':
'Stores your Blender ID credentials for usage with other add-ons',
"doc_url": "{BLENDER_MANUAL_URL}/addons/system/blender_id.html",
'category': 'System',
'support': 'OFFICIAL',
}
import datetime
import typing
import bpy
from bpy.types import AddonPreferences, Operator, PropertyGroup
from bpy.props import PointerProperty, StringProperty
from bpy.app.translations import pgettext_tip as tip_
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 the empty string if not logged in.
"""
if not BlenderIdProfile.user_id:
return ''
return BlenderIdProfile.subclients[subclient_id]['subclient_user_id']
def validate_token() -> typing.Optional[str]:
"""Validates the current user's token with Blender ID.
Also refreshes the stored token expiry time.
:returns: None if everything was ok, otherwise returns an error message.
"""
expires, err = communication.blender_id_server_validate(token=BlenderIdProfile.token)
if err is not None:
return err
BlenderIdProfile.expires = expires
BlenderIdProfile.save_json()
return None
def token_expires() -> typing.Optional[datetime.datetime]:
"""Returns the token expiry timestamp.
Returns None if the token expiry is unknown. This can happen when
the last login/validation was performed using a version of this
add-on that was older than 1.3.
"""
exp = BlenderIdProfile.expires
if not exp:
return None
# Try parsing as different formats. A new Blender ID is coming,
# which may change the format in which timestamps are sent.
formats = [
'%Y-%m-%dT%H:%M:%SZ', # ISO 8601 with Z-suffix
'%Y-%m-%dT%H:%M:%S.%fZ', # ISO 8601 with fractional seconds and Z-suffix
'%a, %d %b %Y %H:%M:%S GMT', # RFC 1123, used by old Blender ID
]
for fmt in formats:
try:
return datetime.datetime.strptime(exp, fmt)
except ValueError:
# Just use the next format string and try again.
pass
# Unable to parse, may as well not be there then.
return None
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(text=self.error_message, icon='ERROR')
if self.ok_message:
sub = layout.row()
sub.label(text=self.ok_message, icon='FILE_TICK')
active_profile = get_active_profile()
if active_profile:
expiry = token_expires()
now = datetime.datetime.utcnow()
if expiry is None:
layout.label(text='We do not know when your token expires, please validate it.')
elif now >= expiry:
layout.label(text='Your login has expired! Log out and log in again to refresh it.',
icon='ERROR')
else:
time_left = expiry - now
if time_left.days > 14:
exp_str = tip_('on {:%Y-%m-%d}').format(expiry)
elif time_left.days > 1:
exp_str = tip_('in %i days.') % time_left.days
elif time_left.seconds >= 7200:
exp_str = tip_('in %i hours.') % round(time_left.seconds / 3600)
elif time_left.seconds >= 120:
exp_str = tip_('in %i minutes.') % round(time_left.seconds / 60)
else:
exp_str = tip_('within seconds')
endpoint = communication.blender_id_endpoint()
if endpoint == communication.BLENDER_ID_ENDPOINT:
msg = tip_('You are logged in as %s.') % active_profile.username
else:
msg = tip_('You are logged in as %s at %s.') % (active_profile.username, endpoint)
col = layout.column(align=True)
col.label(text=msg, icon='WORLD_DATA')
if time_left.days < 14:
col.label(text=tip_('Your token will expire %s. Please log out and log in again '
'to refresh it.') % exp_str, icon='PREVIEW_RANGE')
else:
col.label(text=tip_('Your authentication token expires %s.') % exp_str,
icon='BLANK1')
row = layout.row().split(factor=0.8)
row.operator('blender_id.logout')
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):
try:
prefs = context.preferences
except AttributeError:
prefs = context.user_preferences
addon_prefs = prefs.addons[__name__].preferences
addon_prefs.reset_messages()
return addon_prefs
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)
auth_result = communication.blender_id_server_authenticate(
username=addon_prefs.blender_id_username,
password=addon_prefs.blender_id_password
)
if auth_result.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(
auth_result,
addon_prefs.blender_id_username,
{}
)
addon_prefs.ok_message = tip_('Logged in')
else:
addon_prefs.error_message = auth_result.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)
err = validate_token()
if err is None:
addon_prefs.ok_message = tip_('Authentication token is valid.')
else:
addon_prefs.error_message = tip_('%s; you probably want to log out and log in again.') % err
BlenderIdProfile.read_json()
return {'FINISHED'}
class BlenderIdLogout(BlenderIdMixin, Operator):
bl_idname = 'blender_id.logout'
bl_label = 'Logout'
def execute(self, context):
addon_prefs = self.addon_prefs(context)
communication.blender_id_server_logout(BlenderIdProfile.user_id,
BlenderIdProfile.token)
profiles.logout(BlenderIdProfile.user_id)
BlenderIdProfile.read_json()
addon_prefs.ok_message = tip_('You have been logged out.')
return {'FINISHED'}
def register():
profiles.register()
BlenderIdProfile.read_json()
bpy.utils.register_class(BlenderIdLogin)
bpy.utils.register_class(BlenderIdLogout)
bpy.utils.register_class(BlenderIdPreferences)
bpy.utils.register_class(BlenderIdValidate)
preferences = BlenderIdMixin.addon_prefs(bpy.context)
preferences.reset_messages()
def unregister():
bpy.utils.unregister_class(BlenderIdLogin)
bpy.utils.unregister_class(BlenderIdLogout)
bpy.utils.unregister_class(BlenderIdPreferences)
bpy.utils.unregister_class(BlenderIdValidate)
if __name__ == '__main__':
register()