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..393d48a333241 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/GenericFunctions.ts @@ -0,0 +1,142 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialTestFunctions, + IDataObject, + 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; + } + + try { + if (noCredentials) { + //@ts-ignore + return 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 + return 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); + } +} + +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; +} 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..022b3b467ae49 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/GoogleChat.node.ts @@ -0,0 +1,462 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + IMessage, + IMessageUi, +} from './MessageInterface'; + +import { + attachmentFields, + attachmentOperations, + mediaFields, + mediaOperations, + memberFields, + memberOperations, + messageFields, + messageOperations, + spaceFields, + spaceOperations +} from './descriptions'; + +import { + googleApiRequest, + googleApiRequestAllItems, + validateJSON, +} from './GenericFunctions'; + +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 + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Media', + value: 'media', + }, + { + name: 'Space', + value: 'space', + }, + { + name: 'Member', + value: 'member', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Attachment', + value: 'attachment', + }, + ], + default: 'message', + description: 'The resource to operate on.', + }, + ...mediaOperations, + ...mediaFields, + ...spaceOperations, + ...spaceFields, + ...memberOperations, + ...memberFields, + ...messageOperations, + ...messageFields, + ...attachmentOperations, + ...attachmentFields, + ], + }; + + 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; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/media/${resourceName}?alt=media`, + ); + } + + } else if (resource === 'space') { + if (operation === 'get') { + + // ---------------------------------------- + // space: get + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces/get + + const name = this.getNodeParameter('name', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${name}`, + ); + + } 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 additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + qs.pageSize = additionalFields.pageSize as number >>> 0; // convert to an unsigned 32-bit integer + qs.pageToken = additionalFields.pageToken; + + 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 name = this.getNodeParameter('name', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${name}`, + ); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // member: getAll + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.members/list + + const parentName = this.getNodeParameter('parentName', i) as string; + + const returnAll = this.getNodeParameter('returnAll', 0) as IDataObject; + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'memberships', + 'GET', + `/v1/${parentName}/members`, + undefined, + qs, + ); + + } else { + // get additional fields input for pageSize and pageToken + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + qs.pageSize = additionalFields.pageSize as number >>> 0; // convert to an unsigned 32-bit integer + qs.pageToken = additionalFields.pageToken; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${parentName}/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 parentName = this.getNodeParameter('parentName', i) as string; + + const threadKey = this.getNodeParameter('threadKey', i) as string; + if (threadKey && threadKey !== '') { + qs.threadKey = threadKey; + } + const requestId = this.getNodeParameter('requestId', i) as string; + if (requestId && requestId !== '') { + qs.requestId = requestId; + } + + let message: IMessage = {}; + const jsonParameterMessage = this.getNodeParameter('jsonParameterMessage', i) as boolean; + if (jsonParameterMessage) { + const jsonStr = this.getNodeParameter('messageJson', i) as string; + + if (validateJSON(jsonStr) !== undefined) { + message = JSON.parse(jsonStr) 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/${parentName}/messages`, + body, + qs, + ); + + } else if (operation === 'delete') { + + // ---------------------------------------- + // message: delete + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages/delete + + const name = this.getNodeParameter('name', i) as string; + + responseData = await googleApiRequest.call( + this, + 'DELETE', + `/v1/${name}`, + ); + + } else if (operation === 'get') { + + // ---------------------------------------- + // message: get + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages/get + + const name = this.getNodeParameter('name', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${name}`, + ); + + } else if (operation === 'update') { + + // ---------------------------------------- + // message: update + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages/update + + const name = this.getNodeParameter('name', i) as string; + + const updateMaskOptions = this.getNodeParameter('updateMask', i) as string[]; + if (updateMaskOptions.length !== 0) { + let updateMask = ''; + for (const option of updateMaskOptions) { + updateMask += option + ','; + } + updateMask = updateMask.slice(0, -1); // remove trailing comma + qs.updateMask = updateMask; + } else { + throw new NodeOperationError(this.getNode(), 'Update Mask must not be empty.'); + } + + let updateOptions: IMessage = {}; + const jsonParameterUpdateOptions = this.getNodeParameter('jsonParameterUpdateOptions', i) as boolean; + if (jsonParameterUpdateOptions) { + const jsonStr = this.getNodeParameter('updateOptionsJson', i) as string; + if (validateJSON(jsonStr) !== undefined) { + updateOptions = JSON.parse(jsonStr) as IDataObject; + } else { + throw new NodeOperationError(this.getNode(), 'Update Options (JSON) must be a valid json'); + } + } else { + if (updateMaskOptions.includes('text')) { + const text = this.getNodeParameter('textUi', i) as string; + if (text !== '') { + updateOptions.text = text; + } else { + throw new NodeOperationError(this.getNode(), 'Text input must be provided.'); + } + } + // if (updateMaskOptions.includes('cards')) { + // // todo: get cards from the ui + // const cardsUi = this.getNodeParameter('cardsUi.metadataValues', i) as IDataObject[]; + // if (cardsUi.length !== 0) { + // updateOptions.cards = cardsUi as IDataObject[]; // todo: map cardsUi to cards[] + // } + // } + } + + const body: IDataObject = {}; + Object.assign(body, updateOptions); + + responseData = await googleApiRequest.call( + this, + 'PUT', + `/v1/${name}`, + body, + qs, + ); + } else if (operation === 'webhook') { + + // ---------------------------------------- + // message: webhook + // ---------------------------------------- + + // https://developers.google.com/chat/how-tos/webhooks + + const uri = this.getNodeParameter('webhookUrl', i) as string; + + const threadKey = this.getNodeParameter('threadKey', i) as string; + if (threadKey && threadKey !== '') { + qs.threadKey = threadKey; + } + const requestId = this.getNodeParameter('requestId', i) as string; + if (requestId && requestId !== '') { + qs.requestId = requestId; + } + + let message: IMessage = {}; + const jsonParameterMessage = this.getNodeParameter('jsonParameterMessage', i) as boolean; + if (jsonParameterMessage) { + const jsonStr = this.getNodeParameter('messageJson', i) as string; + if (validateJSON(jsonStr) !== undefined) { + message = JSON.parse(jsonStr) 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, + ); + } + + } else if (resource === 'attachment') { + + if (operation === 'get') { + // ---------------------------------------- + // attachment: get + // ---------------------------------------- + + // https://developers.google.com/chat/reference/rest/v1/spaces.messages.attachments/get + + const name = this.getNodeParameter('name', i) as string; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/v1/${name}`, + ); + } + } + 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 + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + 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..e57ba0c2a6772 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts @@ -0,0 +1,101 @@ +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 { +// 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 { +// // actionLabel?: string, +// // onClick?: IOnClick, +// } +// +// export interface ISection { +// +// } +// +// export interface ICardAction { +// +// } +// +// export interface IAnnotation { +// +// } +// +// export interface IThread { +// +// } +// +// export interface ISpace { +// +// } +// +// export interface IActionResponse { +// +// } +// +// export interface ISlashCommand { +// +// } +// +// export interface IAttachment { +// +// } 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..82756002895b5 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/AttachmentDescription.ts @@ -0,0 +1,52 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const attachmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + 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', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const attachmentFields = [ + /* -------------------------------------------------------------------------- */ + /* attachments:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Resource name of the attachment, in the form "spaces/*/messages/*/attachments/*".', + }, + +] as INodeProperties[]; 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..8e946dc58eda6 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts @@ -0,0 +1,70 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const mediaOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'media', + ], + }, + }, + options: [ + { + name: 'Download', + value: 'download', + description: 'Downloads media.', + }, + ], + default: 'download', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const mediaFields = [ + /* -------------------------------------------------------------------------- */ + /* 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.', + }, + +] as INodeProperties[]; 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..fefe5595b5d9b --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/MemberDescription.ts @@ -0,0 +1,137 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const memberOperations = [ + { + displayName: 'Operation', + name: 'operation', + 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', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + + +export const memberFields = [ + /* -------------------------------------------------------------------------- */ + /* member:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Resource name of the membership to be retrieved, in the form "spaces/*/members/*".', + }, + + /* -------------------------------------------------------------------------- */ + /* members:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Parent Name', + name: 'parentName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: 'The resource name of the space for which membership list is to be fetched, in the form "spaces/*".', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + options: [ + { + displayName: 'Page Size', + name: 'pageSize', + type: 'number', + typeOptions: { + minValue: 1, + numberStepSize: 1, + }, + default: '', + description: 'Requested page size. The value is capped at 1000. Server may return fewer results than requested. If unspecified, server will default to 100.', + }, + { + displayName: 'Page Token', + name: 'pageToken', + type: 'string', + default: '', + description: 'A token identifying a page of results the server should return.', + }, + ], + }, +] as INodeProperties[]; + 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..74267b6d0bd5e --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/MessageDescription.ts @@ -0,0 +1,653 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const messageOperations = [ + { + displayName: 'Operation', + name: 'operation', + 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.', + }, + { + name: 'Webhook', + value: 'webhook', + description: 'Creates a message through webhook (no chat bot needed).', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + + +export const messageFields = [ + + /* -------------------------------------------------------------------------- */ + /* message:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Parent Name', + name: 'parentName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Space resource name, in the form "spaces/*". Example: spaces/AAAAMpdlehY', + }, + { + displayName: 'Thread Key', + name: 'threadKey', + type: 'string', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + 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', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + 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.', + }, + { + displayName: 'Json Parameter Message', + name: 'jsonParameterMessage', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: false, + description: 'Pass message object as JSON.', + }, + { + displayName: 'Message', + name: 'messageUi', + type: 'collection', + required: true, + placeholder: 'Add Options', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + jsonParameterMessage: [ + false, + ], + }, + }, + default: {'text': ''}, + description: '', + options: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The message text.', + }, + // { // todo: get cards from the ui + // 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', + ], + jsonParameterMessage: [ + true, + ], + }, + }, + default: '', + }, + { + displayName: 'Message (JSON)', + name: 'messageJson', + type: 'json', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + jsonParameterMessage: [ + true, + ], + }, + }, + default: '', + description: 'Message input as JSON Object or JSON String.', + }, + + /* -------------------------------------------------------------------------- */ + /* messages:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + 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: 'Name', + name: 'name', + 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: 'Name', + name: 'name', + 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: 'Update Mask', + name: 'updateMask', + type: 'multiOptions', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + name: 'Text', + value: 'text', + }, + { + name: 'Cards', + value: 'cards', + }, + ], + default: [], + description: 'The fields to be updated.', + }, + { + displayName: 'Json Parameter Update Options', + name: 'jsonParameterUpdateOptions', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + updateMask: [ + 'text', + 'cards', + ], + }, + }, + default: false, + description: 'Pass update options as JSON.', + }, + { + displayName: 'Text', + name: 'textUi', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + updateMask: [ + 'text', + ], + jsonParameterUpdateOptions: [ + false, + ], + }, + }, + default: '', + description: 'The message text.', + }, + // + // { // todo: get cards from the ui + // displayName: 'Cards', + // name: 'cardsUi', + // placeholder: 'Add Cards', + // type: 'fixedCollection', + // displayOptions: { + // show: { + // resource: [ + // 'message', + // ], + // operation: [ + // 'update', + // ], + // updateMask: [ + // 'cards', + // ], + // jsonParameterUpdateOptions: [ + // false, + // ], + // }, + // }, + // 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', + ], + jsonParameterUpdateOptions: [ + true, + ], + }, + }, + default: '', + }, + { + displayName: 'Update Options (JSON)', + name: 'updateOptionsJson', + type: 'json', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + updateMask: [ + 'text', + 'cards', + ], + jsonParameterUpdateOptions: [ + true, + ], + }, + }, + default: '', + description: 'Update options input as JSON Object or JSON String.', + }, + + + + /* -------------------------------------------------------------------------- */ + /* message:webhook */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'See Google Chat guide to webhooks', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'webhook', + ], + }, + }, + default: '', + }, + { + displayName: 'Incoming Webhook URL', + name: 'webhookUrl', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'webhook', + ], + }, + }, + default: '', + description: 'URL for the incoming webhook.', + }, + + { + displayName: 'Thread Key (Full Path)', + name: 'threadKey', + type: 'string', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'webhook', + ], + }, + }, + default: '', + description: 'In the format of "spaces/*/threads/*". Thread identifier which groups messages into a single thread. Has no effect if thread field, corresponding to an existing thread, is set in message.', + }, + { + displayName: 'Request ID', + name: 'requestId', + type: 'string', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'webhook', + ], + }, + }, + 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.', + }, + { + displayName: 'Json Parameter Message', + name: 'jsonParameterMessage', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'webhook', + ], + }, + }, + default: false, + description: 'Pass message object as JSON.', + }, + { + displayName: 'Message', + name: 'messageUi', + type: 'collection', + required: true, + placeholder: 'Add Options', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'webhook', + ], + jsonParameterMessage: [ + false, + ], + }, + }, + default: {'text': ''}, + description: '', + 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: [ + 'message', + ], + operation: [ + 'webhook', + ], + jsonParameterMessage: [ + true, + ], + }, + }, + default: '', + }, + { + displayName: 'Message (JSON)', + name: 'messageJson', + type: 'json', + required: true, + displayOptions: { + show: { + + resource: [ + 'message', + ], + operation: [ + 'webhook', + ], + jsonParameterMessage: [ + true, + ], + }, + }, + default: '', + description: 'Message input as JSON Object or JSON String.', + }, + +] as INodeProperties[]; 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..d4dbe3b3c7870 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/SpaceDescription.ts @@ -0,0 +1,118 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const spaceOperations = [ + { + displayName: 'Operation', + name: 'operation', + 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', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const spaceFields = [ + /* -------------------------------------------------------------------------- */ + /* space:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'space', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Resource name of the space, in the form "spaces/*".', + }, + + /* -------------------------------------------------------------------------- */ + /* space:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'space', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'space', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + options: [ + { + displayName: 'Page Size', + name: 'pageSize', + type: 'number', + typeOptions: { + minValue: 1, + numberStepSize: 1, + }, + default: '', + description: 'Requested page size. The value is capped at 1000. Server may return fewer results than requested. If unspecified, server will default to 100.', + }, + { + displayName: 'Page Token', + name: 'pageToken', + type: 'string', + default: '', + description: 'A token identifying a page of results the server should return.', + }, + ], + }, + +] as INodeProperties[]; 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..fa6b7964db3d7 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/index.ts @@ -0,0 +1,5 @@ +export * from './MediaDescription'; +export * from './SpaceDescription'; +export * from './MemberDescription'; +export * from './MessageDescription'; +export * from './AttachmentDescription'; 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..ad37728f2af1e --- /dev/null +++ b/packages/nodes-base/nodes/Google/Chat/googleChat.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e1dc5c1b321f6..c15e5ebec7fbb 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -420,6 +420,7 @@ "dist/nodes/Google/BigQuery/GoogleBigQuery.node.js", "dist/nodes/Google/Books/GoogleBooks.node.js", "dist/nodes/Google/Calendar/GoogleCalendar.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",