Add Google Drive Trigger (#2219)

*  Google Drive Trigger

*  Small change

*  Small change

*  Improvements

*  Improvement

*  Improvements

*  Improvements

*  Improvements
This commit is contained in:
Ricardo Espinoza 2021-10-21 14:20:24 -04:00 committed by GitHub
parent d0403dd875
commit 029b9738eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 464 additions and 301 deletions

View file

@ -9,14 +9,17 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, NodeApiError, NodeOperationError, IDataObject,
IPollFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as moment from 'moment-timezone'; import * as moment from 'moment-timezone';
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string; const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string;
let options: OptionsWithUri = { let options: OptionsWithUri = {
@ -29,7 +32,9 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
uri: uri || `https://www.googleapis.com${resource}`, uri: uri || `https://www.googleapis.com${resource}`,
json: true, json: true,
}; };
options = Object.assign({}, options, option); options = Object.assign({}, options, option);
try { try {
if (Object.keys(body).length === 0) { if (Object.keys(body).length === 0) {
delete options.body; delete options.body;
@ -59,17 +64,16 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
} }
} }
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
let responseData; let responseData;
query.maxResults = 100; query.maxResults = query.maxResults || 100;
query.pageSize = 100; query.pageSize = query.pageSize || 100;
do { do {
responseData = await googleApiRequest.call(this, method, endpoint, body, query); responseData = await googleApiRequest.call(this, method, endpoint, body, query);
query.pageToken = responseData['nextPageToken'];
returnData.push.apply(returnData, responseData[propertyName]); returnData.push.apply(returnData, responseData[propertyName]);
} while ( } while (
responseData['nextPageToken'] !== undefined && responseData['nextPageToken'] !== undefined &&
@ -79,7 +83,7 @@ export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOp
return returnData; return returnData;
} }
function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise<IDataObject> { function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, credentials: IDataObject): Promise<IDataObject> {
//https://developers.google.com/identity/protocols/oauth2/service-account#httprest //https://developers.google.com/identity/protocols/oauth2/service-account#httprest
const scopes = [ const scopes = [
@ -125,3 +129,17 @@ function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoa
return this.helpers.request!(options); return this.helpers.request!(options);
} }
export function extractId(url: string): string {
if (url.includes('/d/')) {
//https://docs.google.com/document/d/1TUJGUf5HUv9e6MJBzcOsPruxXDeGMnGYTBWfkMagcg4/edit
const data = url.match(/[-\w]{25,}/);
if (Array.isArray(data)) {
return data[0];
}
} else if (url.includes('/folders/')) {
//https://drive.google.com/drive/u/0/folders/19MqnruIXju5sAWYD3J71im1d2CBJkZzy
return url.split('/folders/')[1];
}
return url;
}

View file

@ -1,294 +1,438 @@
// import { google } from 'googleapis'; import {
IPollFunctions,
// import { } from 'n8n-core';
// IHookFunctions,
// IWebhookFunctions, import {
// } from 'n8n-core'; IDataObject,
ILoadOptionsFunctions,
// import { INodeExecutionData,
// IDataObject, INodePropertyOptions,
// INodeTypeDescription, INodeType,
// INodeType, INodeTypeDescription,
// IWebhookResponseData, NodeApiError,
// NodeOperationError, } from 'n8n-workflow';
// } from 'n8n-workflow';
import {
// import { getAuthenticationClient } from './GoogleApi'; extractId,
googleApiRequest,
googleApiRequestAllItems,
// export class GoogleDriveTrigger implements INodeType { } from './GenericFunctions';
// description: INodeTypeDescription = {
// displayName: 'Google Drive Trigger', import * as moment from 'moment';
// name: 'googleDriveTrigger',
// icon: 'file:googleDrive.png', export class GoogleDriveTrigger implements INodeType {
// group: ['trigger'], description: INodeTypeDescription = {
// version: 1, displayName: 'Google Drive Trigger',
// subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', name: 'googleDriveTrigger',
// description: 'Starts the workflow when a file on Google Drive is changed', icon: 'file:googleDrive.svg',
// defaults: { group: ['trigger'],
// name: 'Google Drive Trigger', version: 1,
// color: '#3f87f2', description: 'Starts the workflow when Google Drive events occur',
// }, subtitle: '={{$parameter["event"]}}',
// inputs: [], defaults: {
// outputs: ['main'], name: 'Google Drive Trigger',
// credentials: [ color: '#4285F4',
// { },
// name: 'googleApi', credentials: [
// required: true, {
// } name: 'googleApi',
// ], required: true,
// webhooks: [ displayOptions: {
// { show: {
// name: 'default', authentication: [
// httpMethod: 'POST', 'serviceAccount',
// responseMode: 'onReceived', ],
// path: 'webhook', },
// }, },
// ], },
// properties: [ {
// { name: 'googleDriveOAuth2Api',
// displayName: 'Resource Id', required: true,
// name: 'resourceId', displayOptions: {
// type: 'string', show: {
// default: '', authentication: [
// required: true, 'oAuth2',
// placeholder: '', ],
// description: 'ID of the resource to watch, for example a file ID.', },
// }, },
// ], },
// }; ],
polling: true,
// // @ts-ignore (because of request) inputs: [],
// webhookMethods = { outputs: ['main'],
// default: { properties: [
// async checkExists(this: IHookFunctions): Promise<boolean> { {
// // const webhookData = this.getWorkflowStaticData('node'); displayName: 'Credential Type',
name: 'authentication',
// // if (webhookData.webhookId === undefined) { type: 'options',
// // // No webhook id is set so no webhook can exist options: [
// // return false; {
// // } name: 'Service Account',
value: 'serviceAccount',
// // // Webhook got created before so check if it still exists },
// // const owner = this.getNodeParameter('owner') as string; {
// // const repository = this.getNodeParameter('repository') as string; name: 'OAuth2',
// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; value: 'oAuth2',
},
// // try { ],
// // await githubApiRequest.call(this, 'GET', endpoint, {}); default: 'oAuth2',
// // } catch (error) { },
// // if (error.message.includes('[404]:')) { {
// // // Webhook does not exist displayName: 'Trigger On',
// // delete webhookData.webhookId; name: 'triggerOn',
// // delete webhookData.webhookEvents; type: 'options',
required: true,
// // return false; default: '',
// // } options: [
{
// // // Some error occured name: 'Changes to a Specific File',
// // throw e; value: 'specificFile',
// // } },
{
// // If it did not error then the webhook exists name: 'Changes Involving a Specific Folder',
// // return true; value: 'specificFolder',
// return false; },
// }, // {
// async create(this: IHookFunctions): Promise<boolean> { // name: 'Changes To Any File/Folder',
// const webhookUrl = this.getNodeWebhookUrl('default'); // value: 'anyFileFolder',
// },
// const resourceId = this.getNodeParameter('resourceId') as string; ],
description: '',
// const credentials = await this.getCredentials('googleApi'); },
{
// if (credentials === undefined) { displayName: 'File URL or ID',
// throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); name: 'fileToWatch',
// } type: 'string',
displayOptions: {
// const scopes = [ show: {
// 'https://www.googleapis.com/auth/drive', triggerOn: [
// 'https://www.googleapis.com/auth/drive.appdata', 'specificFile',
// 'https://www.googleapis.com/auth/drive.photos.readonly', ],
// ]; },
},
// const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes); default: '',
description: 'The address of this file when you view it in your browser (or just the ID contained within the URL)',
// const drive = google.drive({ required: true,
// version: 'v3', },
// auth: client, {
// }); displayName: 'Watch For',
name: 'event',
type: 'options',
// const accessToken = await client.getAccessToken(); displayOptions: {
// console.log('accessToken: '); show: {
// console.log(accessToken); triggerOn: [
'specificFile',
// const asdf = await drive.changes.getStartPageToken(); ],
// // console.log('asdf: '); },
// // console.log(asdf); },
required: true,
default: 'fileUpdated',
options: [
{
// const response = await drive.changes.watch({ name: 'File Updated',
// // value: 'fileUpdated',
// pageToken: asdf.data.startPageToken, },
// requestBody: { ],
// id: 'asdf-test-2', description: 'When to trigger this node',
// address: webhookUrl, },
// resourceId, {
// type: 'web_hook', displayName: 'Folder URL or ID',
// // page_token: '', name: 'folderToWatch',
// } type: 'string',
// }); displayOptions: {
show: {
// console.log('...response...CREATE'); triggerOn: [
// console.log(JSON.stringify(response, null, 2)); 'specificFolder',
],
},
},
default: '',
description: 'The address of this folder when you view it in your browser (or just the ID contained within the URL)',
// // const endpoint = `/repos/${owner}/${repository}/hooks`; required: true,
},
// // const body = { {
// // name: 'web', displayName: 'Watch For',
// // config: { name: 'event',
// // url: webhookUrl, type: 'options',
// // content_type: 'json', displayOptions: {
// // // secret: '...later...', show: {
// // insecure_ssl: '1', // '0' -> not allow inscure ssl | '1' -> allow insercure SSL triggerOn: [
// // }, 'specificFolder',
// // events, ],
// // active: true, },
// // }; },
required: true,
default: '',
// // let responseData; options: [
// // try { {
// // responseData = await githubApiRequest.call(this, 'POST', endpoint, body); name: 'File Created',
// // } catch (error) { value: 'fileCreated',
// // if (error.message.includes('[422]:')) { description: 'When a file is created in the watched folder',
// // throw new NodeOperationError(this.getNode(), 'A webhook with the identical URL exists already. Please delete it manually on Github!'); },
// // } {
name: 'File Updated',
// // throw e; value: 'fileUpdated',
// // } description: 'When a file is updated in the watched folder',
},
// // if (responseData.id === undefined || responseData.active !== true) { {
// // // Required data is missing so was not successful name: 'Folder Created',
// // throw new NodeOperationError(this.getNode(), 'Github webhook creation response did not contain the expected data.'); value: 'folderCreated',
// // } description: 'When a folder is created in the watched folder',
},
// // const webhookData = this.getWorkflowStaticData('node'); {
// // webhookData.webhookId = responseData.id as string; name: 'Folder Updated',
// // webhookData.webhookEvents = responseData.events as string[]; value: 'folderUpdated',
description: 'When a folder is updated in the watched folder',
// return true; },
// }, {
// async delete(this: IHookFunctions): Promise<boolean> { name: 'Watch Folder Updated',
// const webhookUrl = this.getNodeWebhookUrl('default'); value: 'watchFolderUpdated',
description: 'When the watched folder itself is modified',
// const resourceId = this.getNodeParameter('resourceId') as string; },
],
// const credentials = await this.getCredentials('googleApi'); },
{
// if (credentials === undefined) { displayName: 'Changes within subfolders won\'t trigger this node',
// throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); name: 'asas',
// } type: 'notice',
displayOptions: {
// const scopes = [ show: {
// 'https://www.googleapis.com/auth/drive', triggerOn: [
// 'https://www.googleapis.com/auth/drive.appdata', 'specificFolder',
// 'https://www.googleapis.com/auth/drive.photos.readonly', ],
// ]; },
hide: {
// const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes); event: [
'watchFolderUpdated',
// const drive = google.drive({ ],
// version: 'v3', },
// auth: client, },
// }); default: '',
},
// // Remove channel {
// const response = await drive.channels.stop({ displayName: 'Drive To Watch',
// requestBody: { name: 'driveToWatch',
// id: 'asdf-test-2', type: 'options',
// address: webhookUrl, displayOptions: {
// resourceId, show: {
// type: 'web_hook', triggerOn: [
// } 'anyFileFolder',
// }); ],
},
},
// console.log('...response...DELETE'); typeOptions: {
// console.log(JSON.stringify(response, null, 2)); loadOptionsMethod: 'getDrives',
},
default: 'root',
required: true,
// // const webhookData = this.getWorkflowStaticData('node'); description: 'The drive to monitor',
},
// // if (webhookData.webhookId !== undefined) { {
// // const owner = this.getNodeParameter('owner') as string; displayName: 'Watch For',
// // const repository = this.getNodeParameter('repository') as string; name: 'event',
// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; type: 'options',
// // const body = {}; displayOptions: {
show: {
// // try { triggerOn: [
// // await githubApiRequest.call(this, 'DELETE', endpoint, body); 'anyFileFolder',
// // } catch (error) { ],
// // return false; },
// // } },
required: true,
// // // Remove from the static workflow data so that it is clear default: 'fileCreated',
// // // that no webhooks are registred anymore options: [
// // delete webhookData.webhookId; {
// // delete webhookData.webhookEvents; name: 'File Created',
// // } value: 'fileCreated',
description: 'When a file is created in the watched drive',
// return true; },
// }, {
// }, name: 'File Updated',
// }; value: 'fileUpdated',
description: 'When a file is updated in the watched drive',
},
{
// async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> { name: 'Folder Created',
// const bodyData = this.getBodyData(); value: 'folderCreated',
description: 'When a folder is created in the watched drive',
// console.log(''); },
// console.log(''); {
// console.log('GOT WEBHOOK CALL'); name: 'Folder Updated',
// console.log(JSON.stringify(bodyData, null, 2)); value: 'folderUpdated',
description: 'When a folder is updated in the watched drive',
},
],
// // Check if the webhook is only the ping from Github to confirm if it workshook_id description: 'When to trigger this node',
// if (bodyData.hook_id !== undefined && bodyData.action === undefined) { },
// // Is only the ping and not an actual webhook call. So return 'OK' {
// // but do not start the workflow. displayName: 'Options',
name: 'options',
// return { type: 'collection',
// webhookResponse: 'OK' displayOptions: {
// }; show: {
// } event: [
'fileCreated',
// // Is a regular webhoook call 'fileUpdated',
],
// // TODO: Add headers & requestPath },
// const returnData: IDataObject[] = []; hide: {
triggerOn: [
// returnData.push( 'specificFile',
// { ],
// body: bodyData, },
// headers: this.getHeaderData(), },
// query: this.getQueryData(), placeholder: 'Add Option',
// } default: {},
// ); options: [
{
// return { displayName: 'File Type',
// workflowData: [ name: 'fileType',
// this.helpers.returnJsonArray(returnData) type: 'options',
// ], options: [
// }; {
// } name: '[All]',
// } value: 'all',
},
{
name: 'Audio',
value: 'application/vnd.google-apps.audio',
},
{
name: 'Google Docs',
value: 'application/vnd.google-apps.document',
},
{
name: 'Google Drawings',
value: 'application/vnd.google-apps.drawing',
},
{
name: 'Google Slides',
value: 'application/vnd.google-apps.presentation',
},
{
name: 'Google Spreadsheets',
value: 'application/vnd.google-apps.spreadsheet',
},
{
name: 'Photos and Images',
value: 'application/vnd.google-apps.photo',
},
{
name: 'Videos',
value: 'application/vnd.google-apps.video',
},
],
default: 'all',
description: 'Triggers only when the file is this type',
},
],
},
],
};
methods = {
loadOptions: {
// Get all the calendars to display them to user so that he can
// select them easily
async getDrives(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const drives = await googleApiRequestAllItems.call(this, 'drives', 'GET', `/drive/v3/drives`);
returnData.push({
name: 'Root',
value: 'root',
});
for (const drive of drives) {
returnData.push({
name: drive.name,
value: drive.id,
});
}
return returnData;
},
},
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const triggerOn = this.getNodeParameter('triggerOn') as string;
const event = this.getNodeParameter('event') as string;
const webhookData = this.getWorkflowStaticData('node');
const options = this.getNodeParameter('options', {}) as IDataObject;
const qs: IDataObject = {};
const now = moment().utc().format();
const startDate = webhookData.lastTimeChecked as string || now;
const endDate = now;
const query = [
'trashed = false',
];
if (triggerOn === 'specificFolder' && event !== 'watchFolderUpdated') {
const folderToWatch = extractId(this.getNodeParameter('folderToWatch') as string);
query.push(`'${folderToWatch}' in parents`);
}
// if (triggerOn === 'anyFileFolder') {
// const driveToWatch = this.getNodeParameter('driveToWatch');
// query.push(`'${driveToWatch}' in parents`);
// }
if (event.startsWith('file')) {
query.push(`mimeType != 'application/vnd.google-apps.folder'`);
} else {
query.push(`mimeType = 'application/vnd.google-apps.folder'`);
}
if (options.fileType && options.fileType !== 'all') {
query.push(`mimeType = '${options.fileType}'`);
}
if (this.getMode() !== 'manual') {
if (event.includes('Created')) {
query.push(`createdTime > '${startDate}'`);
} else {
query.push(`modifiedTime > '${startDate}'`);
}
}
qs.q = query.join(' AND ');
qs.fields = 'nextPageToken, files(*)';
let files;
if (this.getMode() === 'manual') {
qs.pageSize = 1;
files = await googleApiRequest.call(this, 'GET', `/drive/v3/files`, {}, qs);
files = files.files;
} else {
files = await googleApiRequestAllItems.call(this, 'files', 'GET', `/drive/v3/files`, {}, qs);
}
if (triggerOn === 'specificFile' && this.getMode() !== 'manual') {
const fileToWatch = extractId(this.getNodeParameter('fileToWatch') as string);
files = files.filter((file: { id: string }) => file.id === fileToWatch);
}
if (triggerOn === 'specificFolder' && event === 'watchFolderUpdated' && this.getMode() !== 'manual') {
const folderToWatch = extractId(this.getNodeParameter('folderToWatch') as string);
files = files.filter((file: { id: string }) => file.id === folderToWatch);
}
webhookData.lastTimeChecked = endDate;
if (Array.isArray(files) && files.length) {
return [this.helpers.returnJsonArray(files)];
}
if (this.getMode() === 'manual') {
throw new NodeApiError(this.getNode(), { message: 'No data with the current filter could be found' });
}
return null;
}
}

View file

@ -419,6 +419,7 @@
"dist/nodes/Google/Contacts/GoogleContacts.node.js", "dist/nodes/Google/Contacts/GoogleContacts.node.js",
"dist/nodes/Google/Docs/GoogleDocs.node.js", "dist/nodes/Google/Docs/GoogleDocs.node.js",
"dist/nodes/Google/Drive/GoogleDrive.node.js", "dist/nodes/Google/Drive/GoogleDrive.node.js",
"dist/nodes/Google/Drive/GoogleDriveTrigger.node.js",
"dist/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.js", "dist/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.js",
"dist/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.js", "dist/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.js",
"dist/nodes/Google/Gmail/Gmail.node.js", "dist/nodes/Google/Gmail/Gmail.node.js",