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..e25ec49e48
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts
@@ -0,0 +1,111 @@
+import {
+ OptionsWithUri,
+} from 'request';
+
+import {
+ IExecuteFunctions,
+ 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 | ILoadOptionsFunctions,
+ method: string,
+ resource: string,
+ body: IDataObject = {},
+ qs: IDataObject = {},
+) {
+ const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string;
+ const options: OptionsWithUri & { headers: IDataObject } = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ method,
+ body,
+ qs,
+ uri: `https://slides.googleapis.com/v1${resource}`,
+ json: true,
+ };
+
+ if (!Object.keys(body).length) {
+ delete options.body;
+ }
+
+ if (!Object.keys(qs).length) {
+ delete options.qs;
+ }
+
+ try {
+ if (authenticationMethod === 'serviceAccount') {
+ const credentials = this.getCredentials('googleApi') as { access_token: string, email: string, privateKey: string };
+ const { access_token } = await getAccessToken.call(this, credentials);
+ options.headers.Authorization = `Bearer ${access_token}`;
+ return await this.helpers.request!(options);
+
+ } else {
+ return await this.helpers.requestOAuth2!.call(this, 'googleSlidesOAuth2Api', options);
+ }
+ } catch (error) {
+
+ if (error?.response?.body?.message) {
+ throw new Error(`Google Slides error response [${error.statusCode}]: ${error.response.body.message}`);
+ }
+ throw error;
+ }
+}
+
+function getAccessToken(
+ this: IExecuteFunctions | ILoadOptionsFunctions,
+ { email, privateKey }: { email: string, privateKey: string },
+) {
+ // https://developers.google.com/identity/protocols/oauth2/service-account#httprest
+
+ const scopes = [
+ 'https://www.googleapis.com/auth/drive.file',
+ 'https://www.googleapis.com/auth/presentations',
+ ];
+
+ const now = moment().unix();
+
+ const signature = jwt.sign(
+ {
+ iss: email,
+ sub: email,
+ scope: scopes.join(' '),
+ aud: 'https://oauth2.googleapis.com/token',
+ iat: now,
+ exp: now + 3600,
+ },
+ privateKey,
+ {
+ algorithm: 'RS256',
+ header: {
+ kid: privateKey,
+ 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,
+ };
+
+ 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..48f7b2c34e
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts
@@ -0,0 +1,554 @@
+import {
+ IExecuteFunctions,
+} from 'n8n-core';
+
+import {
+ IDataObject,
+ ILoadOptionsFunctions,
+ INodeExecutionData,
+ INodePropertyOptions,
+ INodeType,
+ INodeTypeDescription,
+} from 'n8n-workflow';
+
+import {
+ googleApiRequest,
+} from './GenericFunctions';
+
+export class GoogleSlides implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Google Slides',
+ name: 'googleSlides',
+ icon: 'file:googleslides.svg',
+ group: ['input', 'output'],
+ version: 1,
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ description: 'Consume the Google Slides API',
+ defaults: {
+ name: 'Google Slides',
+ color: '#edba25',
+ },
+ 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: 'OAuth2',
+ value: 'oAuth2',
+ },
+ {
+ name: 'Service Account',
+ value: 'serviceAccount',
+ },
+ ],
+ default: 'serviceAccount',
+ },
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ options: [
+ {
+ name: 'Page',
+ value: 'page',
+ },
+ {
+ name: 'Presentation',
+ value: 'presentation',
+ },
+ ],
+ default: 'presentation',
+ description: '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',
+ },
+ {
+ name: 'Get Slides',
+ value: 'getSlides',
+ description: 'Get presentation slides',
+ },
+ {
+ name: 'Replace Text',
+ value: 'replaceText',
+ description: 'Replace text in a presentation',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: [
+ 'presentation',
+ ],
+ },
+ },
+ default: 'create',
+ description: '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: [
+ 'page',
+ ],
+ },
+ },
+ default: 'get',
+ description: 'Operation to perform',
+ },
+ {
+ displayName: 'Title',
+ name: 'title',
+ description: 'Title of the presentation to create.',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'presentation',
+ ],
+ operation: [
+ 'create',
+ ],
+ },
+ },
+ },
+ {
+ displayName: 'Presentation ID',
+ name: 'presentationId',
+ description: 'ID of the presentation to retrieve. Found in the presentation URL:
https://docs.google.com/presentation/d/PRESENTATION_ID/edit',
+ placeholder: '1wZtNFZ8MO-WKrxhYrOLMvyiqSgFwdSz5vn8_l_7eNqw',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'presentation',
+ 'page',
+ ],
+ operation: [
+ 'get',
+ 'getThumbnail',
+ 'getSlides',
+ 'replaceText',
+ ],
+ },
+ },
+ },
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ operation: [
+ 'getSlides',
+ ],
+ resource: [
+ 'presentation',
+ ],
+ },
+ },
+ default: false,
+ description: 'If all results should be returned or only up to a given limit.',
+ },
+ {
+ displayName: 'Limit',
+ name: 'limit',
+ type: 'number',
+ displayOptions: {
+ show: {
+ operation: [
+ 'getSlides',
+ ],
+ resource: [
+ 'presentation',
+ ],
+ returnAll: [
+ false,
+ ],
+ },
+ },
+ typeOptions: {
+ minValue: 1,
+ maxValue: 500,
+ },
+ default: 100,
+ description: 'How many results to return.',
+ },
+ {
+ displayName: 'Page Object ID',
+ name: 'pageObjectId',
+ description: 'ID of the page object to retrieve.',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'page',
+ ],
+ operation: [
+ 'get',
+ 'getThumbnail',
+ ],
+ },
+ },
+ },
+ {
+ displayName: 'Texts To Replace',
+ name: 'textUi',
+ placeholder: 'Add Text',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ displayOptions: {
+ show: {
+ resource: [
+ 'presentation',
+ ],
+ operation: [
+ 'replaceText',
+ ],
+ },
+ },
+ default: {},
+ options: [
+ {
+ name: 'textValues',
+ displayName: 'Text',
+ values: [
+ {
+ displayName: 'Match Case',
+ name: 'matchCase',
+ type: 'boolean',
+ default: false,
+ description: 'Indicates whether the search should respect case. True : the search is case sensitive. False : the search is case insensitive.',
+ },
+ {
+ displayName: 'Page IDs',
+ name: 'pageObjectIds',
+ type: 'multiOptions',
+ default: [],
+ typeOptions: {
+ loadOptionsMethod: 'getPages',
+ loadOptionsDependsOn: [
+ 'presentationId',
+ ],
+ },
+ description: 'If non-empty, limits the matches to page elements only on the given pages.',
+ },
+ {
+ displayName: 'Replace Text',
+ name: 'replaceText',
+ type: 'string',
+ default: '',
+ description: 'The text that will replace the matched text.',
+ },
+ {
+ displayName: 'Text',
+ name: 'text',
+ type: 'string',
+ default: '',
+ description: 'The text to search for in the shape or table.',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ displayOptions: {
+ show: {
+ operation: [
+ 'replaceText',
+ ],
+ resource: [
+ 'presentation',
+ ],
+ },
+ },
+ default: {},
+ options: [
+ {
+ displayName: 'Revision ID',
+ name: 'revisionId',
+ type: 'string',
+ default: '',
+ description: `The revision ID of the presentation required for the write request.
+ If specified and the requiredRevisionId doesn't exactly match the presentation's
+ current revisionId, the request will not be processed and will return a 400 bad request error.`,
+ },
+ ],
+ },
+
+ {
+ displayName: 'Download',
+ name: 'download',
+ type: 'boolean',
+ default: false,
+ displayOptions: {
+ show: {
+ resource: [
+ 'page',
+ ],
+ operation: [
+ 'getThumbnail',
+ ],
+ },
+ },
+ description: 'Name of the binary property to which to
write the data of the read page.',
+ },
+ {
+ displayName: 'Binary Property',
+ name: 'binaryProperty',
+ type: 'string',
+ required: true,
+ default: 'data',
+ description: 'Name of the binary property to which to write to.',
+ displayOptions: {
+ show: {
+ resource: [
+ 'page',
+ ],
+ operation: [
+ 'getThumbnail',
+ ],
+ download: [
+ true,
+ ],
+ },
+ },
+ },
+ ],
+ };
+
+ methods = {
+ loadOptions: {
+ // Get all the pages to display them to user so that he can
+ // select them easily
+ async getPages(
+ this: ILoadOptionsFunctions,
+ ): Promise {
+ const returnData: INodePropertyOptions[] = [];
+ const presentationId = this.getCurrentNodeParameter('presentationId') as string;
+ const { slides } = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}`, {}, { fields: 'slides' });
+ for (const slide of slides) {
+ returnData.push({
+ name: slide.objectId,
+ value: slide.objectId,
+ });
+ }
+ return returnData;
+ },
+ },
+ };
+
+
+ async execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+
+ const resource = this.getNodeParameter('resource', 0) as string;
+ const operation = this.getNodeParameter('operation', 0) as string;
+
+ let responseData;
+ const returnData: INodeExecutionData[] = [];
+
+ for (let i = 0; i < items.length; i++) {
+
+ if (resource === 'page') {
+
+ // *********************************************************************
+ // page
+ // *********************************************************************
+
+ if (operation === 'get') {
+
+ // ----------------------------------
+ // page: get
+ // ----------------------------------
+
+ const presentationId = this.getNodeParameter('presentationId', i) as string;
+ const pageObjectId = this.getNodeParameter('pageObjectId', i) as string;
+ responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}/pages/${pageObjectId}`);
+ returnData.push({ json: responseData });
+
+ } else if (operation === 'getThumbnail') {
+
+ // ----------------------------------
+ // page: getThumbnail
+ // ----------------------------------
+
+ const presentationId = this.getNodeParameter('presentationId', i) as string;
+ const pageObjectId = this.getNodeParameter('pageObjectId', i) as string;
+ responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}/pages/${pageObjectId}/thumbnail`);
+
+ const download = this.getNodeParameter('download', 0) as boolean;
+ if (download === true) {
+ const binaryProperty = this.getNodeParameter('binaryProperty', i) as string;
+
+ const data = await this.helpers.request({
+ uri: responseData.contentUrl,
+ method: 'GET',
+ json: false,
+ encoding: null,
+ });
+
+ const fileName = pageObjectId + '.png';
+ const binaryData = await this.helpers.prepareBinaryData(data, fileName || fileName);
+ returnData.push({
+ json: responseData,
+ binary: {
+ [binaryProperty]: binaryData,
+ },
+ });
+ } else {
+ returnData.push({ json: responseData });
+ }
+ }
+
+ } else if (resource === 'presentation') {
+
+ // *********************************************************************
+ // presentation
+ // *********************************************************************
+
+ if (operation === 'create') {
+
+ // ----------------------------------
+ // presentation: create
+ // ----------------------------------
+
+ const body = {
+ title: this.getNodeParameter('title', i) as string,
+ };
+
+ responseData = await googleApiRequest.call(this, 'POST', '/presentations', body);
+ returnData.push({ json: responseData });
+
+ } else if (operation === 'get') {
+
+ // ----------------------------------
+ // presentation: get
+ // ----------------------------------
+
+ const presentationId = this.getNodeParameter('presentationId', i) as string;
+ responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}`);
+ returnData.push({ json: responseData });
+
+ } else if (operation === 'getSlides') {
+
+ // ----------------------------------
+ // presentation: getSlides
+ // ----------------------------------
+ const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
+ const presentationId = this.getNodeParameter('presentationId', i) as string;
+ responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}`, {}, { fields: 'slides' });
+ responseData = responseData.slides;
+ if (returnAll === false) {
+ const limit = this.getNodeParameter('limit', i) as number;
+ responseData = responseData.slice(0, limit);
+ }
+ returnData.push(...this.helpers.returnJsonArray(responseData))
+
+ } else if (operation === 'replaceText') {
+
+ // ----------------------------------
+ // presentation: replaceText
+ // ----------------------------------
+ const presentationId = this.getNodeParameter('presentationId', i) as string;
+ const texts = this.getNodeParameter('textUi.textValues', i, []) as IDataObject[];
+ const options = this.getNodeParameter('options', i) as IDataObject;
+ const requests = texts.map((text => {
+ return {
+ replaceAllText: {
+ replaceText: text.replaceText,
+ pageObjectIds: text.pageObjectIds || [],
+ containsText: {
+ text: text.text,
+ matchCase: text.matchCase,
+ },
+ },
+ };
+ }));
+
+ const body: IDataObject = {
+ requests,
+ };
+
+ if (options.revisionId) {
+ body['writeControl'] = {
+ requiredRevisionId: options.revisionId as string,
+ };
+ }
+
+ responseData = await googleApiRequest.call(this, 'POST', `/presentations/${presentationId}:batchUpdate`, { requests });
+ returnData.push({ json: responseData });
+
+ }
+ }
+ }
+
+ return [returnData];
+ }
+}
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..d1fcb9c20b
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Slides/googleslides.svg
@@ -0,0 +1 @@
+
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index f7a15339d7..cb7e05155c 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -104,6 +104,7 @@
"dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.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/GoogleTranslateOAuth2Api.credentials.js",
@@ -371,6 +372,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/Translate/GoogleTranslate.node.js",
"dist/nodes/Google/YouTube/YouTube.node.js",