diff --git a/packages/nodes-base/nodes/Google/Chat/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Chat/GenericFunctions.ts new file mode 100644 index 0000000000000..0e971e4b53877 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/GenericFunctions.ts @@ -0,0 +1,193 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialTestFunctions, + IDataObject, + INodeProperties, + 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, noCredentials = false, encoding?: null | undefined): Promise { // tslint:disable-line:no-any + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://chat.googleapis.com${resource}`, + json: true, + }; + + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (encoding === null) { + options.encoding = null; + } + + let responseData: IDataObject | undefined; + try { + if (noCredentials) { + //@ts-ignore + responseData = await this.helpers.request(options); + } else{ + const credentials = await this.getCredentials('googleApi'); + + if (credentials === undefined) { + throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); + } + + const { access_token } = await getAccessToken.call(this, credentials as ICredentialDataDecryptedObject); + options.headers!.Authorization = `Bearer ${access_token}`; + + //@ts-ignore + responseData = await this.helpers.request(options); + } + } catch (error) { + if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') { + error.statusCode = '401'; + } + throw new NodeApiError(this.getNode(), error); + } + if(Object.keys(responseData as IDataObject).length !== 0) { + return responseData; + } + else { + return { 'success': true }; + } +} + +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.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 && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} + +export function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | ICredentialTestFunctions, credentials: ICredentialDataDecryptedObject): Promise { + //https://developers.google.com/identity/protocols/oauth2/service-account#httprest + + const scopes = [ + 'https://www.googleapis.com/auth/chat.bot', + ]; + + const now = moment().unix(); + + const signature = jwt.sign( + { + 'iss': credentials.email as string, + 'sub': credentials.delegatedEmail || 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); +} + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} + +export function getPagingParameters(resource: string, operation = 'getAll') { + const pagingParameters: INodeProperties [] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + resource, + ], + operation: [ + operation, + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + resource, + ], + operation: [ + operation, + ], + returnAll: [ + false, + ], + }, + }, + default: 100, + description: 'The limit of records to return. The value is capped at 1000. Server may return fewer results than requested. If unspecified, server will default to 100.', + }, + ]; + return pagingParameters; +} diff --git a/packages/nodes-base/nodes/Google/Chat/GoogleChat.node.json b/packages/nodes-base/nodes/Google/Chat/GoogleChat.node.json new file mode 100644 index 0000000000000..f0940eff097d9 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/GoogleChat.node.json @@ -0,0 +1,27 @@ +{ + "node": "n8n-nodes-base.googleChat", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/google" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleChat/" + } + ], + "generic": [ + { + "label": "15 Google apps you can combine and automate to increase productivity", + "icon": "💡", + "url": "https://n8n.io/blog/automate-google-apps-for-productivity/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Google/Chat/GoogleChat.node.ts b/packages/nodes-base/nodes/Google/Chat/GoogleChat.node.ts new file mode 100644 index 0000000000000..7e15228a3ecad --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/GoogleChat.node.ts @@ -0,0 +1,627 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeCredentialTestResult, + NodeOperationError, +} from 'n8n-workflow'; + +import { + IMessage, + IMessageUi, +} from './MessageInterface'; + +import { + attachmentFields, + attachmentOperations, + incomingWebhookFields, + incomingWebhookOperations, + mediaFields, + mediaOperations, + memberFields, + memberOperations, + messageFields, + messageOperations, + spaceFields, + spaceOperations +} from './descriptions'; + +import { + googleApiRequest, + googleApiRequestAllItems, + validateJSON, +} from './GenericFunctions'; + +import * as moment from 'moment-timezone'; + +import * as jwt from 'jsonwebtoken'; + +import { OptionsWithUri } from 'request'; + +export class GoogleChat implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Chat', + name: 'googleChat', + icon: 'file:googleChat.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Google Chat API', + defaults: { + name: 'Google Chat', + color: '#0aa55c', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleApi', + required: false, // not required, webhooks do not need credentials + testedBy: 'testGoogleTokenAuth', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + required: true, + noDataExpression: true, + type: 'options', + options: [ + { + name: 'Attachment', + value: 'attachment', + }, + { + name: 'Incoming Webhook', + value: 'incomingWebhook', + }, + { + name: 'Media', + value: 'media', + }, + { + name: 'Member', + value: 'member', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Space', + value: 'space', + }, + ], + default: 'message', + description: 'The resource to operate on', + }, + ...attachmentOperations, + ...attachmentFields, + ...incomingWebhookOperations, + ...incomingWebhookFields, + ...mediaOperations, + ...mediaFields, + ...memberOperations, + ...memberFields, + ...messageOperations, + ...messageFields, + ...spaceOperations, + ...spaceFields, + ], + }; + + methods = { + loadOptions: { + // Get all the spaces to display them to user so that he can + // select them easily + async getSpaces( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const spaces = await googleApiRequestAllItems.call( + this, + 'spaces', + 'GET', + `/v1/spaces`, + ); + for (const space of spaces) { + const spaceName = space.name; + const spaceId = space.name; + returnData.push({ + name: spaceName, + value: spaceId, + }); + } + return returnData; + }, + }, + credentialTest: { + async testGoogleTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { + + const scopes = [ + 'https://www.googleapis.com/auth/chat.bot', + ]; + + const now = moment().unix(); + + try { + const signature = jwt.sign( + { + 'iss': credential.data!.email as string, + 'sub': credential.data!.delegatedEmail || credential.data!.email as string, + 'scope': scopes.join(' '), + 'aud': `https://oauth2.googleapis.com/token`, + 'iat': now, + 'exp': now, + }, + credential.data!.privateKey as string, + { + algorithm: 'RS256', + header: { + 'kid': credential.data!.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, + }; + + const response = await this.helpers.request(options); + + if (!response.access_token) { + return { + status: 'Error', + message: JSON.stringify(response), + }; + } + } catch (err) { + return { + status: 'Error', + message: `${err.message}`, + }; + } + + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + try { + if (resource === 'media') { + if (operation === 'download') { + // ---------------------------------------- + // media: download + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/media/download + + const resourceName = this.getNodeParameter('resourceName', i) as string; + + const endpoint = `/v1/media/${resourceName}?alt=media`; + + // Return the data as a buffer + const encoding = null; + + responseData = await googleApiRequest.call( + this, + 'GET', + endpoint, + undefined, + undefined, + undefined, + undefined, + encoding, + ); + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, items[i].binary); + } + + items[i] = newItem; + + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData, endpoint); + + } + + } else if (resource === 'space') { + if (operation === 'get') { + + // ---------------------------------------- + // space: get + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces/get + + const spaceName = this.getNodeParameter('spaceName', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${spaceName}`, + ); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // space: getAll + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces/list + + const returnAll = this.getNodeParameter('returnAll', 0) as IDataObject; + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'spaces', + 'GET', + `/v1/spaces`, + ); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.pageSize = limit >>> 0; // convert to an unsigned 32-bit integer + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/spaces`, + undefined, + qs, + ); + responseData = responseData.spaces; + } + } + } else if (resource === 'member') { + if (operation === 'get') { + + // ---------------------------------------- + // member: get + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.members/get + + const memberName = this.getNodeParameter('memberName', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${memberName}`, + ); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // member: getAll + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.members/list + + const spaceName = this.getNodeParameter('spaceName', i) as string; + + const returnAll = this.getNodeParameter('returnAll', 0) as IDataObject; + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'memberships', + 'GET', + `/v1/${spaceName}/members`, + undefined, + qs, + ); + + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.pageSize = limit >>> 0; // convert to an unsigned 32-bit integer + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${spaceName}/members`, + undefined, + qs, + ); + responseData = responseData.memberships; + } + + } + } else if (resource === 'message') { + if (operation === 'create') { + + // ---------------------------------------- + // message: create + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages/create + + const spaceName = this.getNodeParameter('spaceName', i) as string; + + // get additional fields for threadKey and requestId + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.threadKey) { + qs.threadKey = additionalFields.threadKey; + } + if (additionalFields.requestId) { + qs.requestId = additionalFields.requestId; + } + + let message: IMessage = {}; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + if (jsonParameters) { + const messageJson = this.getNodeParameter('messageJson', i); + + if (messageJson instanceof Object) { + // if it is an object + message = messageJson as IMessage; + } else { + // if it is a string + if (validateJSON(messageJson as string) !== undefined) { + message = JSON.parse(messageJson as string) as IMessage; + } else { + throw new NodeOperationError(this.getNode(), 'Message (JSON) must be a valid json'); + } + } + + } else { + const messageUi = this.getNodeParameter('messageUi', i) as IMessageUi; + if (messageUi.text && messageUi.text !== '') { + message.text = messageUi.text; + } else { + throw new NodeOperationError(this.getNode(), 'Message Text must be provided.'); + } + // // TODO: get cards from the UI + // if (messageUi?.cards?.metadataValues && messageUi?.cards?.metadataValues.length !== 0) { + // const cards = messageUi.cards.metadataValues as IDataObject[]; // TODO: map cards to messageUi.cards.metadataValues + // message.cards = cards; + // } + } + + const body: IDataObject = {}; + Object.assign(body, message); + + responseData = await googleApiRequest.call( + this, + 'POST', + `/v1/${spaceName}/messages`, + body, + qs, + ); + + } else if (operation === 'delete') { + + // ---------------------------------------- + // message: delete + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages/delete + + const messageName = this.getNodeParameter('messageName', i) as string; + + responseData = await googleApiRequest.call( + this, + 'DELETE', + `/v1/${messageName}`, + ); + + } else if (operation === 'get') { + + // ---------------------------------------- + // message: get + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages/get + + const messageName = this.getNodeParameter('messageName', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${messageName}`, + ); + + } else if (operation === 'update') { + + // ---------------------------------------- + // message: update + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages/update + + const messageName = this.getNodeParameter('messageName', i) as string; + + let message: IMessage = {}; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + if (jsonParameters) { + const updateFieldsJson = this.getNodeParameter('updateFieldsJson', i); + + if (updateFieldsJson instanceof Object) { + // if it is an object + message = updateFieldsJson as IMessage; + } else { + // if it is a string + if (validateJSON(updateFieldsJson as string) !== undefined) { + message = JSON.parse(updateFieldsJson as string) as IMessage; + } else { + throw new NodeOperationError(this.getNode(), 'Update Fields (JSON) must be a valid json'); + } + } + + } else { + const updateFieldsUi = this.getNodeParameter('updateFieldsUi', i) as IDataObject; + if (updateFieldsUi.text) { + message.text = updateFieldsUi.text as string; + } + // // TODO: get cards from the UI + // if (updateFieldsUi.cards) { + // message.cards = updateFieldsUi.cards as IDataObject[]; + // } + } + + const body: IDataObject = {}; + Object.assign(body, message); + + // get update mask + let updateMask = ''; + if (message.text) { + updateMask += 'text,'; + } + if (message.cards) { + updateMask += 'cards,'; + } + updateMask = updateMask.slice(0, -1); // remove trailing comma + qs.updateMask = updateMask; + + responseData = await googleApiRequest.call( + this, + 'PUT', + `/v1/${messageName}`, + body, + qs, + ); + } + + } else if (resource === 'attachment') { + + if (operation === 'get') { + // ---------------------------------------- + // attachment: get + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages.attachments/get + + const attachmentName = this.getNodeParameter('attachmentName', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${attachmentName}`, + ); + } + } else if (resource === 'incomingWebhook') { + if (operation === 'create') { + + // ---------------------------------------- + // incomingWebhook: create + // ---------------------------------------- + + // https://developers.google.com/chat/how-tos/webhooks + + const uri = this.getNodeParameter('incomingWebhookUrl', i) as string; + + // get additional fields for threadKey + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.threadKey) { + qs.threadKey = additionalFields.threadKey; + } + + let message: IMessage = {}; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + if (jsonParameters) { + const messageJson = this.getNodeParameter('messageJson', i); + + if (messageJson instanceof Object) { + // if it is an object + message = messageJson as IMessage; + } else { + // if it is a string + if (validateJSON(messageJson as string) !== undefined) { + message = JSON.parse(messageJson as string) as IMessage; + } else { + throw new NodeOperationError(this.getNode(), 'Message (JSON) must be a valid json'); + } + } + + } else { + const messageUi = this.getNodeParameter('messageUi', i) as IMessageUi; + if (messageUi.text && messageUi.text !== '') { + message.text = messageUi.text; + } else { + throw new NodeOperationError(this.getNode(), 'Message Text must be provided.'); + } + } + + const body: IDataObject = {}; + Object.assign(body, message); + + responseData = await googleApiRequest.call( + this, + 'POST', + '', + body, + qs, + uri, + true, + ); + } + + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } catch (error) { + if (this.continueOnFail()) { + // Return the actual reason as error + if (operation === 'download') { + items[i].json = { error: error.message }; + } else { + returnData.push({ error: error.message }); + } + continue; + } + throw error; + } + } + + if (operation === 'download') { + // For file downloads the files get attached to the existing items + return this.prepareOutputData(items); + } else { + // For all other ones does the output get replaced + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts b/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts new file mode 100644 index 0000000000000..a2057a911a92c --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts @@ -0,0 +1,108 @@ +import { IDataObject } from 'n8n-workflow'; + +export interface IMessage { + name?: string; + sender?: IUser; + createTime?: string; + text?: string; + cards?: IDataObject[]; + previewText?: string; + annotations?: IDataObject[]; + thread?: IDataObject[]; + space?: IDataObject; + fallbackText?: string; + actionResponse?: IDataObject; + argumentText?: string; + slashCommand?: IDataObject; + attachment?: IDataObject[]; +} + +export interface IMessageUi { + text?: string; + cards?: { + metadata: IDataObject[]; + }; +} + +export interface IUser { + name?: string; + displayName?: string; + domainId?: string; + type?: Type; + isAnonymous?: boolean; +} +enum Type { + 'TYPE_UNSPECIFIED', + 'HUMAN', + 'BOT', +} + +// // TODO: define other interfaces +// +// export interface IMessage {s +// name?: string; +// sender?: IUser; +// createTime?: string; +// text?: string; +// cards?: ICard[]; +// previewText?: string; +// annotations?: IAnnotation[]; +// thread?: IThread[]; +// space?: ISpace; +// fallbackText?: string; +// actionResponse?: IActionResponse; +// argumentText?: string; +// slashCommand?: ISlashCommand; +// attachment?: IAttachment[]; +// } +// +// export interface ICard { +// header?: ICardHeader; +// sections?: ISection[]; +// cardActions?: ICardAction[]; +// name?: string; +// } +// +// export interface ICardHeader { +// title: string; +// subtitle: string; +// imageStyle: ImageStyleType; +// imageUrl: string; +// } +// enum ImageStyleType { +// 'IMAGE_STYLE_UNSPECIFIED', +// 'IMAGE', +// 'AVATAR', +// } +// +// export interface ISection { +// +// } +// +// export interface ICardAction { +// +// } +// +// export interface IAnnotation { +// +// } +// +// export interface IThread { +// +// } +// +// export interface ISpace { +// +// } +// +// export interface IActionResponse { +// +// } +// +// export interface ISlashCommand { +// +// } +// +// export interface IAttachment { +// // attachments are not available for bots +// } diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/AttachmentDescription.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/AttachmentDescription.ts new file mode 100644 index 0000000000000..3d5e1797bd824 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/AttachmentDescription.ts @@ -0,0 +1,51 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const attachmentOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'attachment', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Gets the metadata of a message attachment. The attachment data is fetched using the media API.', + }, + ], + default: 'get', + }, +]; + +export const attachmentFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* attachments:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment Name', + name: 'attachmentName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Resource name of the attachment, in the form "spaces/*/messages/*/attachments/*"', + }, +]; diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/IncomingWebhookDescription.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/IncomingWebhookDescription.ts new file mode 100644 index 0000000000000..b25871da6afa3 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/IncomingWebhookDescription.ts @@ -0,0 +1,186 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const incomingWebhookOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Creates a message through incoming webhook (no chat bot needed)', + }, + ], + default: 'create', + }, +]; + + +export const incomingWebhookFields: INodeProperties[] = [ + + /* -------------------------------------------------------------------------- */ + /* incomingWebhook:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'See Google Chat Guide To Webhooks', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + }, + { + displayName: 'Incoming Webhook URL', + name: 'incomingWebhookUrl', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'URL for the incoming webhook', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + operation: [ + 'create', + ], + }, + }, + default: false, + description: 'Whether to pass the message object as JSON', + }, + { + displayName: 'Message', + name: 'messageUi', + type: 'collection', + required: true, + placeholder: 'Add Options', + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + default: {'text': ''}, + description: 'The message object', + options: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The message text', + }, + ], + }, + { + displayName: 'See Google Chat Guide To Creating Messages', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + default: '', + }, + { + displayName: 'Message (JSON)', + name: 'messageJson', + type: 'json', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + default: '', + description: 'Message input as JSON Object or JSON String', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'incomingWebhook', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Thread Key', + name: 'threadKey', + type: 'string', + default: '', + description: 'Thread identifier which groups messages into a single thread. Has no effect if thread field, corresponding to an existing thread, is set in message. Example: spaces/AAAAMpdlehY/threads/MZ8fXhZXGkk.', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts new file mode 100644 index 0000000000000..8ac67e6a3e2d5 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts @@ -0,0 +1,69 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const mediaOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'media', + ], + }, + }, + options: [ + { + name: 'Download', + value: 'download', + description: 'Download media', + }, + ], + default: 'download', + }, +]; + +export const mediaFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* media:download */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Resource Name', + name: 'resourceName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'media', + ], + operation: [ + 'download', + ], + }, + }, + default: '', + description: 'Name of the media that is being downloaded', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + resource: [ + 'media', + ], + operation: [ + 'download', + ], + }, + }, + description: 'Name of the binary property to which to write the data of the read file', + }, +]; diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/MemberDescription.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/MemberDescription.ts new file mode 100644 index 0000000000000..301868a51822b --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/MemberDescription.ts @@ -0,0 +1,89 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + getPagingParameters +} from '../GenericFunctions'; + +export const memberOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'member', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Returns a membership', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Lists human memberships in a space', + }, + ], + default: 'get', + }, +]; + + +export const memberFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* member:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Member Name', + name: 'memberName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Member to be retrieved in the form "spaces/*/members/*"', + }, + + /* -------------------------------------------------------------------------- */ + /* member:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Space Name', + name: 'spaceName', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSpaces', + }, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + default: [], + description: 'The name of the space for which to retrieve members, in the form "spaces/*"', + }, + + ...getPagingParameters('member'), +]; + diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/MessageDescription.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/MessageDescription.ts new file mode 100644 index 0000000000000..bbe7bb51f9245 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/MessageDescription.ts @@ -0,0 +1,445 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const messageOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Creates a message', + }, + { + name: 'Delete', + value: 'delete', + description: 'Deletes a message', + }, + { + name: 'Get', + value: 'get', + description: 'Returns a message', + }, + { + name: 'Update', + value: 'update', + description: 'Updates a message', + }, + ], + default: 'create', + }, +]; + +export const messageFields: INodeProperties[] = [ + + /* -------------------------------------------------------------------------- */ + /* message:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Space Name', + name: 'spaceName', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSpaces', + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Space resource name, in the form "spaces/*". Example: spaces/AAAAMpdlehY.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: false, + description: 'Whether to pass the message object as JSON', + }, + { + displayName: 'Message', + name: 'messageUi', + type: 'collection', + required: true, + placeholder: 'Add Options', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + default: {'text': ''}, + description: 'The message object', + options: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The message text', + }, + // { // TODO: get cards from the UI (check the Slack node, specifically the blocks parameter under message: post) + // displayName: 'Cards', + // name: 'cards', + // placeholder: 'Add Cards', + // type: 'fixedCollection', + // default: '', + // typeOptions: { + // multipleValues: true, + // }, + // description: 'Rich, formatted and interactive cards that can be used to display UI elements such as: formatted texts, buttons, clickable images', + // options: [ + // { + // name: 'metadataValues', + // displayName: 'Metadata', + // values: [ + // { + // displayName: 'Name', + // name: 'name', + // type: 'string', + // default: '', + // description: 'Name of the card', + // }, + // { + // displayName: 'Header', + // name: 'header', + // type: 'json', + // default: '', + // description: 'Header of the card', + // }, + // { + // displayName: 'Sections', + // name: 'sections', + // type: 'json', + // default: '', + // description: 'Sections of the card', + // }, + // { + // displayName: 'Actions', + // name: 'cardActions', + // type: 'json', + // default: '', + // description: 'Actions of the card', + // }, + // ], + // }, + // ], + // }, + ], + }, + { + displayName: 'See Google Chat Guide To Creating Messages', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + default: '', + }, + { + displayName: 'Message (JSON)', + name: 'messageJson', + type: 'json', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + default: '', + description: 'Message input as JSON Object or JSON String', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Thread Key', + name: 'threadKey', + type: 'string', + default: '', + description: 'Thread identifier which groups messages into a single thread. Has no effect if thread field, corresponding to an existing thread, is set in message. Example: spaces/AAAAMpdlehY/threads/MZ8fXhZXGkk.', + }, + { + displayName: 'Request ID', + name: 'requestId', + type: 'string', + default: '', + description: 'A unique request ID for this message. If a message has already been created in the space with this request ID, the subsequent request will return the existing message and no new message will be created.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* messages:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Message Name', + name: 'messageName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'Resource name of the message to be deleted, in the form "spaces/*/messages/*"', + }, + + /* -------------------------------------------------------------------------- */ + /* message:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Message Name', + name: 'messageName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Resource name of the message to be deleted, in the form "spaces/*/messages/*"', + }, + + /* -------------------------------------------------------------------------- */ + /* message:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Message Name', + name: 'messageName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Resource name of the message to be retrieved, in the form "spaces/*/messages/*"', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + default: false, + description: 'Whether to pass the update fields object as JSON', + }, + { + displayName: 'Update Fields', + name: 'updateFieldsUi', + type: 'collection', + required: true, + placeholder: 'Add Options', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + jsonParameters: [ + false, + ], + }, + }, + default: {'text': ''}, + description: 'The message object', + options: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The message text', + }, + // { // TODO: get cards from the UI (check the Slack node, specifically the blocks parameter under message: post) + // displayName: 'Cards', + // name: 'cards', + // placeholder: 'Add Cards', + // type: 'fixedCollection', + // default: '', + // typeOptions: { + // multipleValues: true, + // }, + // description: 'Rich, formatted and interactive cards that can be used to display UI elements such as: formatted texts, buttons, clickable images', + // options: [ + // { + // name: 'metadataValues', + // displayName: 'Metadata', + // values: [ + // { + // displayName: 'Name', + // name: 'name', + // type: 'string', + // default: '', + // description: 'Name of the card', + // }, + // { + // displayName: 'Header', + // name: 'header', + // type: 'json', + // default: '', + // description: 'Header of the card', + // }, + // { + // displayName: 'Sections', + // name: 'sections', + // type: 'json', + // default: '', + // description: 'Sections of the card', + // }, + // { + // displayName: 'Actions', + // name: 'cardActions', + // type: 'json', + // default: '', + // description: 'Actions of the card', + // }, + // ], + // }, + // ], + // }, + ], + }, + { + displayName: 'See Google Chat Guide To Creating Messages', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + default: '', + }, + { + displayName: 'Update Fields (JSON)', + name: 'updateFieldsJson', + type: 'json', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + default: '', + description: 'Message input as JSON Object or JSON String', + }, +]; diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/SpaceDescription.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/SpaceDescription.ts new file mode 100644 index 0000000000000..0f05d2e256010 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/SpaceDescription.ts @@ -0,0 +1,69 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + getPagingParameters +} from '../GenericFunctions'; + +export const spaceOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + noDataExpression: true, + type: 'options', + displayOptions: { + show: { + resource: [ + 'space', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Returns a space', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Lists spaces the caller is a member of', + }, + ], + default: 'get', + }, +]; + +export const spaceFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* space:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Space Name', + name: 'spaceName', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSpaces', + }, + displayOptions: { + show: { + resource: [ + 'space', + ], + operation: [ + 'get', + ], + }, + }, + default: [], + description: 'Resource name of the space, in the form "spaces/*"', + }, + + /* -------------------------------------------------------------------------- */ + /* space:getAll */ + /* -------------------------------------------------------------------------- */ + + ...getPagingParameters('space'), +]; diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/index.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/index.ts new file mode 100644 index 0000000000000..f90a65f79103b --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/index.ts @@ -0,0 +1,6 @@ +export * from './MediaDescription'; +export * from './SpaceDescription'; +export * from './MemberDescription'; +export * from './MessageDescription'; +export * from './AttachmentDescription'; +export * from './IncomingWebhookDescription'; diff --git a/packages/nodes-base/nodes/Google/Chat/googleChat.svg b/packages/nodes-base/nodes/Google/Chat/googleChat.svg new file mode 100644 index 0000000000000..9799f878d6cf3 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/googleChat.svg @@ -0,0 +1 @@ +google-chat \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 2da78b154afc8..e5e1190743ddc 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -423,6 +423,7 @@ "dist/nodes/Google/Books/GoogleBooks.node.js", "dist/nodes/Google/Calendar/GoogleCalendar.node.js", "dist/nodes/Google/Calendar/GoogleCalendarTrigger.node.js", + "dist/nodes/Google/Chat/GoogleChat.node.js", "dist/nodes/Google/CloudNaturalLanguage/GoogleCloudNaturalLanguage.node.js", "dist/nodes/Google/Contacts/GoogleContacts.node.js", "dist/nodes/Google/Docs/GoogleDocs.node.js",