BlenderKit: Oauth browser login. No more hassle with API keys for the users. Needs testing.

renamed get_bkit_url to get_api_url
task queue is a new simple module to do tasks, more timed tasks should come here instead of being in assetbar as of now. (download, search checks and more)
This commit is contained in:
Vilem Duha 2019-04-25 22:35:26 +02:00
parent d2808959bb
commit e16c55a110
12 changed files with 269 additions and 33 deletions

View File

@ -41,9 +41,11 @@ if "bpy" in locals():
importlib.reload(bg_blender)
importlib.reload(paths)
importlib.reload(utils)
importlib.reload(oauth)
importlib.reload(tasks_queue)
else:
from blenderkit import asset_inspector, search, download, upload, ratings, autothumb, ui, bg_blender, paths, utils, \
overrides, ui_panels, categories
overrides, ui_panels, categories, oauth, tasks_queue
import os
import math
@ -71,7 +73,6 @@ from bpy.types import (
PropertyGroup,
)
# logging.basicConfig(filename = 'blenderkit.log', level = logging.INFO,
# format = ' %(asctime)s:%(filename)s:%(funcName)s:%(lineno)d:%(message)s')
@ -1208,9 +1209,8 @@ class BlenderKitAddonPreferences(AddonPreferences):
# this must match the addon name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __name__
from os.path import expanduser
home = expanduser("~")
default_global_dict = home + os.sep + 'blenderkit_data'
default_global_dict = paths.default_global_dict()
api_key: StringProperty(
name="BlenderKit API Key",
@ -1220,6 +1220,20 @@ class BlenderKitAddonPreferences(AddonPreferences):
update=utils.save_prefs
)
api_key_refresh: StringProperty(
name="BlenderKit refresh API Key",
description="API key used to refresh the token regularly.",
default="",
subtype="PASSWORD",
update=utils.save_prefs
)
login_attempt: BoolProperty(
name="Login/Signup attempt",
description="When this is on, BlenderKit is trying to connect and login.",
default=False
)
global_dir: StringProperty(
name="Global Files Directory",
description="Global storage for your assets, will use subdirectories for the contents",
@ -1389,6 +1403,8 @@ def register():
bpy.app.handlers.load_post.append(scene_load)
utils.load_prefs()
overrides.register_overrides()
oauth.register()
tasks_queue.register()
def unregister():
@ -1403,6 +1419,8 @@ def unregister():
ui_panels.unregister_ui_panels()
bg_blender.unregister()
overrides.unregister_overrides()
oauth.unregister()
tasks_queue.unregister()
del bpy.types.Scene.blenderkit_models
del bpy.types.Scene.blenderkit_scene

View File

@ -60,7 +60,7 @@ def copy_categories():
def fetch_categories(API_key):
BLENDERKIT_API_MAIN = "https://www.blenderkit.com/api/v1/"
url = paths.get_bkit_url() + 'categories/'
url = paths.get_api_url() + 'categories/'
headers = utils.get_headers(API_key)

View File

@ -174,7 +174,7 @@ def report_usages():
api_key = user_preferences.api_key
sid = get_scene_id()
headers = utils.get_headers(api_key)
url = paths.get_bkit_url() + paths.BLENDERKIT_REPORT_URL
url = paths.get_api_url() + paths.BLENDERKIT_REPORT_URL
assets = {}
asset_obs = []

176
blenderkit/oauth.py Normal file
View File

@ -0,0 +1,176 @@
# ##### 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 #####
import bpy
import json
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
import requests
import threading
from blenderkit import tasks_queue, utils, paths
CLIENT_ID = "IdFRwa3SGA8eMpzhRVFMg5Ts8sPK93xBjif93x0F"
PORTS = [62485, 1234]
class SimpleOAuthAuthenticator(object):
def __init__(self, server_url, client_id, ports):
self.server_url = server_url
self.client_id = client_id
self.ports = ports
def _get_tokens(self, authorization_code=None, refresh_token=None, grant_type="authorization_code"):
data = {
"grant_type": grant_type,
"state": "random_state_string",
"client_id": self.client_id,
"scopes": "read write",
}
if authorization_code:
data['code'] = authorization_code
if refresh_token:
data['refresh_token'] = refresh_token
response = requests.post(
'%s/o/token/' % self.server_url,
data=data
)
if response.status_code != 200:
return None, None
refresh_token = json.loads(response.content)['refresh_token']
access_token = json.loads(response.content)['access_token']
return access_token, refresh_token
def get_new_token(self):
class HTTPServerHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
if 'code' in self.path:
self.auth_code = self.path.split('=')[1]
# Display to the user that they no longer need the browser window
self.wfile.write(bytes('<html><h1>You may now close this window.</h1></html>', 'utf-8'))
qs = parse_qs(urlparse(self.path).query)
self.server.authorization_code = qs['code'][0]
for port in self.ports:
try:
httpServer = HTTPServer(('localhost', port), HTTPServerHandler)
except OSError:
continue
break
webbrowser.open_new(
"%s/o/authorize?client_id=%s&state=random_state_string&response_type=code&"
"redirect_uri=http://localhost:%s/consumer/exchange/" % (self.server_url, self.client_id, port),
)
httpServer.handle_request()
authorization_code = httpServer.authorization_code
return self._get_tokens(authorization_code=authorization_code)
def get_refreshed_token(self, refresh_token):
return self._get_tokens(refresh_token=refresh_token, grant_type="refresh_token")
def login_thread():
thread = threading.Thread(target=login, args=([]), daemon=True)
thread.start()
def login():
authenticator = SimpleOAuthAuthenticator(server_url=paths.get_bkit_url(), client_id=CLIENT_ID, ports=PORTS)
auth_token, refresh_token = authenticator.get_new_token()
print('tokens retrieved')
tasks_queue.tasks_queue.put('blenderkit.oauth.write_tokens("%s", "%s")' % (auth_token, refresh_token))
def refresh_token_thread():
preferences = bpy.context.preferences.addons['blenderkit'].preferences
if len(preferences.api_key_refresh) > 0:
thread = threading.Thread(target=refresh_token, args=([preferences.api_key_refresh]), daemon=True)
thread.start()
def refresh_token(api_key_refresh):
authenticator = SimpleOAuthAuthenticator(server_url=paths.get_bkit_url(), client_id=CLIENT_ID, ports=PORTS)
auth_token, refresh_token = authenticator.get_refreshed_token(api_key_refresh)
tasks_queue.tasks_queue.put('blenderkit.oauth.write_tokens("%s", "%s")' % (auth_token, refresh_token))
def write_tokens(auth_token, refresh_token):
print('writing tokens?')
preferences = bpy.context.preferences.addons['blenderkit'].preferences
preferences.api_key = auth_token
preferences.api_key_refresh = refresh_token
preferences.login_attempt = False
props = utils.get_search_props()
props.report = 'Login success!'
class RegisterLoginOnline(bpy.types.Operator):
"""Bring linked object hierarchy to scene and make it editable."""
bl_idname = "wm.blenderkit_login"
bl_label = "BlenderKit login or signup"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
preferences = bpy.context.preferences.addons['blenderkit'].preferences
preferences.login_attempt = True
login_thread()
return {'FINISHED'}
class CancelLoginOnline(bpy.types.Operator):
"""Cancel login attempt."""
bl_idname = "wm.blenderkit_login_cancel"
bl_label = "BlenderKit login cancel"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
preferences = bpy.context.preferences.addons['blenderkit'].preferences
preferences.login_attempt = False
return {'FINISHED'}
classess = (
RegisterLoginOnline,
CancelLoginOnline,
)
def register():
for c in classess:
bpy.utils.register_class(c)
def unregister():
for c in classess:
bpy.utils.unregister_class(c)

View File

@ -18,9 +18,10 @@
import bpy, os, sys
BLENDERKIT_API_LOCAL = "http://localhost:8001/api/v1/"
BLENDERKIT_API_MAIN = "https://www.blenderkit.com/api/v1/"
BLENDERKIT_API_DEVEL = "https://devel.blenderkit.com/api/v1/"
BLENDERKIT_LOCAL = "http://localhost:8001"
BLENDERKIT_MAIN = "https://www.blenderkit.com"
BLENDERKIT_DEVEL = "https://devel.blenderkit.com"
BLENDERKIT_API = "/api/v1/"
BLENDERKIT_REPORT_URL = "usage_report/"
BLENDERKIT_USER_ASSETS = "https://www.blenderkit.com/my-assets"
BLENDERKIT_PLANS = "https://www.blenderkit.com/plans/pricing/"
@ -38,14 +39,24 @@ def get_bkit_url():
d = bpy.app.debug_value
# d = 2
if d == 1:
url = BLENDERKIT_API_LOCAL
url = BLENDERKIT_LOCAL
elif d == 2:
url = BLENDERKIT_API_DEVEL
url = BLENDERKIT_DEVEL
else:
url = BLENDERKIT_API_MAIN
url = BLENDERKIT_MAIN
return url
def get_api_url():
return get_bkit_url() + BLENDERKIT_API
def default_global_dict():
from os.path import expanduser
home = expanduser("~")
return home + os.sep + 'blenderkit_data'
def get_categories_filepath():
tempdir = get_temp_dir()
return os.path.join(tempdir, 'categories.json')

View File

@ -90,7 +90,7 @@ def upload_rating(asset):
bkit_ratings = asset.bkit_ratings
# print('rating asset', asset_data['name'], asset_data['asset_base_id'])
url = paths.get_bkit_url() + 'assets/' + asset['asset_data']['id'] + '/rating/'
url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/'
ratings = [
]
@ -103,7 +103,7 @@ def upload_rating(asset):
thread = threading.Thread(target=uplaod_rating_thread, args=(url, ratings, headers))
thread.start()
url = paths.get_bkit_url() + 'assets/' + asset['asset_data']['id'] + '/review'
url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/review'
reviews = {
'reviewText': bkit_ratings.rating_compliments,

View File

@ -24,8 +24,9 @@ if "bpy" in locals():
imp.reload(categories)
imp.reload(ui)
imp.reload(version_checker)
imp.reload(oauth)
else:
from blenderkit import paths, utils, categories, ui, version_checker
from blenderkit import paths, utils, categories, ui, oauth, version_checker
import blenderkit
from bpy.app.handlers import persistent
@ -79,7 +80,7 @@ def scene_load(context):
wm = bpy.context.window_manager
fetch_server_data()
# following doesn't necessarilly happen if version isn't checked yet or similar, first run.
wm['bkit_update'] = version_checker.compare_versions(blenderkit)
# wm['bkit_update'] = version_checker.compare_versions(blenderkit)
utils.load_categories()
@ -89,6 +90,7 @@ def fetch_server_data():
url = paths.BLENDERKIT_ADDON_URL
api_key = user_preferences.api_key
# version_checker.check_version_thread(url, api_key, blenderkit)
oauth.refresh_token_thread()
categories.fetch_categories_thread(api_key)
@ -534,7 +536,7 @@ class Searcher(threading.Thread):
params['get_next'] = False
if not params['get_next']:
# build a new request
url = paths.get_bkit_url() + 'search/'
url = paths.get_api_url() + 'search/'
nquery = {
# 'tags': query['keywords'],

20
blenderkit/tasks_queue.py Normal file
View File

@ -0,0 +1,20 @@
import bpy
import queue
import blenderkit
tasks_queue = queue.Queue()
def every_2_seconds():
while not tasks_queue.empty():
print('as a task: ')
fstring = tasks_queue.get()
eval(fstring)
return 2.0
def register():
bpy.app.timers.register(every_2_seconds)
def unregister():
bpy.app.timers.unregister(every_2_seconds)

View File

@ -523,13 +523,17 @@ class VIEW3D_PT_blenderkit_unified(Panel):
row.prop(ui_props, 'asset_type', expand=True, icon_only=True)
w = context.region.width
if user_preferences.login_attempt:
layout.label(text = 'Login through browser')
layout.label(text = 'in progress.')
layout.operator("wm.blenderkit_login_cancel", text = "Cancel", icon = 'CANCEL')
return
if len(user_preferences.api_key) < 35 and user_preferences.asset_counter >25:
op = layout.operator("wm.url_open", text="Register online",
icon='QUESTION')
op.url = paths.BLENDERKIT_SIGNUP_URL
layout.label(text='Paste your API Key:')
layout.prop(user_preferences, 'api_key', text='')
if len(user_preferences.api_key) < 20 and user_preferences.asset_counter >-10:
layout.operator("wm.blenderkit_login", text="Login/ Sign up",
icon='URL')
# layout.label(text='Paste your API Key:')
# layout.prop(user_preferences, 'api_key', text='')
layout.separator()
elif bpy.data.filepath == '':

View File

@ -465,7 +465,7 @@ def mark_for_validation(self, context, asset_type):
"verificationStatus": "ready"
}
url = paths.get_bkit_url() + 'assets/'
url = paths.get_api_url() + 'assets/'
headers = utils.get_headers(user_preferences.api_key)
@ -559,7 +559,7 @@ def start_upload(self, context, asset_type, as_new, metadata_only):
return {'CANCELLED'}
# first upload metadata to server, so it can be saved inside the current file
url = paths.get_bkit_url() + 'assets/'
url = paths.get_api_url() + 'assets/'
headers = utils.get_headers(upload_data['token'])

View File

@ -76,7 +76,7 @@ def upload_files(filepath, upload_data, files):
'fileIndex': f['index'],
'originalFilename': os.path.basename(f['file_path'])
}
upload_create_url = paths.get_bkit_url() + 'uploads/'
upload_create_url = paths.get_api_url() + 'uploads/'
upload = requests.post(upload_create_url, json=upload_info, headers=headers, verify=True)
upload = upload.json()
@ -108,7 +108,7 @@ def upload_files(filepath, upload_data, files):
time.sleep(1)
# confirm single file upload to bkit server
upload_done_url = paths.get_bkit_url() + 'uploads_s3/' + upload['id'] + '/upload-file/'
upload_done_url = paths.get_api_url() + 'uploads_s3/' + upload['id'] + '/upload-file/'
upload_response = requests.post(upload_done_url, headers=headers, verify=True)
bg_blender.progress('finished uploading')
@ -187,7 +187,7 @@ if __name__ == "__main__":
"verificationStatus": "uploaded"
}
url = paths.get_bkit_url() + 'assets/'
url = paths.get_api_url() + 'assets/'
headers = utils.get_headers(upload_data['token'])

