Files
taiko-web2/public/src/js/gpicker.js
LoveEevee b8e63c650f ImportSongs: Implement Google Drive beta testing
Instances that wish to enable Google Drive support should first enable it to only a small subset of users (100 maximum) to allow the OAuth screen to be verified by Google without hitting the user limit. Minimum level in the config can be set to enable beta testing of this feature and then disabled by setting it to None.

- Add user level assignment screen to the administration panel
- Add privacy policy and links to it in various places
- Add switch accounts link near the Google Drive picker
2020-12-21 16:02:56 +03:00

272 lines
7.5 KiB
JavaScript

class Gpicker{
constructor(){
this.apiKey = gameConfig.google_credentials.api_key
this.oauthClientId = gameConfig.google_credentials.oauth_client_id
this.projectNumber = gameConfig.google_credentials.project_number
this.scope = "https://www.googleapis.com/auth/drive.readonly"
this.folder = "application/vnd.google-apps.folder"
this.filesUrl = "https://www.googleapis.com/drive/v3/files/"
this.resolveQueue = []
this.queueActive = false
}
browse(lockedCallback, errorCallback){
return this.loadApi()
.then(() => this.getToken(lockedCallback, errorCallback))
.then(() => new Promise((resolve, reject) => {
this.displayPicker(data => {
if(data.action === "picked"){
var file = data.docs[0]
var folders = []
var rateLimit = -1
var lastBatch = 0
var walk = (files, output=[]) => {
for(var i = 0; i < files.length; i++){
var path = files[i].path ? files[i].path + "/" : ""
var list = files[i].list
if(!list){
continue
}
for(var j = 0; j < list.length; j++){
var file = list[j]
if(file.mimeType === this.folder){
folders.push({
path: path + file.name,
id: file.id
})
}else{
output.push(new GdriveFile({
path: path + file.name,
name: file.name,
id: file.id
}))
}
}
}
var batchList = []
for(var i = 0; i < folders.length && batchList.length < 100; i++){
if(!folders[i].listed){
folders[i].pos = i
folders[i].listed = true
batchList.push(folders[i])
}
}
if(batchList.length){
var batch = gapi.client.newBatch()
batchList.forEach(folder => {
var req = {
q: "'" + folder.id + "' in parents and trashed = false",
orderBy: "name_natural"
}
if(folder.pageToken){
req.pageToken = folder.pageToken
}
batch.add(gapi.client.drive.files.list(req), {id: folder.pos})
})
if(lastBatch + batchList.length > 100){
var waitPromise = this.sleep(1000)
}else{
var waitPromise = Promise.resolve()
}
return waitPromise.then(() => this.queue()).then(() => batch.then(responses => {
var files = []
var rateLimited = false
for(var i in responses.result){
var result = responses.result[i].result
if(result.error){
if(result.error.errors[0].domain !== "usageLimits"){
console.warn(result)
}else if(!rateLimited){
rateLimited = true
rateLimit++
folders.push({
path: folders[i].path,
id: folders[i].id,
pageToken: folders[i].pageToken
})
}
}else{
if(result.nextPageToken){
folders.push({
path: folders[i].path,
id: folders[i].id,
pageToken: result.nextPageToken
})
}
files.push({path: folders[i].path, list: result.files})
}
}
if(rateLimited){
return this.sleep(Math.pow(2, rateLimit) * 1000).then(() => walk(files, output))
}else{
return walk(files, output)
}
}))
}else{
return output
}
}
if(file.mimeType === this.folder){
return walk([{list: [file]}]).then(resolve, reject)
}else{
return reject("cancel")
}
}else if(data.action === "cancel"){
return reject("cancel")
}
})
}))
}
loadApi(){
if(window.gapi && gapi.client && gapi.client.drive){
return Promise.resolve()
}
return loader.loadScript("https://apis.google.com/js/api.js")
.then(() => new Promise((resolve, reject) =>
gapi.load("auth2:picker:client", {
callback: resolve,
onerror: reject
})
))
.then(() => new Promise((resolve, reject) =>
gapi.client.load("drive", "v3").then(resolve, reject)
))
}
getAuth(errorCallback=()=>{}){
if(!this.auth){
return new Promise((resolve, reject) => {
gapi.auth2.init({
clientId: this.oauthClientId,
fetch_basic_profile: false,
scope: this.scope
}).then(() => {
this.auth = gapi.auth2.getAuthInstance()
resolve(this.auth)
}, e => {
if(e.details){
var errorStr = strings.gpicker.authError.replace("%s", e.details)
if(/cookie/i.test(e.details)){
errorStr += "\n\n" + strings.gpicker.cookieError
}
errorCallback(errorStr)
}
reject(e)
})
})
}else{
return Promise.resolve(this.auth)
}
}
getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){
if(this.oauthToken && !force){
return Promise.resolve()
}
return this.getAuth(errorCallback).then(auth => {
var user = force || auth.currentUser.get()
if(force || !this.checkScope(user)){
lockedCallback(false)
return auth.signIn(force ? {
prompt: "select_account"
} : undefined).then(user => {
if(this.checkScope(user)){
lockedCallback(true)
}else{
return Promise.reject("cancel")
}
}, () => Promise.reject("cancel"))
}
})
}
checkScope(user){
if(user.hasGrantedScopes(this.scope)){
this.oauthToken = user.getAuthResponse(true).access_token
return this.oauthToken
}else{
return false
}
}
switchAccounts(lockedCallback, errorCallback){
return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true))
}
displayPicker(callback){
var picker = gapi.picker.api
new picker.PickerBuilder()
.setDeveloperKey(this.apiKey)
.setAppId(this.projectNumber)
.setOAuthToken(this.oauthToken)
.setLocale(strings.gpicker.locale)
.hideTitleBar()
.addView(new picker.DocsView("folders")
.setLabel(strings.gpicker.myDrive)
.setParent("root")
.setSelectFolderEnabled(true)
.setMode("grid")
)
.addView(new picker.DocsView("folders")
.setLabel(strings.gpicker.starred)
.setStarred(true)
.setSelectFolderEnabled(true)
.setMode("grid")
)
.addView(new picker.DocsView("folders")
.setLabel(strings.gpicker.sharedWithMe)
.setOwnedByMe(false)
.setSelectFolderEnabled(true)
.setMode("list")
)
.setCallback(callback)
.setSize(Infinity, Infinity)
.build()
.setVisible(true)
}
downloadFile(id, arrayBuffer, retry){
var url = this.filesUrl + id + "?alt=media"
return this.queue().then(this.getToken.bind(this)).then(() =>
loader.ajax(url, request => {
if(arrayBuffer){
request.responseType = "arraybuffer"
}
request.setRequestHeader("Authorization", "Bearer " + this.oauthToken)
}, true).then(event => {
var request = event.target
var reject = () => Promise.reject(`${url} (${request.status})`)
if(request.status === 200){
return request.response
}else if(request.status === 401 && !retry){
return new Response(request.response).json().then(response => {
var e = response.error
if(e && e.errors[0].reason === "authError"){
delete this.oauthToken
return this.downloadFile(id, arrayBuffer, true)
}else{
return reject()
}
}, reject)
}
return reject()
})
)
}
sleep(time){
return new Promise(resolve => setTimeout(resolve, time))
}
queue(){
return new Promise(resolve => {
this.resolveQueue.push(resolve)
if(!this.queueActive){
this.queueActive = true
this.queueTimer = setInterval(this.parseQueue.bind(this), 100)
this.parseQueue()
}
})
}
parseQueue(){
if(this.resolveQueue.length){
var resolve = this.resolveQueue.shift()
resolve()
}else{
this.queueActive = false
clearInterval(this.queueTimer)
}
}
}