diff --git a/packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts b/packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts new file mode 100644 index 0000000000..8d97262296 --- /dev/null +++ b/packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts @@ -0,0 +1,46 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class CiscoWebexOAuth2Api implements ICredentialType { + name = 'ciscoWebexOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Cisco Webex OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://webexapis.com/v1/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://webexapis.com/v1/access_token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'spark:memberships_read meeting:recordings_read spark:kms meeting:schedules_read spark:rooms_read spark:messages_write spark:memberships_write meeting:recordings_write meeting:preferences_read spark:messages_read meeting:schedules_write', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Cisco/Webex/CiscoWebex.node.ts b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebex.node.ts new file mode 100644 index 0000000000..c75a261353 --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebex.node.ts @@ -0,0 +1,499 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + getAttachemnts, + webexApiRequest, + webexApiRequestAllItems, +} from './GenericFunctions'; + +import { + meetingFields, + meetingOperations, + // meetingTranscriptFields, + // meetingTranscriptOperations, + messageFields, + messageOperations, +} from './descriptions'; + +import * as moment from 'moment-timezone'; + +export class CiscoWebex implements INodeType { + description: INodeTypeDescription = { + displayName: 'Cisco Webex', + name: 'ciscoWebex', + icon: 'file:ciscoWebex.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Cisco Webex API', + defaults: { + name: 'Cisco Webex', + color: '#29b6f6', + }, + credentials: [ + { + name: 'ciscoWebexOAuth2Api', + required: true, + }, + ], + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Meeting', + value: 'meeting', + }, + // { + // name: 'Meeeting Transcript', + // value: 'meetingTranscript', + // }, + { + name: 'Message', + value: 'message', + }, + ], + default: 'message', + description: 'Resource to consume', + }, + ...meetingOperations, + ...meetingFields, + // ...meetingTranscriptOperations, + // ...meetingTranscriptFields, + ...messageOperations, + ...messageFields, + ], + }; + + methods = { + loadOptions: { + async getRooms(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const rooms = await webexApiRequestAllItems.call(this, 'items', 'GET', '/rooms'); + for (const room of rooms) { + returnData.push({ + name: room.title, + value: room.id, + }); + } + return returnData; + }, + async getSites(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const sites = await webexApiRequestAllItems.call(this, 'sites', 'GET', '/meetingPreferences/sites'); + for (const site of sites) { + returnData.push({ + name: site.siteUrl, + value: site.siteUrl, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const timezone = this.getTimezone(); + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + + + for (let i = 0; i < items.length; i++) { + + try { + if (resource === 'message') { + + // ********************************************************************** + // message + // ********************************************************************** + + if (operation === 'create') { + + // ---------------------------------------- + // message: create + // ---------------------------------------- + + // https://developer.webex.com/docs/api/v1/messages/create-a-message + const destination = this.getNodeParameter('destination', i); + const file = this.getNodeParameter('additionalFields.fileUi.fileValue', i, {}) as IDataObject; + const markdown = this.getNodeParameter('additionalFields.markdown', i, '') as boolean; + const body = {} as IDataObject; + if (destination === 'room') { + body['roomId'] = this.getNodeParameter('roomId', i); + } + + if (destination === 'person') { + const specifyPersonBy = this.getNodeParameter('specifyPersonBy', 0) as string; + if (specifyPersonBy === 'id') { + body['toPersonId'] = this.getNodeParameter('toPersonId', i); + } else { + body['toPersonEmail'] = this.getNodeParameter('toPersonEmail', i); + } + } + + if (markdown) { + body['markdown'] = markdown; + } + + body['text'] = this.getNodeParameter('text', i); + + body.attachments = getAttachemnts(this.getNodeParameter('additionalFields.attachmentsUi.attachmentValues', i, []) as IDataObject[]); + + if (Object.keys(file).length) { + + const isBinaryData = file.fileLocation === 'binaryData' ? true : false; + + if (isBinaryData) { + + if (!items[i].binary) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!'); + } + + const binaryPropertyName = file.binaryPropertyName as string; + + const binaryData = items[i].binary![binaryPropertyName] as IBinaryData; + + const formData = { + files: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }, + }, + }; + Object.assign(body, formData); + } else { + const url = file.url as string; + Object.assign(body, { files: url }); + } + } + + if (file.fileLocation === 'binaryData') { + responseData = await webexApiRequest.call(this, 'POST', '/messages', {}, {}, undefined, { formData: body }); + } else { + responseData = await webexApiRequest.call(this, 'POST', '/messages', body); + } + + + } else if (operation === 'delete') { + + // ---------------------------------------- + // message: delete + // ---------------------------------------- + + // https://developer.webex.com/docs/api/v1/messages/delete-a-message + const messageId = this.getNodeParameter('messageId', i); + + const endpoint = `/messages/${messageId}`; + responseData = await webexApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // message: get + // ---------------------------------------- + + // https://developer.webex.com/docs/api/v1/messages/get-message-details + const messageId = this.getNodeParameter('messageId', i); + + const endpoint = `/messages/${messageId}`; + responseData = await webexApiRequest.call(this, 'GET', endpoint); + + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // message: getAll + // ---------------------------------------- + + // https://developer.webex.com/docs/api/v1/messages/list-messages + const qs: IDataObject = { + roomId: this.getNodeParameter('roomId', i), + }; + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + + if (Object.keys(filters).length) { + Object.assign(qs, filters); + } + + if (returnAll === true) { + responseData = await webexApiRequestAllItems.call(this, 'items', 'GET', '/messages', {}, qs); + } else { + qs.max = this.getNodeParameter('limit', i) as number; + responseData = await webexApiRequest.call(this, 'GET', '/messages', {}, qs); + responseData = responseData.items; + } + + + } else if (operation === 'update') { + + // ---------------------------------------- + // message: update + // ---------------------------------------- + + // https://developer.webex.com/docs/api/v1/messages/edit-a-message + const messageId = this.getNodeParameter('messageId', i) as string; + const markdown = this.getNodeParameter('markdown', i) as boolean; + + const endpoint = `/messages/${messageId}`; + + responseData = await webexApiRequest.call(this, 'GET', endpoint); + + const body = { + roomId: responseData.roomId, + } as IDataObject; + + if (markdown === true) { + body['markdown'] = this.getNodeParameter('markdownText', i); + } else { + body['text'] = this.getNodeParameter('text', i); + } + + responseData = await webexApiRequest.call(this, 'PUT', endpoint, body); + } + } + + if (resource === 'meeting') { + if (operation === 'create') { + const title = this.getNodeParameter('title', i) as string; + const start = this.getNodeParameter('start', i) as string; + const end = this.getNodeParameter('end', i) as string; + const invitees = this.getNodeParameter('additionalFields.inviteesUi.inviteeValues', i, []) as IDataObject[]; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + title, + start: moment.tz(start, timezone).format(), + end: moment.tz(end, timezone).format(), + ...additionalFields, + }; + + if (body.requireRegistrationInfo) { + body['registration'] = (body.requireRegistrationInfo as string[]) + .reduce((obj, value) => Object.assign(obj, { [`${value}`]: true }), {}); + delete body.requireRegistrationInfo; + } + + if (invitees) { + body['invitees'] = invitees; + delete body.inviteesUi; + } + + responseData = await webexApiRequest.call(this, 'POST', '/meetings', body); + + } + + if (operation === 'delete') { + const meetingId = this.getNodeParameter('meetingId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + const qs: IDataObject = { + ...options, + }; + + responseData = await webexApiRequest.call(this, 'DELETE', `/meetings/${meetingId}`, {}, qs); + responseData = { success: true }; + } + + if (operation === 'get') { + const meetingId = this.getNodeParameter('meetingId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + let headers = {}; + + const qs: IDataObject = { + ...options, + }; + + if (options.passsword) { + headers = { + passsword: options.passsword, + }; + } + + responseData = await webexApiRequest.call(this, 'GET', `/meetings/${meetingId}`, {}, qs, undefined, { headers }); + } + + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const qs: IDataObject = { + ...filters, + }; + + if (qs.from) { + qs.from = moment(qs.from as string).utc(true).format(); + } + + if (qs.to) { + qs.to = moment(qs.to as string).utc(true).format(); + } + + if (returnAll === true) { + responseData = await webexApiRequestAllItems.call(this, 'items', 'GET', '/meetings', {}, qs); + returnData.push(...responseData); + } else { + qs.max = this.getNodeParameter('limit', i) as number; + responseData = await webexApiRequest.call(this, 'GET', '/meetings', {}, qs); + responseData = responseData.items; + } + } + + if (operation === 'update') { + const meetingId = this.getNodeParameter('meetingId', i) as string; + const invitees = this.getNodeParameter('updateFields.inviteesUi.inviteeValues', i, []) as IDataObject[]; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const { + title, + password, + start, + end, + } = await webexApiRequest.call(this, 'GET', `/meetings/${meetingId}`); + + const body: IDataObject = { + ...updateFields, + }; + + if (body.requireRegistrationInfo) { + body['registration'] = (body.requireRegistrationInfo as string[]) + .reduce((obj, value) => Object.assign(obj, { [`${value}`]: true }), {}); + delete body.requireRegistrationInfo; + } + + if (invitees.length) { + body['invitees'] = invitees; + } + + if (body.start) { + body.start = moment.tz(updateFields.start, timezone).format(); + } else { + body.start = start; + } + + if (body.end) { + body.end = moment.tz(updateFields.end, timezone).format(); + } else { + body.end = end; + } + + if (!body.title) { + body.title = title; + } + + if (!body.password) { + body.password = password; + } + + responseData = await webexApiRequest.call(this, 'PUT', `/meetings/${meetingId}`, body); + + } + } + + 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()) { + returnData.push({ error: error.toString() }); + continue; + } + + throw error; + } + + } + + // if (resource === 'meetingTranscript') { + + // if (operation === 'download') { + // for (let i = 0; i < items.length; i++) { + // const transcriptId = this.getNodeParameter('transcriptId', i) as string; + // const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + // const meetingId = this.getNodeParameter('meetingId', i) as string; + // const options = this.getNodeParameter('options', i) as IDataObject; + + // const qs: IDataObject = { + // meetingId, + // ...options, + // }; + // const transcription = await webexApiRequest.call(this, 'GET', `/meetingTranscripts/${transcriptId}/download`, {}, qs); + + // responseData = { + // json: {}, + // binary: { + // [binaryPropertyName]: { + // data: Buffer.from(transcription, BINARY_ENCODING), + // //contentType: + // //FILE + // } + // } + // } + + // } + // } + + // if (operation === 'getAll') { + // for (let i = 0; i < items.length; i++) { + // try { + // const meetingId = this.getNodeParameter('meetingId', i) as string; + // const filters = this.getNodeParameter('filters', i) as IDataObject; + // const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + // const qs: IDataObject = { + // meetingId, + // ...filters, + // }; + + // if (returnAll === true) { + // responseData = await webexApiRequestAllItems.call(this, 'items', 'GET', '/meetingTranscripts', {}, qs); + // returnData.push(...responseData); + // } else { + // qs.max = this.getNodeParameter('limit', i) as number; + // responseData = await webexApiRequest.call(this, 'GET', '/meetingTranscripts', {}, qs); + // returnData.push(...responseData.items); + // } + // } catch (error) { + // if (this.continueOnFail()) { + // returnData.push({ + // error: error.message, + // }); + // } + // } + // } + // } + // } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Cisco/Webex/CiscoWebexTrigger.node.ts b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebexTrigger.node.ts new file mode 100644 index 0000000000..bff18e37ee --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebexTrigger.node.ts @@ -0,0 +1,683 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + getAutomaticSecret, + getEvents, + mapResource, + webexApiRequest, + webexApiRequestAllItems, +} from './GenericFunctions'; + +import { + createHmac, +} from 'crypto'; + +export class CiscoWebexTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Cisco Webex Trigger', + name: 'ciscoWebexTrigger', + icon: 'file:ciscoWebex.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["resource"] + ":" + $parameter["event"]}}', + description: 'Starts the workflow when Cisco Webex events occur.', + defaults: { + name: 'Cisco Webex Trigger', + color: '#29b6f6', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'ciscoWebexOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Attachment Action', + value: 'attachmentAction', + }, + { + name: 'Meeting', + value: 'meeting', + }, + { + name: 'Membership', + value: 'membership', + }, + { + name: 'Message', + value: 'message', + }, + // { + // name: 'Telephony Call', + // value: 'telephonyCall', + // }, + { + name: 'Recording', + value: 'recording', + }, + { + name: 'Room', + value: 'room', + }, + { + name: '*', + value: 'all', + }, + ], + default: 'meeting', + required: true, + }, + ...getEvents(), + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'attachmentAction', + ], + }, + }, + default: true, + description: 'By default the response only contain a reference to the data the user inputed
If this option gets activated it will resolve the data automatically.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Has Files', + name: 'hasFiles', + type: 'boolean', + displayOptions: { + show: { + '/resource': [ + 'message', + ], + '/event': [ + 'created', + 'deleted', + ], + }, + }, + default: false, + description: 'Limit to messages which contain file content attachments', + }, + { + displayName: 'Is Locked', + name: 'isLocked', + type: 'boolean', + displayOptions: { + show: { + '/resource': [ + 'room', + ], + '/event': [ + 'created', + 'updated', + ], + }, + }, + default: false, + description: 'Limit to rooms that are locked', + }, + { + displayName: 'Is Moderator', + name: 'isModerator', + type: 'boolean', + displayOptions: { + show: { + '/resource': [ + 'membership', + ], + '/event': [ + 'created', + 'updated', + 'deleted', + ], + }, + }, + default: false, + description: 'Limit to moderators of a room', + }, + { + displayName: 'Mentioned People', + name: 'mentionedPeople', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'message', + ], + '/event': [ + 'created', + 'deleted', + ], + }, + }, + default: '', + description: `Limit to messages which contain these mentioned people, by person ID; accepts me as a shorthand for your own person ID; separate multiple values with commas`, + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'attachmentAction', + ], + '/event': [ + 'created', + ], + }, + }, + default: '', + description: 'Limit to a particular message, by ID', + }, + { + displayName: 'Owned By', + name: 'ownedBy', + displayOptions: { + show: { + '/resource': [ + 'meeting', + ], + }, + }, + type: 'string', + default: '', + }, + { + displayName: 'Person Email', + name: 'personEmail', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'membership', + ], + '/event': [ + 'created', + 'updated', + 'deleted', + ], + }, + }, + default: '', + description: 'Limit to a particular person, by email', + }, + { + displayName: 'Person Email', + name: 'personEmail', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'message', + ], + '/event': [ + 'created', + 'deleted', + ], + }, + }, + default: '', + description: 'Limit to a particular person, by email', + }, + { + displayName: 'Person ID', + name: 'personId', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'attachmentAction', + ], + '/event': [ + 'created', + ], + }, + }, + default: '', + description: 'Limit to a particular person, by ID', + }, + { + displayName: 'Person ID', + name: 'personId', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'membership', + ], + '/event': [ + 'created', + 'updated', + 'deleted', + ], + }, + }, + default: '', + description: 'Limit to a particular person, by ID', + }, + { + displayName: 'Person ID', + name: 'personId', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'message', + ], + '/event': [ + 'created', + 'deleted', + ], + }, + }, + default: '', + description: 'Limit to a particular person, by ID', + }, + + { + displayName: 'Room ID', + name: 'roomId', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'attachmentAction', + ], + '/event': [ + 'created', + ], + }, + }, + default: '', + description: 'Limit to a particular room, by ID', + }, + { + displayName: 'Room ID', + name: 'roomId', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'membership', + ], + '/event': [ + 'created', + 'updated', + 'deleted', + ], + }, + }, + default: '', + description: 'Limit to a particular room, by ID', + }, + { + displayName: 'Room ID', + name: 'roomId', + type: 'string', + displayOptions: { + show: { + '/resource': [ + 'message', + ], + '/event': [ + 'created', + 'updated', + ], + }, + }, + default: '', + description: 'Limit to a particular room, by ID', + }, + { + displayName: 'Room Type', + name: 'roomType', + type: 'options', + options: [ + { + name: 'Direct', + value: 'direct', + }, + { + name: 'Group', + value: 'group', + }, + ], + displayOptions: { + show: { + '/resource': [ + 'message', + ], + '/event': [ + 'created', + 'deleted', + ], + }, + }, + default: '', + description: `Limit to a particular room type`, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Direct', + value: 'direct', + }, + { + name: 'Group', + value: 'group', + }, + ], + displayOptions: { + show: { + '/resource': [ + 'room', + ], + '/event': [ + 'created', + 'updated', + ], + }, + }, + default: '', + description: `Limit to a particular room type`, + }, + // { + // displayName: 'Call Type', + // name: 'callType', + // type: 'options', + // options: [ + // { + // name: 'Emergency', + // value: 'emergency', + // }, + // { + // name: 'External', + // value: 'external', + // }, + // { + // name: 'Location', + // value: 'location', + // }, + // { + // name: 'Disconnected', + // value: 'disconnected', + // }, + // { + // name: 'Organization', + // value: 'organization', + // }, + // { + // name: 'Other', + // value: 'other', + // }, + // { + // name: 'Repair', + // value: 'repair', + // }, + // ], + // displayOptions: { + // show: { + // '/resource': [ + // 'telephonyCall', + // ], + // '/event': [ + // 'created', + // 'deleted', + // 'updated', + // ], + // }, + // }, + // default: '', + // description: `Limit to a particular call type`, + // }, + // { + // displayName: 'Person ID', + // name: 'personId', + // type: 'string', + // displayOptions: { + // show: { + // '/resource': [ + // 'telephonyCall', + // ], + // '/event': [ + // 'created', + // 'deleted', + // 'updated', + // ], + // }, + // }, + // default: '', + // description: 'Limit to a particular person, by ID', + // }, + // { + // displayName: 'Personality', + // name: 'personality', + // type: 'options', + // options: [ + // { + // name: 'Click To Dial', + // value: 'clickToDial', + // }, + // { + // name: 'Originator', + // value: 'originator', + // }, + // { + // name: 'Terminator', + // value: 'terminator', + // }, + // ], + // displayOptions: { + // show: { + // '/resource': [ + // 'telephonyCall', + // ], + // '/event': [ + // 'created', + // 'deleted', + // 'updated', + // ], + // }, + // }, + // default: '', + // description: `Limit to a particular call personality`, + // }, + // { + // displayName: 'State', + // name: 'state', + // type: 'options', + // options: [ + // { + // name: 'Alerting', + // value: 'alerting', + // }, + // { + // name: 'Connected', + // value: 'connected', + // }, + // { + // name: 'Connecting', + // value: 'connecting', + // }, + // { + // name: 'Disconnected', + // value: 'disconnected', + // }, + // { + // name: 'Held', + // value: 'held', + // }, + // { + // name: 'Remote Held', + // value: 'remoteHeld', + // }, + // ], + // displayOptions: { + // show: { + // '/resource': [ + // 'telephonyCall', + // ], + // '/event': [ + // 'created', + // 'deleted', + // 'updated', + // ], + // }, + // }, + // default: '', + // description: `Limit to a particular call state`, + // }, + ], + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const resource = this.getNodeParameter('resource') as string; + const event = this.getNodeParameter('event') as string; + + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const data = await webexApiRequestAllItems.call(this, 'items', 'GET', '/webhooks'); + for (const webhook of data) { + if (webhook.url === webhookUrl + && webhook.resource === mapResource(resource) + && webhook.event === event + && webhook.status === 'active') { + webhookData.webhookId = webhook.id as string; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const event = this.getNodeParameter('event') as string; + const resource = this.getNodeParameter('resource') as string; + const filters = this.getNodeParameter('filters', {}) as IDataObject; + const secret = getAutomaticSecret(this.getCredentials('ciscoWebexOAuth2Api')!); + const filter = []; + for (const key of Object.keys(filters)) { + if (key !== 'ownedBy') { + filter.push(`${key}=${filters[key]}`); + } + } + const endpoint = '/webhooks'; + + const body: IDataObject = { + name: `n8n-webhook:${webhookUrl}`, + targetUrl: webhookUrl, + event, + resource: mapResource(resource), + }; + + if (filters.ownedBy) { + body['ownedBy'] = filters.ownedBy as string; + } + + body['secret'] = secret; + + if (filter.length) { + body['filter'] = filter.join('&'); + } + + const responseData = await webexApiRequest.call(this, 'POST', endpoint, body); + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + webhookData.secret = secret; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await webexApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + let bodyData = this.getBodyData(); + const webhookData = this.getWorkflowStaticData('node'); + const headers = this.getHeaderData() as IDataObject; + const req = this.getRequestObject(); + const resolveData = this.getNodeParameter('resolveData', false) as boolean; + + //@ts-ignore + const computedSignature = createHmac('sha1', webhookData.secret).update(req.rawBody).digest('hex'); + if (headers['x-spark-signature'] !== computedSignature) { + return {}; + } + + if (resolveData) { + const { data: { id } } = bodyData as { data: { id: string } }; + bodyData = await webexApiRequest.call(this, 'GET', `/attachment/actions/${id}`); + } + + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Cisco/Webex/GenericFunctions.ts b/packages/nodes-base/nodes/Cisco/Webex/GenericFunctions.ts new file mode 100644 index 0000000000..8bb552c259 --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/GenericFunctions.ts @@ -0,0 +1,657 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + IDataObject, + INodeProperties, + IWebhookFunctions, + NodeApiError, +} from 'n8n-workflow'; + +import { + upperFirst, +} from 'lodash'; + +import { + createHash, +} from 'crypto'; + +export async function webexApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUri = { + method, + body, + qs, + uri: uri || `https://webexapis.com/v1${resource}`, + json: true, + }; + try { + if (Object.keys(option).length !== 0) { + options = Object.assign({}, options, option); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(qs).length === 0) { + delete options.qs; + } + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'ciscoWebexOAuth2Api', options, { tokenType: 'Bearer' }); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function webexApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, options: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.max = 100; + do { + responseData = await webexApiRequest.call(this, method, endpoint, body, query, uri, { resolveWithFullResponse: true, ...options }); + if (responseData.headers.link) { + uri = responseData.headers['link'].split(';')[0].replace('<', '').replace('>', ''); + } + returnData.push.apply(returnData, responseData.body[propertyName]); + } while ( + responseData.headers['link'] !== undefined && + responseData.headers['link'].includes('rel="next"') + ); + return returnData; +} + +export function getEvents() { + const resourceEvents: { [key: string]: string[] } = { + 'attachmentAction': ['created', 'deleted', 'updated', '*'], + 'membership': ['created', 'deleted', 'updated', '*'], + 'message': ['created', 'deleted', 'updated', '*'], + 'room': ['created', 'deleted', 'updated', '*'], + 'meeting': ['created', 'deleted', 'updated', 'started', 'ended', '*'], + 'recording': ['created', 'deleted', 'updated', '*'], + 'telephonyCall': ['created', 'deleted', 'updated'], + '*': ['created', 'updated', 'deleted', '*'], + }; + + const elements: INodeProperties[] = []; + + for (const resource of Object.keys(resourceEvents)) { + elements.push({ + displayName: 'Event', + name: 'event', + type: 'options', + displayOptions: { + show: { + resource: [ + (resource === '*') ? 'all' : resource, + ], + }, + }, + options: resourceEvents[resource].map((event) => ({ value: (event === '*' ? 'all' : event), name: upperFirst(event) })), + default: '', + required: true, + }); + } + return elements; +} + +export function mapResource(event: string) { + return ({ + 'attachmentAction': 'attachmentActions', + 'membership': 'memberships', + 'message': 'messages', + 'room': 'rooms', + 'meeting': 'meetings', + 'recording': 'recordings', + 'telephonyCall': 'telephony_calls', + 'all': 'all', + } as { [key: string]: string })[event]; +} + +export function getAttachemnts(attachements: IDataObject[]) { + const _attachments: IDataObject[] = []; + for (const attachment of attachements) { + const body: IDataObject[] = []; + const actions: IDataObject[] = []; + for (const element of (attachment?.elementsUi as IDataObject).elementValues as IDataObject[] || []) { + // tslint:disable-next-line: no-any + const { type, ...rest } = element as { type: string, [key: string]: any }; + if (type.startsWith('input')) { + body.push({ type: `Input.${upperFirst(type.replace('input', ''))}`, ...removeEmptyProperties(rest) }); + } else { + body.push({ type: upperFirst(type), ...removeEmptyProperties(rest) }); + } + } + for (const action of (attachment?.actionsUi as IDataObject).actionValues as IDataObject[] || []) { + // tslint:disable-next-line: no-any + const { type, ...rest } = action as { type: string, [key: string]: any }; + actions.push({ type: `Action.${upperFirst(type)}`, ...removeEmptyProperties(rest) }); + } + _attachments.push({ + contentType: 'application/vnd.microsoft.card.adaptive', + content: { + $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', + type: 'AdaptiveCard', + version: '1.2', + body, + actions, + }, + }); + } + return _attachments; +} + +export function getActionInheritedProperties() { + return [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + description: 'Label for button or link that represents this action.', + }, + { + displayName: 'Icon URL', + name: 'iconUrl', + type: 'string', + default: '', + description: 'Optional icon to be shown on the action in conjunction with the title. Supports data URI in version 1.2+', + }, + { + displayName: 'Style', + name: 'style', + type: 'options', + options: [ + { + name: 'Default', + value: 'default', + }, + { + name: 'Positive', + value: 'positive', + }, + { + name: 'Destructive', + value: 'destructive', + }, + ], + default: 'default', + description: 'Controls the style of an Action, which influences how the action is displayed, spoken, etc.', + }, + ]; +} + +export function getTextBlockProperties() { + return [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + required: true, + description: 'Text to display. A subset of markdown is supported (https://aka.ms/ACTextFeatures)', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + options: [ + { + name: 'Default', + value: 'default', + }, + { + name: 'Dark', + value: 'dark', + }, + { + name: 'Light', + value: 'light', + }, + { + name: 'Accent', + value: 'accent', + }, + { + name: 'Good', + value: 'good', + }, + { + name: 'Warning', + value: 'warning', + }, + { + name: 'Attention', + value: 'attention', + }, + ], + default: 'default', + description: 'Color of the TextBlock element', + }, + { + displayName: 'Font Type', + name: 'fontType', + type: 'options', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + options: [ + { + name: 'Default', + value: 'default', + }, + { + name: 'Monospace', + value: 'monospace', + }, + ], + default: 'default', + description: 'Type of font to use for rendering', + }, + { + displayName: 'Horizontal Alignment', + name: 'horizontalAlignment', + type: 'options', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + options: [ + { + name: 'Left', + value: 'left', + }, + { + name: 'Center', + value: 'center', + }, + { + name: 'Right', + value: 'right', + }, + ], + default: 'left', + description: 'Controls the horizontal text alignment', + }, + { + displayName: 'Is Subtle', + name: 'isSubtle', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + default: false, + description: 'Displays text slightly toned down to appear less prominent', + }, + { + displayName: 'Max Lines', + name: 'maxLines', + type: 'number', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + default: 1, + description: 'Specifies the maximum number of lines to display', + }, + { + displayName: 'Size', + name: 'size', + type: 'options', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + options: [ + { + name: 'Default', + value: 'default', + }, + { + name: 'Small', + value: 'small', + }, + { + name: 'Medium', + value: 'medium', + }, + { + name: 'Large', + value: 'large', + }, + { + name: 'Extra Large', + value: 'extraLarge', + }, + ], + default: 'default', + description: 'Controls size of text', + }, + { + displayName: 'Weight', + name: 'weight', + type: 'options', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + options: [ + { + name: 'Default', + value: 'default', + }, + { + name: 'Lighter', + value: 'lighter', + }, + { + name: 'Bolder', + value: 'bolder', + }, + ], + default: 'default', + description: 'Controls the weight of TextBlock elements', + }, + { + displayName: 'Wrap', + name: 'wrap', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + default: true, + description: 'If true, allow text to wrap. Otherwise, text is clipped', + }, + { + displayName: 'Height', + name: 'height', + type: 'options', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + options: [ + { + name: 'Auto', + value: 'auto', + }, + { + name: 'Stretch', + value: 'stretch', + }, + ], + default: 'auto', + description: 'Specifies the height of the element', + }, + { + displayName: 'Separator', + name: 'separator', + type: 'boolean', + default: false, + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + description: 'When true, draw a separating line at the top of the element.', + }, + { + displayName: 'Spacing', + name: 'spacing', + type: 'options', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + options: [ + { + name: 'Default', + value: 'default', + }, + { + name: 'None', + value: 'none', + }, + { + name: 'Small', + value: 'small', + }, + { + name: 'Medium', + value: 'medium', + }, + { + name: 'Large', + value: 'large', + }, + { + name: 'Extra Large', + value: 'extraLarge', + }, + { + name: 'Padding', + value: 'padding', + }, + ], + default: 'default', + description: 'Controls the amount of spacing between this element and the preceding element', + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + default: '', + description: 'A unique identifier associated with the item', + }, + { + displayName: 'Is Visible', + name: 'isVisible', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'textBlock', + ], + }, + }, + default: true, + description: 'If false, this item will be removed from the visual trees', + }, + ]; +} + +export function getInputTextProperties() { + return [ + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + type: [ + 'inputText', + ], + }, + }, + default: '', + description: 'Unique identifier for the value. Used to identify collected input when the Submit action is performed', + }, + { + displayName: 'Is Multiline', + name: 'isMultiline', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'inputText', + ], + }, + }, + default: false, + description: 'If true, allow multiple lines of input', + }, + { + displayName: 'Max Length', + name: 'maxLength', + type: 'number', + displayOptions: { + show: { + type: [ + 'inputText', + ], + }, + }, + default: 1, + description: 'Hint of maximum length characters to collect (may be ignored by some clients)', + }, + { + displayName: 'Placeholder', + name: 'placeholder', + type: 'string', + displayOptions: { + show: { + type: [ + 'inputText', + ], + }, + }, + default: '', + description: 'Description of the input desired. Displayed when no text has been input', + }, + { + displayName: 'Regex', + name: 'regex', + type: 'string', + displayOptions: { + show: { + type: [ + 'inputText', + ], + }, + }, + default: '', + description: 'Regular expression indicating the required format of this text input', + }, + { + displayName: 'Style', + name: 'style', + type: 'options', + displayOptions: { + show: { + type: [ + 'inputText', + ], + }, + }, + options: [ + { + name: 'Text', + value: 'text', + }, + { + name: 'Tel', + value: 'tel', + }, + { + name: 'URL', + value: 'url', + }, + { + name: 'Email', + value: 'email', + }, + ], + default: 'text', + description: 'Style hint for text input', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + displayOptions: { + show: { + type: [ + 'inputText', + ], + }, + }, + default: '', + description: 'The initial value for this field', + }, + ]; +} + +// tslint:disable-next-line: no-any +function removeEmptyProperties(rest: { [key: string]: any }) { + return Object.keys(rest) + .filter((k) => rest[k] !== '') + .reduce((a, k) => ({ ...a, [k]: rest[k] }), {}); +} + +export function getAutomaticSecret(credentials: ICredentialDataDecryptedObject) { + const data = `${credentials.clientId},${credentials.clientSecret}`; + return createHash('md5').update(data).digest('hex'); +} diff --git a/packages/nodes-base/nodes/Cisco/Webex/ciscoWebex.svg b/packages/nodes-base/nodes/Cisco/Webex/ciscoWebex.svg new file mode 100644 index 0000000000..f8253bbf23 --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/ciscoWebex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Cisco/Webex/descriptions/MeetingDescription.ts b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MeetingDescription.ts new file mode 100644 index 0000000000..8937ff0314 --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MeetingDescription.ts @@ -0,0 +1,969 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const meetingOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const meetingFields = [ + // ---------------------------------------- + // meeting: create + // ---------------------------------------- + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Meeting title. The title can be a maximum of 128 characters long', + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Date and time for the start of meeting. Acceptable format', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Date and time for the end of meeting. Acceptable format', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Agenda', + name: 'agenda', + type: 'string', + default: '', + description: 'Meeting agenda. The agenda can be a maximum of 1300 characters long', + }, + { + displayName: 'Allow Any User To Be Co-Host', + name: 'allowAnyUserToBeCoHost', + type: 'boolean', + default: false, + description: `Whether or not to allow any attendee with a host account on the target site to become a co-host when joining the meeting`, + }, + { + displayName: 'Allow Authenticated Devices', + name: 'allowAuthenticatedDevices', + type: 'boolean', + default: false, + description: `Whether or not to allow authenticated video devices in the meeting's organization to start or join the meeting without a prompt`, + }, + { + displayName: 'Allow First User To Be Co-Host', + name: 'allowFirstUserToBeCoHost', + type: 'boolean', + default: false, + description: `Whether or not to allow the first attendee of the meeting with a host account on the target site to become a co-host`, + }, + { + displayName: 'Auto Accept Request', + name: 'autoAcceptRequest', + type: 'boolean', + default: false, + description: 'Whether or not meeting registration request is accepted automatically', + }, + { + displayName: 'Enable Connect Audio Before Host', + name: 'enableConnectAudioBeforeHost', + type: 'boolean', + default: false, + description: `Whether or not to allow any attendee to connect audio in the meeting before the host joins the meeting`, + }, + { + displayName: 'Enabled Auto Record Meeting', + name: 'enabledAutoRecordMeeting', + type: 'boolean', + default: false, + description: `Whether or not meeting is recorded automatically`, + }, + { + displayName: 'Enabled Join Before Host', + name: 'enabledJoinBeforeHost', + type: 'boolean', + default: false, + description: `Whether or not to allow any attendee to join the meeting before the host joins the meeting`, + }, + { + displayName: 'Exclude Password', + name: 'excludePassword', + type: 'boolean', + default: false, + description: `Whether or not to exclude password from the meeting email invitation`, + }, + { + displayName: 'Host Email', + name: 'hostEmail', + type: 'string', + default: '', + description: `Email address for the meeting host. Can only be set if you're an admin`, + }, + { + displayName: 'Integration Tags', + name: 'integrationTags', + type: 'string', + default: '', + description: `External keys created by an integration application in its own domain. They could be Zendesk ticket IDs, Jira IDs, Salesforce Opportunity IDs, etc`, + }, + { + displayName: 'Invitees', + name: 'inviteesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: '', + placeholder: 'Add Invitee', + options: [ + { + displayName: 'Invitee', + name: 'inviteeValues', + values: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'Email address of meeting invitee', + }, + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + default: '', + description: 'Display name of meeting invitee', + }, + { + displayName: 'Co-Host', + name: 'coHost', + type: 'boolean', + default: false, + description: 'Whether or not invitee is allowed to be a co-host for the meeting', + }, + ], + }, + ], + }, + { + displayName: 'Join Before Host Minutes', + name: 'joinBeforeHostMinutes', + type: 'options', + options: [ + { + name: '0', + value: 0, + }, + { + name: '5', + value: 5, + }, + { + name: '10', + value: 10, + }, + { + name: '15', + value: 15, + }, + ], + default: 0, + description: `The number of minutes an attendee can join the meeting before the meeting start time and the host joins`, + }, + { + displayName: 'Public Meeting', + name: 'publicMeeting', + type: 'boolean', + default: false, + description: `Whether or not to allow the meeting to be listed on the public calendar`, + }, + { + displayName: 'Recurrence', + name: 'recurrence', + type: 'string', + default: '', + description: `Rule for how the meeting should recur. Acceptable format`, + }, + { + displayName: 'Required Registration Info', + name: 'requireRegistrationInfo', + type: 'multiOptions', + options: [ + { + name: 'Require First Name', + value: 'requireFirstName', + }, + { + name: 'Require Last Name', + value: 'requireLastName', + }, + { + name: 'Require Email', + value: 'requireEmail', + }, + { + name: 'Require Job Title', + value: 'requireJobTitle', + }, + { + name: 'Require Company Name', + value: 'requireCompanyName', + }, + { + name: 'Require Address 1', + value: 'requireAddress1', + }, + { + name: 'Require Address 2', + value: 'requireAddress2', + }, + { + name: 'Require City', + value: 'requireCity', + }, + { + name: 'Require State', + value: 'requireState', + }, + { + name: 'Require Zip Code', + value: 'requireZipCode', + }, + { + name: 'Require Country Region', + value: 'requireCountryRegion', + }, + { + name: 'Require Work Phone', + value: 'requireWorkPhone', + }, + { + name: 'Require Fax', + value: 'requireFax', + }, + ], + default: [], + description: 'Data required for meeting registration', + }, + { + displayName: 'Reminder Time', + name: 'reminderTime', + type: 'number', + default: 1, + description: `The number of minutes before the meeting begins, for sending an email reminder to the host`, + }, + { + displayName: 'Send Email', + name: 'sendEmail', + type: 'boolean', + default: true, + description: `Whether or not to send emails to host and invitees`, + }, + { + displayName: 'Site URL', + name: 'siteUrl', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: `URL of the Webex site which the meeting is created on. If not specified, the meeting is created on user's preferred site`, + }, + ], + }, + + // ---------------------------------------- + // meeting: delete + // ---------------------------------------- + { + displayName: 'Meeting ID', + name: 'meetingId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'ID of the meeting', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'delete', + ], + }, + }, + options: [ + { + displayName: 'Host Email', + name: 'hostEmail', + type: 'string', + default: '', + description: 'Email address for the meeting host. This parameter is only used if the user or application calling the API has the admin-level scopes', + }, + { + displayName: 'Send Email', + name: 'sendEmail', + type: 'boolean', + default: true, + description: 'Whether or not to send emails to host and invitees.', + }, + ], + }, + + // ---------------------------------------- + // meeting: get + // ---------------------------------------- + { + displayName: 'Meeting ID', + name: 'meetingId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'get', + ], + }, + }, + description: 'ID of the meeting', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Host Email', + name: 'hostEmail', + type: 'string', + default: '', + description: 'Email address for the meeting host. This parameter is only used if the user or application calling the API has the admin-level scopes', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: `Meeting password. It's required when the meeting is protected by a password and the current user is not privileged to view it if they are not a host, co-host or invitee of the meeting`, + }, + { + displayName: 'Send Email', + name: 'sendEmail', + type: 'boolean', + default: true, + description: 'Whether or not to send emails to host and invitees. It is an optional field and default value is true', + }, + ], + }, + + // ---------------------------------------- + // meeting: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + description: 'Start date and time (inclusive) for the meeting. Acceptable format', + }, + { + displayName: 'Host Email', + name: 'hostEmail', + type: 'string', + default: '', + description: 'Email address for the meeting host', + }, + { + displayName: 'Integration Tag', + name: 'integrationTag', + type: 'string', + default: '', + description: 'External tag created by another application, e.g. Zendesk ticket ID or Jira ID', + }, + { + displayName: 'Limit to Current Meetings', + name: 'current', + type: 'boolean', + default: true, + description: 'For meeting series, whether to return just the current meeting or all meetings', + }, + { + displayName: 'Meeting Number', + name: 'meetingNumber', + type: 'string', + default: '', + description: 'Meeting number for the meeting objects being requested', + }, + { + displayName: 'Meeting Type', + name: 'meetingType', + type: 'options', + options: [ + { + name: 'Meeting Series', + value: 'meetingSeries', + description: 'Master of a scheduled series of meetings which consists of one or more scheduled meeting based on a recurrence rule', + }, + { + name: 'Scheduled Meeting', + value: 'scheduledMeeting', + description: 'Instance from a master meeting series', + }, + { + name: 'Meeting', + value: 'meeting', + description: 'Meeting instance that is actually happening or has happened', + }, + ], + default: 'meetingSeries', + }, + { + displayName: 'Participant Email', + name: 'participantEmail', + type: 'string', + default: '', + description: 'Email of a person that must be a meeting participant', + }, + { + displayName: 'Site URL', + name: 'siteUrl', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: 'URL of the Webex site which the API lists meetings from', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Scheduled', + value: 'scheduled', + }, + { + name: 'Ready', + value: 'ready', + }, + { + name: 'Lobby', + value: 'lobby', + }, + { + name: 'In Progress', + value: 'inProgress', + }, + { + name: 'Ended', + value: 'ended', + }, + { + name: 'Missed', + value: 'missed', + }, + { + name: 'Expired', + value: 'expired', + }, + ], + default: '', + description: 'Meeting state for the meeting objects being requested', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'End date and time (inclusive) for the meeting. Acceptable format', + }, + { + displayName: 'Weblink', + name: 'webLink', + type: 'string', + default: '', + description: 'URL encoded link to information page for the meeting objects being requested', + }, + ], + }, + + // ---------------------------------------- + // meeting: update + // ---------------------------------------- + { + displayName: 'Meeting ID', + name: 'meetingId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'update', + ], + }, + }, + description: 'ID of the meeting', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + operation: [ + 'update', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Agenda', + name: 'agenda', + type: 'string', + default: '', + description: `The meeting's agenda. Cannot be longer that 1300 characters`, + }, + { + displayName: 'Allow Any User To Be Co-Host', + name: 'allowAnyUserToBeCoHost', + type: 'boolean', + default: false, + description: `Whether or not to allow any attendee with a host account on the target site to become a co-host when joining the meeting`, + }, + { + displayName: 'Allow Authenticated Devices', + name: 'allowAuthenticatedDevices', + type: 'boolean', + default: false, + description: `Whether or not to allow authenticated video devices in the meeting's organization to start or join the meeting without a prompt`, + }, + { + displayName: 'Allow First User To Be Co-Host', + name: 'allowFirstUserToBeCoHost', + type: 'boolean', + default: false, + description: `Whether or not to allow the first attendee of the meeting with a host account on the target site to become a co-host`, + }, + { + displayName: 'Enable Connect Audio Before Host', + name: 'enableConnectAudioBeforeHost', + type: 'boolean', + default: false, + description: `Whether or not to allow any attendee to connect audio in the meeting before the host joins the meeting`, + }, + { + displayName: 'Enabled Auto Record Meeting', + name: 'enabledAutoRecordMeeting', + type: 'boolean', + default: false, + description: `Whether or not meeting is recorded automatically`, + }, + { + displayName: 'Enabled Join Before Host', + name: 'enabledJoinBeforeHost', + type: 'boolean', + default: false, + description: `Whether or not to allow any attendee to join the meeting before the host joins the meeting`, + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + description: 'Date and time for the end of meeting. Acceptable format', + }, + { + displayName: 'Exclude Password', + name: 'excludePassword', + type: 'boolean', + default: false, + description: `Whether or not to exclude password from the meeting email invitation`, + }, + { + displayName: 'Host Email', + name: 'hostEmail', + type: 'string', + default: '', + description: `Email address for the meeting host. This attribute should only be set if the user or application calling the API has the admin-level scopes`, + }, + { + displayName: 'Invitees', + name: 'inviteesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: '', + placeholder: 'Add Invitee', + options: [ + { + displayName: 'Invitee', + name: 'inviteeValues', + values: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'Email address of meeting invitee', + }, + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + default: '', + description: 'Display name of meeting invitee', + }, + { + displayName: 'Co-Host', + name: 'coHost', + type: 'boolean', + default: false, + description: 'Whether or not invitee is allowed to be a co-host for the meeting', + }, + ], + }, + ], + }, + { + displayName: 'Join Before Host Minutes', + name: 'joinBeforeHostMinutes', + type: 'options', + options: [ + { + name: '0', + value: 0, + }, + { + name: '5', + value: 5, + }, + { + name: '10', + value: 10, + }, + { + name: '15', + value: 15, + }, + ], + default: 0, + description: `The number of minutes an attendee can join the meeting before the meeting start time and the host joins`, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: `Meeting password. Must conform to the site's password complexity settings.
+ If not specified, a random password conforming to the site's password rules will be generated automatically`, + }, + { + displayName: 'Public Meeting', + name: 'publicMeeting', + type: 'boolean', + default: false, + description: `Whether or not to allow the meeting to be listed on the public calendar`, + }, + { + displayName: 'Recurrence', + name: 'recurrence', + type: 'string', + default: '', + description: `Meeting series recurrence rule (conforming with RFC 2445), applying only to meeting series`, + }, + { + displayName: 'Required Registration Info', + name: 'requireRegistrationInfo', + type: 'multiOptions', + options: [ + { + name: 'Require First Name', + value: 'requireFirstName', + }, + { + name: 'Require Last Name', + value: 'requireLastName', + }, + { + name: 'Require Email', + value: 'requireEmail', + }, + { + name: 'Require Job Title', + value: 'requireJobTitle', + }, + { + name: 'Require Company Name', + value: 'requireCompanyName', + }, + { + name: 'Require Address 1', + value: 'requireAddress1', + }, + { + name: 'Require Address 2', + value: 'requireAddress2', + }, + { + name: 'Require City', + value: 'requireCity', + }, + { + name: 'Require State', + value: 'requireState', + }, + { + name: 'Require Zip Code', + value: 'requireZipCode', + }, + { + name: 'Require Country Region', + value: 'requireCountryRegion', + }, + { + name: 'Require Work Phone', + value: 'requireWorkPhone', + }, + { + name: 'Require Fax', + value: 'requireFax', + }, + ], + default: [], + description: 'Data required for meeting registration', + }, + { + displayName: 'Reminder Time', + name: 'reminderTime', + type: 'number', + default: 1, + description: `The number of minutes before the meeting begins, for sending an email reminder to the host`, + }, + { + displayName: 'Send Email', + name: 'sendEmail', + type: 'boolean', + default: false, + description: `Whether or not to send emails to host and invitees. It is an optional field and default value is true`, + }, + { + displayName: 'Site URL', + name: 'siteUrl', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: `URL of the Webex site which the meeting is created on. If not specified, the meeting is created on user's preferred site`, + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + description: 'Date and time for the start of meeting. Acceptable format', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Meeting title', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Cisco/Webex/descriptions/MeetingTranscript.ts b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MeetingTranscript.ts new file mode 100644 index 0000000000..ed87bbc34e --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MeetingTranscript.ts @@ -0,0 +1,196 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const meetingTranscriptOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + }, + }, + options: [ + { + name: 'Download', + value: 'download', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'download', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const meetingTranscriptFields = [ + // ---------------------------------------- + // meetingTranscript: download + // ---------------------------------------- + { + displayName: 'Transcript ID', + name: 'transcriptId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + operation: [ + 'download', + ], + }, + }, + description: 'Unique identifier for the meeting transcript', + }, + { + displayName: 'Meeting ID', + name: 'meetingId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + operation: [ + 'download', + ], + }, + }, + description: 'Unique identifier for the meeting instance which the transcripts belong to', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + operation: [ + 'download', + ], + }, + }, + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'txt', + value: 'txt', + }, + { + name: 'vtt', + value: 'vtt', + }, + ], + default: 'vtt', + description: 'Format for the downloaded meeting transcript', + }, + ], + }, + + // ---------------------------------------- + // meetingTranscript: getAll + // ---------------------------------------- + { + displayName: 'Meeting ID', + name: 'meetingId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Unique identifier for the meeting instance which the transcripts belong to', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'meetingTranscript', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Host Email', + name: 'hostEmail', + type: 'string', + default: '', + description: 'Email address for the meetingTranscript host', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Cisco/Webex/descriptions/MessageDescription.ts b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MessageDescription.ts new file mode 100644 index 0000000000..87a3984655 --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MessageDescription.ts @@ -0,0 +1,657 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + getActionInheritedProperties, getInputTextProperties, getTextBlockProperties, +} from '../GenericFunctions'; + +export const messageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const messageFields = [ + // ---------------------------------------- + // message: create + // ---------------------------------------- + { + displayName: 'Destination', + name: 'destination', + type: 'options', + options: [ + { + name: 'Room', + value: 'room', + }, + { + name: 'Person', + value: 'person', + }, + ], + required: true, + default: 'room', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Room ID', + name: 'roomId', + description: ' The room ID', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getRooms', + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + destination: [ + 'room', + ], + }, + }, + }, + { + displayName: 'Specify Person By', + name: 'specifyPersonBy', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'ID', + value: 'id', + }, + ], + required: true, + default: 'email', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + destination: [ + 'person', + ], + }, + }, + }, + { + displayName: 'Person ID', + name: 'toPersonId', + description: 'The person ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + specifyPersonBy: [ + 'id', + ], + }, + }, + }, + { + displayName: 'Person Email', + name: 'toPersonEmail', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + specifyPersonBy: [ + 'email', + ], + }, + }, + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The message, in plain text', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Attachments', + name: 'attachmentsUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Attachment', + options: [ + { + displayName: 'Attachment', + name: 'attachmentValues', + values: [ + { + displayName: 'Elements', + name: 'elementsUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + placeholder: 'Add Element', + options: [ + { + displayName: 'Element', + name: 'elementValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Text Block', + value: 'textBlock', + }, + { + name: 'Input Text', + value: 'inputText', + }, + ], + default: 'textBlock', + description: '', + }, + ...getTextBlockProperties(), + ...getInputTextProperties(), + ], + }, + ], + }, + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + placeholder: 'Add Action', + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Execute', + value: 'execute', + }, + { + name: 'Open URL', + value: 'openUrl', + }, + { + name: 'Submit', + value: 'submit', + }, + ], + default: 'openUrl', + description: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + displayOptions: { + show: { + type: [ + 'openUrl', + ], + }, + }, + description: 'The URL to open', + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + displayOptions: { + show: { + type: [ + 'submit', + 'execute', + ], + }, + }, + default: '', + description: 'Any extra data to pass along. These are essentially ‘hidden’ properties', + }, + { + displayName: 'Verb', + name: 'verb', + type: 'string', + displayOptions: { + show: { + type: [ + 'execute', + ], + }, + }, + default: '', + description: 'The card author-defined verb associated with this action', + }, + ...getActionInheritedProperties(), + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'File', + name: 'fileUi', + placeholder: 'Add File', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'fileValue', + displayName: 'File', + values: [ + { + displayName: 'File Location', + name: 'fileLocation', + type: 'options', + options: [ + { + name: 'URL', + value: 'url', + }, + { + name: 'Binary Data', + value: 'binaryData', + }, + ], + default: 'url', + description: '', + }, + { + displayName: 'Input Field With File', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + fileLocation: [ + 'binaryData', + ], + }, + }, + description: 'The field in the node input containing the binary file data', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + displayOptions: { + show: { + fileLocation: [ + 'url', + ], + }, + }, + description: 'The public URL of the file', + }, + ], + }, + ], + }, + { + displayName: 'Markdown', + name: 'markdown', + type: 'string', + default: '', + description: 'The message in markdown format. When used the text parameter is used to provide alternate text for UI clients that do not support rich text', + }, + ], + }, + + // ---------------------------------------- + // message: delete + // ---------------------------------------- + { + displayName: 'Message ID', + name: 'messageId', + description: 'ID of the message to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // message: get + // ---------------------------------------- + { + displayName: 'Message ID', + name: 'messageId', + description: 'ID of the message to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // message: getAll + // ---------------------------------------- + { + displayName: 'Room ID', + name: 'roomId', + description: 'List messages in a room, by ID', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getRooms', + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Before', + name: 'before', + description: 'List messages sent before a date and time', + type: 'dateTime', + default: '', + }, + { + displayName: 'Before Message', + name: 'beforeMessage', + description: 'List messages sent before a message, by ID', + type: 'string', + default: '', + }, + { + displayName: 'Parent Message ID', + name: 'parentId', + description: 'List messages with a parent, by ID', + type: 'string', + default: '', + }, + { + displayName: 'Mentioned Person', + name: 'mentionedPeople', + type: 'string', + default: '', + description: `List only messages with certain person mentioned. Enter their ID. You can use 'me' as a shorthand for yourself`, + }, + ], + }, + + // ---------------------------------------- + // message: update + // ---------------------------------------- + { + displayName: 'Message ID', + name: 'messageId', + description: 'ID of the message to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Is Markdown', + name: 'markdown', + description: 'Whether the message uses markdown', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + markdown: [ + false, + ], + }, + }, + description: 'The message, in plain text', + }, + { + displayName: 'Markdown', + name: 'markdownText', + description: 'The message, in Markdown format. The maximum message length is 7439 bytes', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + markdown: [ + true, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Cisco/Webex/descriptions/index.ts b/packages/nodes-base/nodes/Cisco/Webex/descriptions/index.ts new file mode 100644 index 0000000000..9d94b4758b --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/descriptions/index.ts @@ -0,0 +1,3 @@ +export * from './MessageDescription'; +export * from './MeetingDescription'; +export * from './MeetingTranscript'; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d54ae825e9..6831c15107 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -53,6 +53,7 @@ "dist/credentials/BubbleApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CircleCiApi.credentials.js", + "dist/credentials/CiscoWebexOAuth2Api.credentials.js", "dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", "dist/credentials/ClickUpOAuth2Api.credentials.js", @@ -327,6 +328,8 @@ "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/CircleCi/CircleCi.node.js", + "dist/nodes/Cisco/Webex/CiscoWebex.node.js", + "dist/nodes/Cisco/Webex/CiscoWebexTrigger.node.js", "dist/nodes/Clearbit/Clearbit.node.js", "dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js",