From 4f90e4f39c6ace3e1198af9b52d5ec46e3ba32e5 Mon Sep 17 00:00:00 2001 From: Tanay Pant Date: Mon, 19 Oct 2020 13:45:33 +0200 Subject: [PATCH] :sparkles: Add Google Slides node --- .../GoogleSlidesOAuth2Api.credentials.ts | 26 ++ .../nodes/Google/Slides/GenericFunctions.ts | 129 +++++++++ .../nodes/Google/Slides/GoogleSlides.node.ts | 249 ++++++++++++++++++ .../nodes/Google/Slides/googleslides.svg | 1 + packages/nodes-base/package.json | 2 + 5 files changed, 407 insertions(+) create mode 100644 packages/nodes-base/credentials/GoogleSlidesOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts create mode 100644 packages/nodes-base/nodes/Google/Slides/googleslides.svg diff --git a/packages/nodes-base/credentials/GoogleSlidesOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleSlidesOAuth2Api.credentials.ts new file mode 100644 index 0000000000..7f5fb14b42 --- /dev/null +++ b/packages/nodes-base/credentials/GoogleSlidesOAuth2Api.credentials.ts @@ -0,0 +1,26 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/presentations', +]; + +export class GoogleSlidesOAuth2Api implements ICredentialType { + name = 'googleSlidesOAuth2Api'; + extends = [ + 'googleOAuth2Api', + ]; + displayName = 'Google Slides OAuth2 API'; + documentationUrl = 'google'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts new file mode 100644 index 0000000000..bf46b17f4d --- /dev/null +++ b/packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts @@ -0,0 +1,129 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import * as moment from 'moment-timezone'; + +import * as jwt from 'jsonwebtoken'; + +export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string; + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://slides.googleapis.com${resource}`, + json: true + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (authenticationMethod === 'serviceAccount') { + const credentials = this.getCredentials('googleApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const { access_token } = await getAccessToken.call(this, credentials as IDataObject); + + options.headers!.Authorization = `Bearer ${access_token}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'googleSlidesOAuth2Api', options); + } + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Google Sheet error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} + +function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise { + //https://developers.google.com/identity/protocols/oauth2/service-account#httprest + + const scopes = [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/presentations', + ]; + + const now = moment().unix(); + + const signature = jwt.sign( + { + 'iss': credentials.email as string, + 'sub': credentials.email as string, + 'scope': scopes.join(' '), + 'aud': `https://oauth2.googleapis.com/token`, + 'iat': now, + 'exp': now + 3600, + }, + credentials.privateKey as string, + { + algorithm: 'RS256', + header: { + 'kid': credentials.privateKey as string, + 'typ': 'JWT', + 'alg': 'RS256', + }, + } + ); + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + form: { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: signature, + }, + uri: 'https://oauth2.googleapis.com/token', + json: true + }; + + //@ts-ignore + return this.helpers.request(options); +} diff --git a/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts new file mode 100644 index 0000000000..4f96cfb22e --- /dev/null +++ b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts @@ -0,0 +1,249 @@ + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + googleApiRequest, +} from './GenericFunctions'; + +export interface IGoogleAuthCredentials { + email: string; + privateKey: string; +} + +export class GoogleSlides implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Slides', + name: 'googleSlides', + icon: 'file:googleslides.svg', + group: ['input', 'output'], + version: 1, + description: 'Read data from Google Slides', + defaults: { + name: 'Google Slides', + color: '#0aa55c', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'serviceAccount', + ], + }, + }, + }, + { + name: 'googleSlidesOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Service Account', + value: 'serviceAccount', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'serviceAccount', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Presentations', + value: 'presentations', + }, + { + name: 'Pages', + value: 'pages', + }, + ], + default: 'presentations', + description: 'The resource to operate on', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a presentation', + }, + { + name: 'Get', + value: 'get', + description: 'Get a presentation', + }, + ], + displayOptions: { + show: { + resource: [ + 'presentations', + ], + }, + }, + default: 'create', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a page', + }, + { + name: 'Get Thumbnail', + value: 'getThumbnail', + description: 'Get a thumbnail', + }, + ], + displayOptions: { + show: { + resource: [ + 'pages', + ], + }, + }, + default: 'get', + description: 'The operation to perform', + }, + + // ---------------------------------- + // All + // ---------------------------------- + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'presentations', + ], + }, + }, + }, + { + displayName: 'Presentation ID', + name: 'presentationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + 'getThumbnail' + ], + resource: [ + 'presentations', + 'pages', + ], + }, + }, + }, + { + displayName: 'Page Object ID', + name: 'pageObjectId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + 'getThumbnail', + ], + resource: [ + 'pages', + ], + }, + }, + }, + ], + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = items.length as unknown as number; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const responseData = []; + + for (let i=0; i < length; i++) { + if (resource === 'presentations') { + if (operation === 'create') { + const title = this.getNodeParameter('title', i) as string; + let body = { + "title": title + }; + + const response = await googleApiRequest.call(this, 'POST', `/v1/presentations`, body); + responseData.push(response); + } else if (operation === 'get') { + const presentationId = this.getNodeParameter('presentationId', i) as string; + + const response = await googleApiRequest.call(this, 'GET', `/v1/presentations/${presentationId}`, {}); + responseData.push(response); + } + } else if (resource === 'pages') { + if (operation === 'get') { + const presentationId = this.getNodeParameter('presentationId', i) as string; + const pageObjectId = this.getNodeParameter('pageObjectId', i) as string; + + const response = await googleApiRequest.call(this, 'GET', `/v1/presentations/${presentationId}/pages/${pageObjectId}`, {}); + responseData.push(response); + } else if (operation === 'getThumbnail') { + const presentationId = this.getNodeParameter('presentationId', i) as string; + const pageObjectId = this.getNodeParameter('pageObjectId', i) as string; + + const response = await googleApiRequest.call(this, 'GET', `/v1/presentations/${presentationId}/pages/${pageObjectId}/thumbnail`, {}); + responseData.push(response); + } + } + } + return [this.helpers.returnJsonArray(responseData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/Slides/googleslides.svg b/packages/nodes-base/nodes/Google/Slides/googleslides.svg new file mode 100644 index 0000000000..1153cb7183 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Slides/googleslides.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7822f86a66..d591b13ba9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -80,6 +80,7 @@ "dist/credentials/GoogleDriveOAuth2Api.credentials.js", "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", + "dist/credentials/GoogleSlidesOAuth2Api.credentials.js", "dist/credentials/GSuiteAdminOAuth2Api.credentials.js", "dist/credentials/GoogleTasksOAuth2Api.credentials.js", "dist/credentials/YouTubeOAuth2Api.credentials.js", @@ -270,6 +271,7 @@ "dist/nodes/Google/Gmail/Gmail.node.js", "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", + "dist/nodes/Google/Slides/GoogleSlides.node.js", "dist/nodes/Google/Task/GoogleTasks.node.js", "dist/nodes/Google/YouTube/YouTube.node.js", "dist/nodes/GraphQL/GraphQL.node.js",