Cleanup: quiet float argument to in type warning
[blender-addons.git] / blender_id / __init__.py
blob6cd331c533683bb00750542de6f83dd964a330ac
1 # SPDX-License-Identifier: GPL-2.0-or-later
2 # Copyright 2014-2018 Blender Foundation.
4 bl_info = {
5 'name': 'Blender ID authentication',
6 'author': 'Sybren A. Stüvel, Francesco Siddi, and Inês Almeida',
7 'version': (2, 1, 0),
8 'blender': (2, 80, 0),
9 'location': 'Add-on preferences',
10 'description':
11 'Stores your Blender ID credentials for usage with other add-ons',
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/system/blender_id.html",
13 'category': 'System',
14 'support': 'OFFICIAL',
17 import datetime
18 import typing
20 import bpy
21 from bpy.types import AddonPreferences, Operator, PropertyGroup
22 from bpy.props import PointerProperty, StringProperty
23 from bpy.app.translations import pgettext_tip as tip_
25 if 'communication' in locals():
26 import importlib
28 # noinspection PyUnboundLocalVariable
29 communication = importlib.reload(communication)
30 # noinspection PyUnboundLocalVariable
31 profiles = importlib.reload(profiles)
32 else:
33 from . import communication, profiles
34 BlenderIdProfile = profiles.BlenderIdProfile
35 BlenderIdCommError = communication.BlenderIdCommError
37 __all__ = ('get_active_profile', 'get_active_user_id', 'is_logged_in', 'create_subclient_token',
38 'BlenderIdProfile', 'BlenderIdCommError')
41 # Public API functions
42 def get_active_user_id() -> str:
43 """Get the id of the currently active profile. If there is no
44 active profile on the file, this function will return an empty string.
45 """
47 return BlenderIdProfile.user_id
50 def get_active_profile() -> BlenderIdProfile:
51 """Returns the active Blender ID profile. If there is no
52 active profile on the file, this function will return None.
54 :rtype: BlenderIdProfile
55 """
57 if not BlenderIdProfile.user_id:
58 return None
60 return BlenderIdProfile
63 def is_logged_in() -> bool:
64 """Returns whether the user is logged in on Blender ID or not."""
66 return bool(BlenderIdProfile.user_id)
69 def create_subclient_token(subclient_id: str, webservice_endpoint: str) -> dict:
70 """Lets the Blender ID server create a subclient token.
72 :param subclient_id: the ID of the subclient
73 :param webservice_endpoint: the URL of the endpoint of the webservice
74 that belongs to this subclient.
75 :returns: the token along with its expiry timestamp, in a {'scst': 'token',
76 'expiry': datetime.datetime} dict.
77 :raises: blender_id.communication.BlenderIdCommError when the
78 token cannot be created.
79 """
81 # Communication between us and Blender ID.
82 profile = get_active_profile()
83 scst_info = communication.subclient_create_token(profile.token, subclient_id)
84 subclient_token = scst_info['token']
86 # Send the token to the webservice.
87 user_id = communication.send_token_to_subclient(webservice_endpoint, profile.user_id,
88 subclient_token, subclient_id)
90 # Now that everything is okay we can store the token locally.
91 profile.subclients[subclient_id] = {'subclient_user_id': user_id, 'token': subclient_token}
92 profile.save_json()
94 return scst_info
97 def get_subclient_user_id(subclient_id: str) -> str:
98 """Returns the user ID at the given subclient.
100 Requires that the user has been authenticated at the subclient using
101 a call to create_subclient_token(...)
103 :returns: the subclient-local user ID, or the empty string if not logged in.
106 if not BlenderIdProfile.user_id:
107 return ''
109 return BlenderIdProfile.subclients[subclient_id]['subclient_user_id']
112 def validate_token() -> typing.Optional[str]:
113 """Validates the current user's token with Blender ID.
115 Also refreshes the stored token expiry time.
117 :returns: None if everything was ok, otherwise returns an error message.
120 expires, err = communication.blender_id_server_validate(token=BlenderIdProfile.token)
121 if err is not None:
122 return err
124 BlenderIdProfile.expires = expires
125 BlenderIdProfile.save_json()
127 return None
130 def token_expires() -> typing.Optional[datetime.datetime]:
131 """Returns the token expiry timestamp.
133 Returns None if the token expiry is unknown. This can happen when
134 the last login/validation was performed using a version of this
135 add-on that was older than 1.3.
138 exp = BlenderIdProfile.expires
139 if not exp:
140 return None
142 # Try parsing as different formats. A new Blender ID is coming,
143 # which may change the format in which timestamps are sent.
144 formats = [
145 '%Y-%m-%dT%H:%M:%SZ', # ISO 8601 with Z-suffix
146 '%Y-%m-%dT%H:%M:%S.%fZ', # ISO 8601 with fractional seconds and Z-suffix
147 '%a, %d %b %Y %H:%M:%S GMT', # RFC 1123, used by old Blender ID
149 for fmt in formats:
150 try:
151 return datetime.datetime.strptime(exp, fmt)
152 except ValueError:
153 # Just use the next format string and try again.
154 pass
156 # Unable to parse, may as well not be there then.
157 return None
160 class BlenderIdPreferences(AddonPreferences):
161 bl_idname = __name__
163 error_message: StringProperty(
164 name='Error Message',
165 default='',
166 options={'HIDDEN', 'SKIP_SAVE'}
168 ok_message: StringProperty(
169 name='Message',
170 default='',
171 options={'HIDDEN', 'SKIP_SAVE'}
173 blender_id_username: StringProperty(
174 name='E-mail address',
175 default='',
176 options={'HIDDEN', 'SKIP_SAVE'}
178 blender_id_password: StringProperty(
179 name='Password',
180 default='',
181 options={'HIDDEN', 'SKIP_SAVE'},
182 subtype='PASSWORD'
185 def reset_messages(self):
186 self.ok_message = ''
187 self.error_message = ''
189 def draw(self, context):
190 layout = self.layout
192 if self.error_message:
193 sub = layout.row()
194 sub.alert = True # labels don't display in red :(
195 sub.label(text=self.error_message, icon='ERROR')
196 if self.ok_message:
197 sub = layout.row()
198 sub.label(text=self.ok_message, icon='FILE_TICK')
200 active_profile = get_active_profile()
201 if active_profile:
202 expiry = token_expires()
203 now = datetime.datetime.utcnow()
205 if expiry is None:
206 layout.label(text='We do not know when your token expires, please validate it.')
207 elif now >= expiry:
208 layout.label(text='Your login has expired! Log out and log in again to refresh it.',
209 icon='ERROR')
210 else:
211 time_left = expiry - now
212 if time_left.days > 14:
213 exp_str = tip_('on {:%Y-%m-%d}').format(expiry)
214 elif time_left.days > 1:
215 exp_str = tip_('in %i days.') % time_left.days
216 elif time_left.seconds >= 7200:
217 exp_str = tip_('in %i hours.') % round(time_left.seconds / 3600)
218 elif time_left.seconds >= 120:
219 exp_str = tip_('in %i minutes.') % round(time_left.seconds / 60)
220 else:
221 exp_str = tip_('within seconds')
223 endpoint = communication.blender_id_endpoint()
224 if endpoint == communication.BLENDER_ID_ENDPOINT:
225 msg = tip_('You are logged in as %s.') % active_profile.username
226 else:
227 msg = tip_('You are logged in as %s at %s.') % (active_profile.username, endpoint)
229 col = layout.column(align=True)
230 col.label(text=msg, icon='WORLD_DATA')
231 if time_left.days < 14:
232 col.label(text=tip_('Your token will expire %s. Please log out and log in again '
233 'to refresh it.') % exp_str, icon='PREVIEW_RANGE')
234 else:
235 col.label(text=tip_('Your authentication token expires %s.') % exp_str,
236 icon='BLANK1')
238 row = layout.row().split(factor=0.8)
239 row.operator('blender_id.logout')
240 row.operator('blender_id.validate')
241 else:
242 layout.prop(self, 'blender_id_username')
243 layout.prop(self, 'blender_id_password')
245 layout.operator('blender_id.login')
248 class BlenderIdMixin:
249 @staticmethod
250 def addon_prefs(context):
251 try:
252 prefs = context.preferences
253 except AttributeError:
254 prefs = context.user_preferences
256 addon_prefs = prefs.addons[__name__].preferences
257 addon_prefs.reset_messages()
258 return addon_prefs
261 class BlenderIdLogin(BlenderIdMixin, Operator):
262 bl_idname = 'blender_id.login'
263 bl_label = 'Login'
265 def execute(self, context):
266 import random
267 import string
269 addon_prefs = self.addon_prefs(context)
271 auth_result = communication.blender_id_server_authenticate(
272 username=addon_prefs.blender_id_username,
273 password=addon_prefs.blender_id_password
276 if auth_result.success:
277 # Prevent saving the password in user preferences. Overwrite the password with a
278 # random string, as just setting to '' might only replace the first byte with 0.
279 pwlen = len(addon_prefs.blender_id_password)
280 rnd = ''.join(random.choice(string.ascii_uppercase + string.digits)
281 for _ in range(pwlen + 16))
282 addon_prefs.blender_id_password = rnd
283 addon_prefs.blender_id_password = ''
285 profiles.save_as_active_profile(
286 auth_result,
287 addon_prefs.blender_id_username,
290 addon_prefs.ok_message = tip_('Logged in')
291 else:
292 addon_prefs.error_message = auth_result.error_message
293 if BlenderIdProfile.user_id:
294 profiles.logout(BlenderIdProfile.user_id)
296 BlenderIdProfile.read_json()
298 return {'FINISHED'}
301 class BlenderIdValidate(BlenderIdMixin, Operator):
302 bl_idname = 'blender_id.validate'
303 bl_label = 'Validate'
305 def execute(self, context):
306 addon_prefs = self.addon_prefs(context)
308 err = validate_token()
309 if err is None:
310 addon_prefs.ok_message = tip_('Authentication token is valid.')
311 else:
312 addon_prefs.error_message = tip_('%s; you probably want to log out and log in again.') % err
314 BlenderIdProfile.read_json()
316 return {'FINISHED'}
319 class BlenderIdLogout(BlenderIdMixin, Operator):
320 bl_idname = 'blender_id.logout'
321 bl_label = 'Logout'
323 def execute(self, context):
324 addon_prefs = self.addon_prefs(context)
326 communication.blender_id_server_logout(BlenderIdProfile.user_id,
327 BlenderIdProfile.token)
329 profiles.logout(BlenderIdProfile.user_id)
330 BlenderIdProfile.read_json()
332 addon_prefs.ok_message = tip_('You have been logged out.')
333 return {'FINISHED'}
336 def register():
337 profiles.register()
338 BlenderIdProfile.read_json()
340 bpy.utils.register_class(BlenderIdLogin)
341 bpy.utils.register_class(BlenderIdLogout)
342 bpy.utils.register_class(BlenderIdPreferences)
343 bpy.utils.register_class(BlenderIdValidate)
345 preferences = BlenderIdMixin.addon_prefs(bpy.context)
346 preferences.reset_messages()
349 def unregister():
350 bpy.utils.unregister_class(BlenderIdLogin)
351 bpy.utils.unregister_class(BlenderIdLogout)
352 bpy.utils.unregister_class(BlenderIdPreferences)
353 bpy.utils.unregister_class(BlenderIdValidate)
356 if __name__ == '__main__':
357 register()