mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-30 11:10:04 +00:00
468 lines
13 KiB
JavaScript
468 lines
13 KiB
JavaScript
/**
|
|
* @ngdoc service
|
|
* @name superProductivity.GoogleApi
|
|
* @description
|
|
* # GoogleApi
|
|
* Service in the superProductivity.
|
|
*/
|
|
(() => {
|
|
'use strict';
|
|
|
|
const DISCOVERY_DOCS = ['https://sheets.googleapis.com/$discovery/rest?version=v4'];
|
|
const SCOPES = '' +
|
|
'https://www.googleapis.com/auth/spreadsheets.readonly' +
|
|
' https://www.googleapis.com/auth/drive.install' +
|
|
' https://www.googleapis.com/auth/drive';
|
|
|
|
const DEFAULT_FIELDS_FOR_DRIVE = 'id,title,mimeType,userPermission,editable,modifiedDate,shared,createdDate,fileSize';
|
|
|
|
/**
|
|
* Helper for building multipart requests for uploading to Drive.
|
|
*/
|
|
const MultiPartBuilder = function() {
|
|
this.boundary = Math.random().toString(36).slice(2);
|
|
this.mimeType = 'multipart/mixed; boundary="' + this.boundary + '"';
|
|
this.parts = [];
|
|
this.body = null;
|
|
};
|
|
|
|
/**
|
|
* Appends a part.
|
|
*
|
|
* @param {String} mimeType Content type of this part
|
|
* @param {Blob|File|String} content Body of this part
|
|
*/
|
|
MultiPartBuilder.prototype.append = function(mimeType, content) {
|
|
if (this.body !== null) {
|
|
throw new Error('Builder has already been finalized.');
|
|
}
|
|
this.parts.push(
|
|
'\r\n--', this.boundary, '\r\n',
|
|
'Content-Type: ', mimeType, '\r\n\r\n',
|
|
content);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Finalizes building of the multipart request and returns a Blob containing
|
|
* the request. Once finalized, appending additional parts will result in an
|
|
* error.
|
|
*
|
|
* @returns {Object} Object containing the mime type (mimeType) & assembled multipart body (body)
|
|
*/
|
|
MultiPartBuilder.prototype.finish = function() {
|
|
if (this.parts.length === 0) {
|
|
throw new Error('No parts have been added.');
|
|
}
|
|
if (this.body === null) {
|
|
this.parts.push('\r\n--', this.boundary, '--');
|
|
this.body = this.parts.join('');
|
|
// TODO - switch to blob once gapi.client.request allows it
|
|
// this.body = new Blob(this.parts, {type: this.mimeType});
|
|
}
|
|
return {
|
|
type: this.mimeType,
|
|
body: this.body
|
|
};
|
|
};
|
|
|
|
class GoogleApi {
|
|
/* @ngInject */
|
|
constructor(GOOGLE, $q, IS_ELECTRON, $http, $rootScope, $mdToast, SimpleToast, $log) {
|
|
this.$q = $q;
|
|
this.$http = $http;
|
|
this.$log = $log;
|
|
this.$rootScope = $rootScope;
|
|
this.GOOGLE = GOOGLE;
|
|
this.IS_ELECTRON = IS_ELECTRON;
|
|
this.SimpleToast = SimpleToast;
|
|
this.$mdToast = $mdToast;
|
|
this.data = this.$rootScope.r.googleTokens;
|
|
this.isLoggedIn = false;
|
|
}
|
|
|
|
initClientLibraryIfNotDone() {
|
|
const defer = this.$q.defer();
|
|
this.loadLib(() => {
|
|
window.gapi.load('client:auth2', () => {
|
|
this.initClient()
|
|
.then(() => {
|
|
const GoogleAuth = window.gapi.auth2.getAuthInstance();
|
|
// used to determine and handle if user is already signed in
|
|
const user = GoogleAuth.currentUser.get();
|
|
defer.resolve(user);
|
|
});
|
|
});
|
|
});
|
|
|
|
return defer.promise;
|
|
}
|
|
|
|
loadJs(url, cb) {
|
|
const that = this;
|
|
const script = document.createElement('script');
|
|
script.setAttribute('src', url);
|
|
script.setAttribute('type', 'text/javascript');
|
|
|
|
this.isScriptLoaded = false;
|
|
const loadFunction = () => {
|
|
if (that.isScriptLoaded) {
|
|
return;
|
|
}
|
|
that.isScriptLoaded = true;
|
|
if (cb) {
|
|
cb();
|
|
}
|
|
};
|
|
script.onload = loadFunction.bind(that);
|
|
script.onreadystatechange = loadFunction.bind(that);
|
|
document.getElementsByTagName('head')[0].appendChild(script);
|
|
}
|
|
|
|
loadLib(cb) {
|
|
this.loadJs('https://apis.google.com/js/api.js', cb);
|
|
}
|
|
|
|
initClient() {
|
|
return window.gapi.client.init({
|
|
apiKey: this.GOOGLE.API_KEY,
|
|
clientId: this.GOOGLE.CLIENT_ID,
|
|
discoveryDocs: DISCOVERY_DOCS,
|
|
scope: SCOPES
|
|
});
|
|
|
|
}
|
|
|
|
login() {
|
|
/*jshint camelcase: false */
|
|
const EXPIRES_SAFETY_MARGIN = 30000;
|
|
const isExpired = (!this.data.expiresAt || window.moment()
|
|
.valueOf() + EXPIRES_SAFETY_MARGIN > this.data.expiresAt);
|
|
|
|
if (isExpired) {
|
|
this.data.accessToken = undefined;
|
|
}
|
|
|
|
if (this.data.accessToken && !isExpired) {
|
|
this.isLoggedIn = true;
|
|
return new Promise((resolve) => resolve());
|
|
}
|
|
|
|
if (this.IS_ELECTRON) {
|
|
window.ipcRenderer.send('TRIGGER_GOOGLE_AUTH');
|
|
return new Promise((resolve, reject) => {
|
|
window.ipcRenderer.on('GOOGLE_AUTH_TOKEN', (ev, data) => {
|
|
const token = data.access_token;
|
|
this.data.accessToken = token;
|
|
this.data.expiresAt = (data.expires_in * 1000) + window.moment().valueOf();
|
|
|
|
//this.data.refreshToken = data.refresh_token;
|
|
this.isLoggedIn = true;
|
|
this.SimpleToast('SUCCESS', 'GoogleApi: Login successful');
|
|
|
|
resolve();
|
|
// TODO remove
|
|
// mainWindow.webContents.removeListener('did-finish-load', handler);
|
|
});
|
|
window.ipcRenderer.on('GOOGLE_AUTH_TOKEN_ERROR', reject);
|
|
});
|
|
} else {
|
|
return this.initClientLibraryIfNotDone()
|
|
.then((user) => {
|
|
if (user && user.Zi && user.Zi.access_token) {
|
|
this.isLoggedIn = true;
|
|
this.saveToken(user);
|
|
this.SimpleToast('SUCCESS', 'GoogleApi: Login successful');
|
|
} else {
|
|
return window.gapi.auth2.getAuthInstance().signIn()
|
|
.then((res) => {
|
|
console.log(res);
|
|
this.isLoggedIn = true;
|
|
this.saveToken(res);
|
|
this.SimpleToast('SUCCESS', 'GoogleApi: Login successful');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
/*jshint camelcase: true */
|
|
}
|
|
|
|
saveToken(res) {
|
|
/*jshint camelcase: false */
|
|
this.data.accessToken = res.Zi.access_token;
|
|
this.data.expiresAt = res.Zi.expires_at;
|
|
/*jshint camelcase: true */
|
|
}
|
|
|
|
logout() {
|
|
this.isLoggedIn = false;
|
|
this.data.accessToken = undefined;
|
|
this.data.expiresAt = undefined;
|
|
this.data.refreshToken = undefined;
|
|
|
|
if (this.IS_ELECTRON) {
|
|
return new Promise((resolve) => {
|
|
resolve();
|
|
});
|
|
} else {
|
|
if (window.gapi) {
|
|
return window.gapi.auth2.getAuthInstance().signOut();
|
|
} else {
|
|
return new Promise((resolve) => {
|
|
resolve();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Other interaction
|
|
//-------------------
|
|
appendRow(spreadsheetId, row) {
|
|
// @see: https://developers.google.com/sheets/api/reference/rest/
|
|
const range = 'A1:Z99';
|
|
return this.requestWrapper(this.$http({
|
|
method: 'POST',
|
|
url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}:append`,
|
|
params: {
|
|
'key': this.GOOGLE.API_KEY,
|
|
insertDataOption: 'INSERT_ROWS',
|
|
valueInputOption: 'USER_ENTERED'
|
|
},
|
|
headers: {
|
|
'Authorization': `Bearer ${this.data.accessToken}`
|
|
},
|
|
data: { values: [row] }
|
|
}));
|
|
}
|
|
|
|
getSpreadsheetData(spreadsheetId, range) {
|
|
// @see: https://developers.google.com/sheets/api/reference/rest/
|
|
return this.requestWrapper(this.$http({
|
|
method: 'GET',
|
|
url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}`,
|
|
params: {
|
|
'key': this.GOOGLE.API_KEY,
|
|
},
|
|
headers: {
|
|
'Authorization': `Bearer ${this.data.accessToken}`
|
|
}
|
|
}));
|
|
}
|
|
|
|
getSpreadsheetHeadingsAndLastRow(spreadsheetId) {
|
|
const defer = this.$q.defer();
|
|
|
|
this.getSpreadsheetData(spreadsheetId, 'A1:Z99')
|
|
.then((response) => {
|
|
const range = response.result || response.data;
|
|
|
|
if (range.values && range.values[0]) {
|
|
defer.resolve({
|
|
headings: range.values[0],
|
|
lastRow: range.values[range.values.length - 1],
|
|
});
|
|
} else {
|
|
defer.reject('No data found');
|
|
this.handleError('No data found');
|
|
}
|
|
});
|
|
|
|
return this.requestWrapper(defer.promise);
|
|
}
|
|
|
|
getFileInfo(fileId) {
|
|
if (!fileId) {
|
|
this.SimpleToast('ERROR', 'GoogleApi: No file id specified');
|
|
return this.$q.reject('No file id given');
|
|
}
|
|
|
|
return this.requestWrapper(this.$http({
|
|
method: 'GET',
|
|
url: `https://content.googleapis.com/drive/v2/files/${encodeURIComponent(fileId)}`,
|
|
params: {
|
|
'key': this.GOOGLE.API_KEY,
|
|
supportsTeamDrives: true,
|
|
fields: DEFAULT_FIELDS_FOR_DRIVE
|
|
},
|
|
headers: {
|
|
'Authorization': `Bearer ${this.data.accessToken}`,
|
|
},
|
|
}));
|
|
}
|
|
|
|
findFile(fileName) {
|
|
if (!fileName) {
|
|
this.SimpleToast('ERROR', 'GoogleApi: No file name specified');
|
|
return this.$q.reject('No file name given');
|
|
}
|
|
|
|
return this.requestWrapper(this.$http({
|
|
method: 'GET',
|
|
url: `https://content.googleapis.com/drive/v2/files`,
|
|
params: {
|
|
'key': this.GOOGLE.API_KEY,
|
|
// should be called name officially instead of title
|
|
q: `title='${fileName}' and trashed=false`,
|
|
},
|
|
headers: {
|
|
'Authorization': `Bearer ${this.data.accessToken}`,
|
|
},
|
|
}));
|
|
}
|
|
|
|
loadFile(fileId) {
|
|
if (!fileId) {
|
|
this.SimpleToast('ERROR', 'GoogleApi: No file id specified');
|
|
return this.$q.reject('No file id given');
|
|
}
|
|
|
|
const metaData = this.getFileInfo(fileId);
|
|
const fileContents = this.requestWrapper(this.$http({
|
|
method: 'GET',
|
|
url: `https://content.googleapis.com/drive/v2/files/${encodeURIComponent(fileId)}`,
|
|
params: {
|
|
'key': this.GOOGLE.API_KEY,
|
|
supportsTeamDrives: true,
|
|
alt: 'media'
|
|
},
|
|
headers: {
|
|
'Authorization': `Bearer ${this.data.accessToken}`,
|
|
},
|
|
}));
|
|
|
|
return this.$q.all([this.$q.when(metaData), this.$q.when(fileContents)])
|
|
.then((res) => {
|
|
return this.$q.when({
|
|
backup: res[1].data,
|
|
meta: res[0].data,
|
|
});
|
|
});
|
|
}
|
|
|
|
saveFile(content, metadata = {}) {
|
|
if (!angular.isString(content)) {
|
|
content = JSON.stringify(content);
|
|
}
|
|
|
|
let path;
|
|
let method;
|
|
|
|
if (metadata.id) {
|
|
path = '/upload/drive/v2/files/' + encodeURIComponent(metadata.id);
|
|
method = 'PUT';
|
|
} else {
|
|
path = '/upload/drive/v2/files/';
|
|
method = 'POST';
|
|
}
|
|
|
|
if (!metadata.mimeType) {
|
|
metadata.mimeType = 'application/json';
|
|
}
|
|
|
|
const multipart = new MultiPartBuilder()
|
|
.append('application/json', JSON.stringify(metadata))
|
|
.append(metadata.mimeType, content)
|
|
.finish();
|
|
|
|
return this.requestWrapper(this.$http({
|
|
method: method,
|
|
url: `https://content.googleapis.com${path}`,
|
|
params: {
|
|
'key': this.GOOGLE.API_KEY,
|
|
uploadType: 'multipart',
|
|
supportsTeamDrives: true,
|
|
fields: DEFAULT_FIELDS_FOR_DRIVE
|
|
},
|
|
headers: {
|
|
'Authorization': `Bearer ${this.data.accessToken}`,
|
|
'Content-Type': multipart.type
|
|
},
|
|
data: multipart.body
|
|
}));
|
|
}
|
|
|
|
handleUnAuthenticated(err) {
|
|
this.$log.error(err);
|
|
this.logout();
|
|
|
|
const icon = 'error';
|
|
const iconColor = '#e15d63';
|
|
const msg = 'GoogleApi: Failed to authenticate please try logging in again!';
|
|
this.$mdToast.show({
|
|
hideDelay: 20 * 1000,
|
|
controller: ($scope, $mdToast, GoogleApi) => {
|
|
$scope.login = () => {
|
|
GoogleApi.login();
|
|
$mdToast.hide();
|
|
};
|
|
},
|
|
template: `
|
|
<md-toast>
|
|
<div class="md-toast-text" flex>
|
|
<ng-md-icon icon="${icon}" style="fill:${iconColor}"></ng-md-icon>
|
|
${msg}
|
|
<md-button ng-click="login()">Login</md-button>
|
|
</div>
|
|
</md-toast>`
|
|
});
|
|
}
|
|
|
|
handleError(err) {
|
|
let errStr = '';
|
|
|
|
if (typeof err === 'string') {
|
|
errStr = err;
|
|
} else if (err && err.data && err.data.error) {
|
|
errStr = err.data.error.message;
|
|
}
|
|
|
|
if (errStr) {
|
|
errStr = ': ' + errStr;
|
|
}
|
|
|
|
this.$log.error(err);
|
|
|
|
if (err && err.status === 401) {
|
|
this.handleUnAuthenticated(err);
|
|
} else {
|
|
this.$log.error(err);
|
|
this.SimpleToast('ERROR', 'GoogleApi Error' + errStr);
|
|
}
|
|
|
|
return this.$q.reject();
|
|
}
|
|
|
|
requestWrapper(request) {
|
|
const defer = this.$q.defer();
|
|
request.then((res) => {
|
|
if (res && res.status < 300) {
|
|
defer.resolve(res);
|
|
} else if (!res) {
|
|
this.handleError('No response body');
|
|
defer.reject(res);
|
|
} else if (res && res.status >= 300) {
|
|
this.handleError(res);
|
|
} else if (res && res.status === 401) {
|
|
this.handleUnAuthenticated(res);
|
|
defer.reject(res);
|
|
} else {
|
|
// in dubio pro reo
|
|
defer.resolve(res);
|
|
}
|
|
|
|
}).catch((err) => {
|
|
this.handleError(err);
|
|
defer.reject(err);
|
|
});
|
|
return defer.promise;
|
|
}
|
|
}
|
|
|
|
angular
|
|
.module('superProductivity')
|
|
.service('GoogleApi', GoogleApi);
|
|
|
|
// hacky fix for ff
|
|
GoogleApi.$$ngIsClass = true;
|
|
})();
|
|
|