sun_position: fix warning from deleted prop in User Preferences
[blender-addons.git] / blender_id / __init__.py
blobcdd0b223e8a6614094bc576af1749399334a96c5
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Copyright (C) 2014-2018 Blender Foundation
19 # ##### END GPL LICENSE BLOCK #####
21 # <pep8 compliant>
23 bl_info = {
24 'name': 'Blender ID authentication',
25 'author': 'Sybren A. Stüvel, Francesco Siddi, and Inês Almeida',
26 'version': (2, 1, 0),
27 'blender': (2, 80, 0),
28 'location': 'Add-on preferences',
29 'description':
30 'Stores your Blender ID credentials for usage with other add-ons',
31 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
32 "system/blender_id.html",
33 'category': 'System',
34 'support': 'OFFICIAL',
37 import datetime
38 import typing
40 import bpy
41 from bpy.types import AddonPreferences, Operator, PropertyGroup
42 from bpy.props import PointerProperty, StringProperty
44 if 'communication' in locals():
45 import importlib
47 # noinspection PyUnboundLocalVariable
48 communication = importlib.reload(communication)
49 # noinspection PyUnboundLocalVariable
50 profiles = importlib.reload(profiles)
51 else:
52 from . import communication, profiles
53 BlenderIdProfile = profiles.BlenderIdProfile
54 BlenderIdCommError = communication.BlenderIdCommError
56 __all__ = ('get_active_profile', 'get_active_user_id', 'is_logged_in', 'create_subclient_token',
57 'BlenderIdProfile', 'BlenderIdCommError')
60 # Public API functions
61 def get_active_user_id() -> str:
62 """Get the id of the currently active profile. If there is no
63 active profile on the file, this function will return an empty string.
64 """
66 return BlenderIdProfile.user_id
69 def get_active_profile() -> BlenderIdProfile:
70 """Returns the active Blender ID profile. If there is no
71 active profile on the file, this function will return None.
73 :rtype: BlenderIdProfile
74 """
76 if not BlenderIdProfile.user_id:
77 return None
79 return BlenderIdProfile
82 def is_logged_in() -> bool:
83 """Returns whether the user is logged in on Blender ID or not."""
85 return bool(BlenderIdProfile.user_id)
88 def create_subclient_token(subclient_id: str, webservice_endpoint: str) -> dict:
89 """Lets the Blender ID server create a subclient token.
91 :param subclient_id: the ID of the subclient
92 :param webservice_endpoint: the URL of the endpoint of the webservice
93 that belongs to this subclient.
94 :returns: the token along with its expiry timestamp, in a {'scst': 'token',
95 'expiry': datetime.datetime} dict.
96 :raises: blender_id.communication.BlenderIdCommError when the
97 token cannot be created.
98 """
100 # Communication between us and Blender ID.
101 profile = get_active_profile()
102 scst_info = communication.subclient_create_token(profile.token, subclient_id)
103 subclient_token = scst_info['token']
105 # Send the token to the webservice.
106 user_id = communication.send_token_to_subclient(webservice_endpoint, profile.user_id,
107 subclient_token, subclient_id)
109 # Now that everything is okay we can store the token locally.
110 profile.subclients[subclient_id] = {'subclient_user_id': user_id, 'token': subclient_token}
111 profile.save_json()
113 return scst_info
116 def get_subclient_user_id(subclient_id: str) -> str:
117 """Returns the user ID at the given subclient.
119 Requires that the user has been authenticated at the subclient using
120 a call to create_subclient_token(...)
122 :returns: the subclient-local user ID, or the empty string if not logged in.
125 if not BlenderIdProfile.user_id:
126 return ''
128 return BlenderIdProfile.subclients[subclient_id]['subclient_user_id']
131 def validate_token() -> typing.Optional[str]:
132 """Validates the current user's token with Blender ID.
134 Also refreshes the stored token expiry time.
136 :returns: None if everything was ok, otherwise returns an error message.
139 expires, err = communication.blender_id_server_validate(token=BlenderIdProfile.token)
140 if err is not None:
141 return err
143 BlenderIdProfile.expires = expires
144 BlenderIdProfile.save_json()
146 return None
149 def token_expires() -> typing.Optional[datetime.datetime]:
150 """Returns the token expiry timestamp.
152 Returns None if the token expiry is unknown. This can happen when
153 the last login/validation was performed using a version of this
154 add-on that was older than 1.3.
157 exp = BlenderIdProfile.expires
158 if not exp:
159 return None
161 # Try parsing as different formats. A new Blender ID is coming,
162 # which may change the format in which timestamps are sent.
163 formats = [
164 '%Y-%m-%dT%H:%M:%SZ', # ISO 8601 with Z-suffix
165 '%Y-%m-%dT%H:%M:%S.%fZ', # ISO 8601 with fractional seconds and Z-suffix
166 '%a, %d %b %Y %H:%M:%S GMT', # RFC 1123, used by old Blender ID
168 for fmt in formats:
169 try:
170 return datetime.datetime.strptime(exp, fmt)
171 except ValueError:
172 # Just use the next format string and try again.
173 pass
175 # Unable to parse, may as well not be there then.
176 return None
179 class BlenderIdPreferences(AddonPreferences):
180 bl_idname = __name__
182 error_message: StringProperty(
183 name='Error Message',
184 default='',
185 options={'HIDDEN', 'SKIP_SAVE'}
187 ok_message: StringProperty(
188 name='Message',
189 default='',
190 options={'HIDDEN', 'SKIP_SAVE'}
192 blender_id_username: StringProperty(
193 name='E-mail address',
194 default='',
195 options={'HIDDEN', 'SKIP_SAVE'}
197 blender_id_password: StringProperty(
198 name='Password',
199 default='',
200 options={'HIDDEN', 'SKIP_SAVE'},
201 subtype='PASSWORD'
204 def reset_messages(self):
205 self.ok_message = ''
206 self.error_message = ''
208 def draw(self, context):
209 layout = self.layout
211 if self.error_message:
212 sub = layout.row()
213 sub.alert = True # labels don't display in red :(
214 sub.label(text=self.error_message, icon='ERROR')
215 if self.ok_message:
216 sub = layout.row()
217 sub.label(text=self.ok_message, icon='FILE_TICK')
219 active_profile = get_active_profile()
220 if active_profile:
221 expiry = token_expires()
222 now = datetime.datetime.utcnow()
224 if expiry is None:
225 layout.label(text='We do not know when your token expires, please validate it.')
226 elif now >= expiry:
227 layout.label(text='Your login has expired! Log out and log in again to refresh it.',
228 icon='ERROR')
229 else:
230 time_left = expiry - now
231 if time_left.days > 14:
232 exp_str = 'on {:%Y-%m-%d}'.format(expiry)
233 elif time_left.days > 1:
234 exp_str = 'in %i days.' % time_left.days
235 elif time_left.seconds >= 7200:
236 exp_str = 'in %i hours.' % round(time_left.seconds / 3600)
237 elif time_left.seconds >= 120:
238 exp_str = 'in %i minutes.' % round(time_left.seconds / 60)
239 else:
240 exp_str = 'within seconds'
242 endpoint = communication.blender_id_endpoint()
243 if endpoint == communication.BLENDER_ID_ENDPOINT:
244 msg = 'You are logged in as %s.' % active_profile.username
245 else:
246 msg = 'You are logged in as %s at %s.' % (active_profile.username, endpoint)
248 col = layout.column(align=True)
249 col.label(text=msg, icon='WORLD_DATA')
250 if time_left.days < 14:
251 col.label(text='Your token will expire %s. Please log out and log in again '
252 'to refresh it.' % exp_str, icon='PREVIEW_RANGE')
253 else:
254 col.label(text='Your authentication token expires %s.' % exp_str,
255 icon='BLANK1')
257 row = layout.row().split(factor=0.8)
258 row.operator('blender_id.logout')
259 row.operator('blender_id.validate')
260 else:
261 layout.prop(self, 'blender_id_username')
262 layout.prop(self, 'blender_id_password')
264 layout.operator('blender_id.login')
267 class BlenderIdMixin:
268 @staticmethod
269 def addon_prefs(context):
270 try:
271 prefs = context.preferences
272 except AttributeError:
273 prefs = context.user_preferences
275 addon_prefs = prefs.addons[__name__].preferences
276 addon_prefs.reset_messages()
277 return addon_prefs
280 class BlenderIdLogin(BlenderIdMixin, Operator):
281 bl_idname = 'blender_id.login'
282 bl_label = 'Login'
284 def execute(self, context):
285 import random
286 import string
288 addon_prefs = self.addon_prefs(context)
290 auth_result = communication.blender_id_server_authenticate(
291 username=addon_prefs.blender_id_username,
292 password=addon_prefs.blender_id_password
295 if auth_result.success:
296 # Prevent saving the password in user preferences. Overwrite the password with a
297 # random string, as just setting to '' might only replace the first byte with 0.
298 pwlen = len(addon_prefs.blender_id_password)
299 rnd = ''.join(random.choice(string.ascii_uppercase + string.digits)
300 for _ in range(pwlen + 16))
301 addon_prefs.blender_id_password = rnd
302 addon_prefs.blender_id_password = ''
304 profiles.save_as_active_profile(
305 auth_result,
306 addon_prefs.blender_id_username,
309 addon_prefs.ok_message = 'Logged in'
310 else:
311 addon_prefs.error_message = auth_result.error_message
312 if BlenderIdProfile.user_id:
313 profiles.logout(BlenderIdProfile.user_id)
315 BlenderIdProfile.read_json()
317 return {'FINISHED'}
320 class BlenderIdValidate(BlenderIdMixin, Operator):
321 bl_idname = 'blender_id.validate'
322 bl_label = 'Validate'
324 def execute(self, context):
325 addon_prefs = self.addon_prefs(context)
327 err = validate_token()
328 if err is None:
329 addon_prefs.ok_message = 'Authentication token is valid.'
330 else:
331 addon_prefs.error_message = '%s; you probably want to log out and log in again.' % err
333 BlenderIdProfile.read_json()
335 return {'FINISHED'}
338 class BlenderIdLogout(BlenderIdMixin, Operator):
339 bl_idname = 'blender_id.logout'
340 bl_label = 'Logout'
342 def execute(self, context):
343 addon_prefs = self.addon_prefs(context)
345 communication.blender_id_server_logout(BlenderIdProfile.user_id,
346 BlenderIdProfile.token)
348 profiles.logout(BlenderIdProfile.user_id)
349 BlenderIdProfile.read_json()
351 addon_prefs.ok_message = 'You have been logged out.'
352 return {'FINISHED'}
355 def register():
356 profiles.register()
357 BlenderIdProfile.read_json()
359 bpy.utils.register_class(BlenderIdLogin)
360 bpy.utils.register_class(BlenderIdLogout)
361 bpy.utils.register_class(BlenderIdPreferences)
362 bpy.utils.register_class(BlenderIdValidate)
364 preferences = BlenderIdMixin.addon_prefs(bpy.context)
365 preferences.reset_messages()
368 def unregister():
369 bpy.utils.unregister_class(BlenderIdLogin)
370 bpy.utils.unregister_class(BlenderIdLogout)
371 bpy.utils.unregister_class(BlenderIdPreferences)
372 bpy.utils.unregister_class(BlenderIdValidate)
375 if __name__ == '__main__':
376 register()