View File

@ -171,17 +171,22 @@ def load_prefs():
if os.path.exists(fpath):
with open(fpath, 'r') as s:
prefs = json.load(s)
user_preferences.api_key = prefs['API_key']
user_preferences.global_dir = prefs['global_dir']
user_preferences.api_key = prefs.get('API_key','')
user_preferences.global_dir = prefs.get('global_dir', paths.default_global_dict())
user_preferences.api_key_refresh = prefs.get('API_key_refresh','')
def save_prefs(self, context):
# print(type(context),type(bpy.context))
if not bpy.app.background and hasattr(bpy.context, 'view_layer'):
user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
if user_preferences.api_key != '':
if len(user_preferences.api_key)>35:
#we test the api key for lenght, so not a random accidentaly typed sequence gets saved.
if len(user_preferences.api_key)>25:
prefs = {
'API_key': user_preferences.api_key,
'API_key_refresh': user_preferences.api_key_refresh,
'global_dir': user_preferences.global_dir,
}
# user_preferences.api_key = user_preferences.api_key.strip()
@ -194,7 +199,7 @@ def save_prefs(self, context):
# reset the api key in case the user writes some nonsense, e.g. a search string instead of the Key
user_preferences.api_key = ''
props = get_search_props()
props.report = 'Please paste a correct API Key.'
props.report = 'Login failed. Please paste a correct API Key.'
def load_categories():
categories.copy_categories()