Add Cisco Webex Node & Trigger (#1865)

*  Cisco Webex Node & Trigger

*  Improvements

*  Improvements

*  Improvements

*  Minor improvements

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ricardo Espinoza 2021-07-23 16:28:18 -04:00 committed by GitHub
parent 2afd93520a
commit ffa7bba6cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 3714 additions and 0 deletions

View file

@ -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',
},
];
}

View file

@ -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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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<br />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<boolean> {
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<boolean> {
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<boolean> {
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<IWebhookResponseData> {
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),
],
};
}
}

View file

@ -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<any> { // 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<any> { // 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');
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#7cb342" d="M32.334,13.733c0.092,0.706,0.144,1.424,0.144,2.155c0,9.181-7.463,16.623-16.669,16.623 c-0.733,0-1.453-0.052-2.161-0.144C14.305,33.584,15.895,36.132,19,38c0.699,0.421,3.13,1.851,6,2c7.195,0.374,14.844-7.424,15-15 c0.021-1.024,0.041-4.057-2-7C36.061,15.203,33.379,14.101,32.334,13.733z"/><path fill="#29b6f6" d="M10.661,24c0-7.315,5.947-13.246,13.283-13.246c3.668,0,6.989,1.483,9.392,3.88l4.789-4.776 C34.496,6.239,29.482,4,23.944,4C12.867,4,3.888,12.954,3.888,24c0,5.523,2.245,10.523,5.874,14.142l4.789-4.776 C12.148,30.969,10.661,27.658,10.661,24z"/><path fill="#244b71" d="M38.126,9.858c-1.323-1.319-3.467-1.319-4.789,0c-1.323,1.319-1.323,3.457,0,4.776c0,0,0,0,0,0 c2.404,2.397,3.89,5.708,3.89,9.366c0,7.315-5.947,13.246-13.283,13.246c-3.668,0-6.989-1.483-9.392-3.88l0,0 c-1.323-1.319-3.467-1.319-4.789,0c-1.323,1.319-1.323,3.457,0,4.776C13.392,41.761,18.406,44,23.944,44C35.021,44,44,35.046,44,24 C44,18.477,41.755,13.477,38.126,9.858z"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -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 <a href="https://datatracker.ietf.org/doc/html/rfc2445" target="_blank"> format</a>',
},
{
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 <a href="https://datatracker.ietf.org/doc/html/rfc2445" target="_blank"> format</a>',
},
{
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 <a href="https://datatracker.ietf.org/doc/html/rfc2445" target="_blank"> format</a>`,
},
{
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 <a href="https://datatracker.ietf.org/doc/html/rfc2445" target="_blank"> format</a>',
},
{
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 <a href="https://datatracker.ietf.org/doc/html/rfc2445" target="_blank"> format</a>',
},
{
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 <a href="https://datatracker.ietf.org/doc/html/rfc2445" target="_blank"> format</a>',
},
{
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.</br>
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 <a href="https://datatracker.ietf.org/doc/html/rfc2445" target="_blank"> format</a>',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Meeting title',
},
],
},
] as INodeProperties[];

View file

@ -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[];

View file

@ -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[];

View file

@ -0,0 +1,3 @@
export * from './MessageDescription';
export * from './MeetingDescription';
export * from './MeetingTranscript';

View file

@ -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",