diff --git a/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts index 726b533655..185e00da2b 100644 --- a/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts @@ -9,14 +9,17 @@ import { } from 'n8n-core'; import { - IDataObject, NodeApiError, NodeOperationError, + IDataObject, + IPollFunctions, + NodeApiError, + NodeOperationError, } 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, option: IDataObject = {}): Promise { // 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 { // tslint:disable-line:no-any const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string; let options: OptionsWithUri = { @@ -29,7 +32,9 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF uri: uri || `https://www.googleapis.com${resource}`, json: true, }; + options = Object.assign({}, options, option); + try { if (Object.keys(body).length === 0) { 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 { // tslint:disable-line:no-any +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; let responseData; - query.maxResults = 100; - query.pageSize = 100; + query.maxResults = query.maxResults || 100; + query.pageSize = query.pageSize || 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 && @@ -79,7 +83,7 @@ export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOp return returnData; } -function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise { +function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, credentials: IDataObject): Promise { //https://developers.google.com/identity/protocols/oauth2/service-account#httprest const scopes = [ @@ -125,3 +129,17 @@ function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoa 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; +} diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts b/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts index c917e64874..a735df0401 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts @@ -1,294 +1,438 @@ -// import { google } from 'googleapis'; - -// import { -// IHookFunctions, -// IWebhookFunctions, -// } from 'n8n-core'; - -// import { -// IDataObject, -// INodeTypeDescription, -// INodeType, -// IWebhookResponseData, -// NodeOperationError, -// } from 'n8n-workflow'; - -// import { getAuthenticationClient } from './GoogleApi'; - - -// export class GoogleDriveTrigger implements INodeType { -// description: INodeTypeDescription = { -// displayName: 'Google Drive Trigger', -// name: 'googleDriveTrigger', -// icon: 'file:googleDrive.png', -// group: ['trigger'], -// version: 1, -// subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', -// description: 'Starts the workflow when a file on Google Drive is changed', -// defaults: { -// name: 'Google Drive Trigger', -// color: '#3f87f2', -// }, -// inputs: [], -// outputs: ['main'], -// credentials: [ -// { -// name: 'googleApi', -// required: true, -// } -// ], -// webhooks: [ -// { -// name: 'default', -// httpMethod: 'POST', -// responseMode: 'onReceived', -// path: 'webhook', -// }, -// ], -// properties: [ -// { -// displayName: 'Resource Id', -// name: 'resourceId', -// type: 'string', -// default: '', -// required: true, -// placeholder: '', -// description: 'ID of the resource to watch, for example a file ID.', -// }, -// ], -// }; - -// // @ts-ignore (because of request) -// webhookMethods = { -// default: { -// async checkExists(this: IHookFunctions): Promise { -// // const webhookData = this.getWorkflowStaticData('node'); - -// // if (webhookData.webhookId === undefined) { -// // // No webhook id is set so no webhook can exist -// // return false; -// // } - -// // // Webhook got created before so check if it still exists -// // const owner = this.getNodeParameter('owner') as string; -// // const repository = this.getNodeParameter('repository') as string; -// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; - -// // try { -// // await githubApiRequest.call(this, 'GET', endpoint, {}); -// // } catch (error) { -// // if (error.message.includes('[404]:')) { -// // // Webhook does not exist -// // delete webhookData.webhookId; -// // delete webhookData.webhookEvents; - -// // return false; -// // } - -// // // Some error occured -// // throw e; -// // } - -// // If it did not error then the webhook exists -// // return true; -// return false; -// }, -// async create(this: IHookFunctions): Promise { -// const webhookUrl = this.getNodeWebhookUrl('default'); - -// const resourceId = this.getNodeParameter('resourceId') as string; - -// const credentials = await this.getCredentials('googleApi'); - -// if (credentials === undefined) { -// throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); -// } - -// const scopes = [ -// 'https://www.googleapis.com/auth/drive', -// 'https://www.googleapis.com/auth/drive.appdata', -// 'https://www.googleapis.com/auth/drive.photos.readonly', -// ]; - -// const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes); - -// const drive = google.drive({ -// version: 'v3', -// auth: client, -// }); - - -// const accessToken = await client.getAccessToken(); -// console.log('accessToken: '); -// console.log(accessToken); - -// const asdf = await drive.changes.getStartPageToken(); -// // console.log('asdf: '); -// // console.log(asdf); - - - - -// const response = await drive.changes.watch({ -// // -// pageToken: asdf.data.startPageToken, -// requestBody: { -// id: 'asdf-test-2', -// address: webhookUrl, -// resourceId, -// type: 'web_hook', -// // page_token: '', -// } -// }); - -// console.log('...response...CREATE'); -// console.log(JSON.stringify(response, null, 2)); - - - - - -// // const endpoint = `/repos/${owner}/${repository}/hooks`; - -// // const body = { -// // name: 'web', -// // config: { -// // url: webhookUrl, -// // content_type: 'json', -// // // secret: '...later...', -// // insecure_ssl: '1', // '0' -> not allow inscure ssl | '1' -> allow insercure SSL -// // }, -// // events, -// // active: true, -// // }; - - -// // let responseData; -// // try { -// // responseData = await githubApiRequest.call(this, 'POST', endpoint, body); -// // } catch (error) { -// // if (error.message.includes('[422]:')) { -// // throw new NodeOperationError(this.getNode(), 'A webhook with the identical URL exists already. Please delete it manually on Github!'); -// // } - -// // throw e; -// // } - -// // if (responseData.id === undefined || responseData.active !== true) { -// // // Required data is missing so was not successful -// // throw new NodeOperationError(this.getNode(), 'Github webhook creation response did not contain the expected data.'); -// // } - -// // const webhookData = this.getWorkflowStaticData('node'); -// // webhookData.webhookId = responseData.id as string; -// // webhookData.webhookEvents = responseData.events as string[]; - -// return true; -// }, -// async delete(this: IHookFunctions): Promise { -// const webhookUrl = this.getNodeWebhookUrl('default'); - -// const resourceId = this.getNodeParameter('resourceId') as string; - -// const credentials = await this.getCredentials('googleApi'); - -// if (credentials === undefined) { -// throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); -// } - -// const scopes = [ -// 'https://www.googleapis.com/auth/drive', -// 'https://www.googleapis.com/auth/drive.appdata', -// 'https://www.googleapis.com/auth/drive.photos.readonly', -// ]; - -// const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes); - -// const drive = google.drive({ -// version: 'v3', -// auth: client, -// }); - -// // Remove channel -// const response = await drive.channels.stop({ -// requestBody: { -// id: 'asdf-test-2', -// address: webhookUrl, -// resourceId, -// type: 'web_hook', -// } -// }); - - -// console.log('...response...DELETE'); -// console.log(JSON.stringify(response, null, 2)); - - - -// // const webhookData = this.getWorkflowStaticData('node'); - -// // if (webhookData.webhookId !== undefined) { -// // const owner = this.getNodeParameter('owner') as string; -// // const repository = this.getNodeParameter('repository') as string; -// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; -// // const body = {}; - -// // try { -// // await githubApiRequest.call(this, 'DELETE', endpoint, body); -// // } catch (error) { -// // return false; -// // } - -// // // Remove from the static workflow data so that it is clear -// // // that no webhooks are registred anymore -// // delete webhookData.webhookId; -// // delete webhookData.webhookEvents; -// // } - -// return true; -// }, -// }, -// }; - - - -// async webhook(this: IWebhookFunctions): Promise { -// const bodyData = this.getBodyData(); - -// console.log(''); -// console.log(''); -// console.log('GOT WEBHOOK CALL'); -// console.log(JSON.stringify(bodyData, null, 2)); - - - -// // Check if the webhook is only the ping from Github to confirm if it workshook_id -// 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. - -// return { -// webhookResponse: 'OK' -// }; -// } - -// // Is a regular webhoook call - -// // TODO: Add headers & requestPath -// const returnData: IDataObject[] = []; - -// returnData.push( -// { -// body: bodyData, -// headers: this.getHeaderData(), -// query: this.getQueryData(), -// } -// ); - -// return { -// workflowData: [ -// this.helpers.returnJsonArray(returnData) -// ], -// }; -// } -// } +import { + IPollFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeApiError, +} from 'n8n-workflow'; + +import { + extractId, + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import * as moment from 'moment'; + +export class GoogleDriveTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Drive Trigger', + name: 'googleDriveTrigger', + icon: 'file:googleDrive.svg', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Google Drive events occur', + subtitle: '={{$parameter["event"]}}', + defaults: { + name: 'Google Drive Trigger', + color: '#4285F4', + }, + credentials: [ + { + name: 'googleApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'serviceAccount', + ], + }, + }, + }, + { + name: 'googleDriveOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, + ], + polling: true, + inputs: [], + outputs: ['main'], + properties: [ + { + displayName: 'Credential Type', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Service Account', + value: 'serviceAccount', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'oAuth2', + }, + { + displayName: 'Trigger On', + name: 'triggerOn', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Changes to a Specific File', + value: 'specificFile', + }, + { + name: 'Changes Involving a Specific Folder', + value: 'specificFolder', + }, + // { + // name: 'Changes To Any File/Folder', + // value: 'anyFileFolder', + // }, + ], + description: '', + }, + { + displayName: 'File URL or ID', + name: 'fileToWatch', + type: 'string', + displayOptions: { + show: { + triggerOn: [ + 'specificFile', + ], + }, + }, + default: '', + description: 'The address of this file when you view it in your browser (or just the ID contained within the URL)', + required: true, + }, + { + displayName: 'Watch For', + name: 'event', + type: 'options', + displayOptions: { + show: { + triggerOn: [ + 'specificFile', + ], + }, + }, + required: true, + default: 'fileUpdated', + options: [ + { + name: 'File Updated', + value: 'fileUpdated', + }, + ], + description: 'When to trigger this node', + }, + { + displayName: 'Folder URL or ID', + name: 'folderToWatch', + type: 'string', + displayOptions: { + show: { + triggerOn: [ + 'specificFolder', + ], + }, + }, + default: '', + description: 'The address of this folder when you view it in your browser (or just the ID contained within the URL)', + required: true, + }, + { + displayName: 'Watch For', + name: 'event', + type: 'options', + displayOptions: { + show: { + triggerOn: [ + 'specificFolder', + ], + }, + }, + required: true, + default: '', + options: [ + { + name: 'File Created', + value: 'fileCreated', + description: 'When a file is created in the watched folder', + }, + { + name: 'File Updated', + value: 'fileUpdated', + description: 'When a file is updated in the watched folder', + }, + { + name: 'Folder Created', + value: 'folderCreated', + description: 'When a folder is created in the watched folder', + }, + { + name: 'Folder Updated', + value: 'folderUpdated', + description: 'When a folder is updated in the watched folder', + }, + { + name: 'Watch Folder Updated', + value: 'watchFolderUpdated', + description: 'When the watched folder itself is modified', + }, + ], + }, + { + displayName: 'Changes within subfolders won\'t trigger this node', + name: 'asas', + type: 'notice', + displayOptions: { + show: { + triggerOn: [ + 'specificFolder', + ], + }, + hide: { + event: [ + 'watchFolderUpdated', + ], + }, + }, + default: '', + }, + { + displayName: 'Drive To Watch', + name: 'driveToWatch', + type: 'options', + displayOptions: { + show: { + triggerOn: [ + 'anyFileFolder', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getDrives', + }, + default: 'root', + required: true, + description: 'The drive to monitor', + }, + { + displayName: 'Watch For', + name: 'event', + type: 'options', + displayOptions: { + show: { + triggerOn: [ + 'anyFileFolder', + ], + }, + }, + required: true, + default: 'fileCreated', + options: [ + { + name: 'File Created', + value: 'fileCreated', + description: 'When a file is created in the watched drive', + }, + { + name: 'File Updated', + value: 'fileUpdated', + description: 'When a file is updated in the watched drive', + }, + { + name: 'Folder Created', + value: 'folderCreated', + description: 'When a folder is created in the watched drive', + }, + { + name: 'Folder Updated', + value: 'folderUpdated', + description: 'When a folder is updated in the watched drive', + }, + ], + description: 'When to trigger this node', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + event: [ + 'fileCreated', + 'fileUpdated', + ], + }, + hide: { + triggerOn: [ + 'specificFile', + ], + }, + }, + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'File Type', + name: 'fileType', + 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 { + 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 { + 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; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b5c91861f4..6233219a5f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -419,6 +419,7 @@ "dist/nodes/Google/Contacts/GoogleContacts.node.js", "dist/nodes/Google/Docs/GoogleDocs.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/RealtimeDatabase/RealtimeDatabase.node.js", "dist/nodes/Google/Gmail/Gmail.node.js",