From 355ccc3201f388b6c0160b8f53339e8fd571845b Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 16 Jun 2020 17:46:38 -0700 Subject: [PATCH 01/44] add authentication --- .../credentials/ZoomApi.credentials.ts | 14 ++ .../credentials/ZoomOAuth2Api.credentials.ts | 50 +++++++ .../nodes-base/nodes/Zoom/GenericFunctions.ts | 128 ++++++++++++++++++ packages/nodes-base/nodes/Zoom/Zoom.node.ts | 103 ++++++++++++++ packages/nodes-base/nodes/Zoom/zoom.png | Bin 0 -> 1848 bytes packages/nodes-base/package.json | 3 + 6 files changed, 298 insertions(+) create mode 100644 packages/nodes-base/credentials/ZoomApi.credentials.ts create mode 100644 packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Zoom/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Zoom/Zoom.node.ts create mode 100644 packages/nodes-base/nodes/Zoom/zoom.png diff --git a/packages/nodes-base/credentials/ZoomApi.credentials.ts b/packages/nodes-base/credentials/ZoomApi.credentials.ts new file mode 100644 index 0000000000..3db4aadbe0 --- /dev/null +++ b/packages/nodes-base/credentials/ZoomApi.credentials.ts @@ -0,0 +1,14 @@ +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; + +export class ZoomApi implements ICredentialType { + name = 'zoomApi'; + displayName = 'Zoom API'; + properties = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '' + } + ]; +} diff --git a/packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts new file mode 100644 index 0000000000..2b05c819a7 --- /dev/null +++ b/packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts @@ -0,0 +1,50 @@ +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; + +const userScopes = [ + 'meeting:read', + 'meeting:write', + 'user:read', + 'user:write', + 'user_profile', + 'webinar:read', + 'webinar:write' +]; + +export class ZoomOAuth2Api implements ICredentialType { + name = 'zoomOAuth2Api'; + extends = ['oAuth2Api']; + displayName = 'Zoom OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://zoom.us/oauth/authorize' + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://zoom.us/oauth/token' + }, + + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '' + }, + { + 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/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts new file mode 100644 index 0000000000..0e2feda8db --- /dev/null +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -0,0 +1,128 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions +} from 'n8n-core'; + +import { IDataObject } from 'n8n-workflow'; +import * as _ from 'lodash'; + +export async function zoomApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: object = {}, + query: object = {}, + headers: {} | undefined = undefined, + option: {} = {} +): Promise { + // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter( + 'authentication', + 0, + 'accessToken' + ) as string; + let options: OptionsWithUri = { + method, + headers: headers || { + 'Content-Type': 'application/json' + }, + body, + qs: query, + uri: `https://zoom.us/oauth${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(query).length === 0) { + delete options.qs; + } + try { + if (authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('zoomApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + options.headers!.Authorization = `Bearer ${credentials.accessToken}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call( + this, + 'zoomOAuth2Api', + options + ); + } + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Zoom credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error( + `Zoom error response [${error.statusCode}]: ${error.response.body.message}` + ); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function zoomApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: any = {}, + query: IDataObject = {} +): Promise { + // tslint:disable-line:no-any + const returnData: IDataObject[] = []; + let responseData; + query.page = 1; + query.count = 100; + do { + responseData = await zoomApiRequest.call( + this, + method, + endpoint, + body, + query + ); + query.cursor = encodeURIComponent( + _.get(responseData, 'response_metadata.next_cursor') + ); + query.page++; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + (responseData.response_metadata !== undefined && + responseData.response_metadata.mext_cursor !== undefined && + responseData.response_metadata.next_cursor !== '' && + responseData.response_metadata.next_cursor !== null) || + (responseData.paging !== undefined && + responseData.paging.pages !== undefined && + responseData.paging.page !== undefined && + responseData.paging.page < responseData.paging.pages) + ); + + return returnData; +} + +export function validateJSON(json: string | undefined): any { + // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts new file mode 100644 index 0000000000..ce32b4c51b --- /dev/null +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -0,0 +1,103 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription +} from 'n8n-workflow'; +import { + zoomApiRequest, + zoomApiRequestAllItems, + validateJSON +} from './GenericFunctions'; +export class Zoom implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zoom', + name: 'zoom', + group: ['input'], + version: 1, + description: 'Consume Zoom API', + defaults: { + name: 'Zoom', + color: '#772244' + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'zoomApi', + required: true, + displayOptions: { + show: { + authentication: ['accessToken'] + } + } + }, + { + name: 'zoomOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: ['oAuth2'] + } + } + } + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken' + }, + { + name: 'OAuth2', + value: 'oAuth2' + } + ], + default: 'accessToken', + description: 'The resource to operate on.' + } + ] + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + let qs: IDataObject; + let responseData; + const authentication = this.getNodeParameter('authentication', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + qs = {}; + if (resource === 'channel') { + //https://api.slack.com/methods/conversations.archive + if (operation === 'archive') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel + }; + responseData = await zoomApiRequest.call( + this, + 'POST', + '/conversations.archive', + body, + qs + ); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Zoom/zoom.png b/packages/nodes-base/nodes/Zoom/zoom.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc72331ebddb8f4f2bfded903500be639581567 GIT binary patch literal 1848 zcmV-82gmq{P)Px%JbF}EbW&k=AaHVTW@&6?Aar?fWgvKMZ~y=}jg?hP zvfLmH+;fT?0ZT$M$MFnim#Qpt{B*;7-uQVkiCtx5B-E{z!0GqTU+D+0SS9IId#sUt zarKLrFv%`nJiPj@O=Rzv%cYB8zPfRygcfU>twlD4@9H(#8e3pt(b#M+=6EBZDi(=o z*Ilw+W7REI$3kxe^F=bh{px!tZHWkAW8TR_w`q8|4<^qrn7eR=Jyu9AtF@AokX(;3 zrA2mCY2cG}QJ^aF#oVH%g=5TJF)PC^((#^8Ptu0lHXJv&gyh35xAX)o;8VoR{E(WY zVPtx181Aea(&;fS*#lm(55@uHMJw+I6T!1h05A#-)Q5_oij4@gzp?-YlnR9quS2N{ zh2U|w`;s774+u+EQ`oPypupex4zT@MB8)X**pSK!KAGepZNAs)7}6TL=S|r38uvUb z0=ARj8dli4L*w+B^m%YLcR(-rbcRczLnFb0GUHR~iOujvfeV5dYgY)+{rasbGZ?{v zPC7=J4O$d!m<1g>cHpe(@R0fh8H#O2E0Ux}q+hJssN8fyN_A|}O?d@c5S@;VSkb)I zgnf^1Y*1UUE^AISVpxZ{D*QlbK|V)hrlC=|3(4>Z%+5YRw&fl2JK*%_;aBuBdf0)E z4~$v4{H5TV-?X_)lm1bg_{1=sYV#HOgEn_*(gAt>Uu}e^0K~zWE&cC%d}{dgn&;7D z_I7%v=P5i*C%*!uG!K2k^At~i0DWd{JA-D?0ssI232;bRa{vGU0RR910RT@W#MS@+ z0Kia8R7L;)|0#0+DY^eCYXATL{V96?D7OFq{{Q{{|0uBkDyjb|ZvQB%|0{t1D5d`> zp#LbW|0{FUP>}zY$o^Hp|AxN*aHamW+Wp(+{&2?rNUr}pa{YDmg%SV&1Gz~= zK~zY`)t75ysxS#S=h0`rOkJY$Xx#LekY=iKf8|ErU$BL)MyWD&~x4T#m6|#L%aLQ;3GC zb4sdyc&(v7JRu(z95Jor$4Ro@{W+`MI^mp`L-X~`>ZXQPH*2ok8Y*u7*Xp8%ZeUq` z@%(`^Tsd9WVx?AVo<(MIyqA}k7qAX4&*=hp$sunDOd#+e2M5|Ex8b&0t3}IptHCdH zwNoB18@^s&{YJ08@`!ze1z+nT*RMXy%Z>otxdyebype25SWH1kAd~(DUe*H?zIxn8ifElSQGzW)pxRvbF(oHlIxF4}vWOU~rxJ z2S))5X<$mBGe(o|1TzHq$p*ifpS4eVgr9Vg(Vl$8!UAbOO$1zue0QgXvAwh84LHsi zrvTq6ktKX5q#ZN(0gUaMV%q3eiX7lMU1We5z*Mwb&VL8XF~4E2qWGu~4)U}qCWCMg z1W$9SsW6hIjgb)!5LP2kgFwLYT=C~ujG4wn!XY|NBYK1*3BMpLNv8ZHz?k>=s9*pz zgGWS#UtHIfaCJVwNaSu4DvIPj48U(1RpAhZ#*TA{V|hl?(EVWep4^Av=xSj1Wh)D* zG&|vRG)5cRm{a(K)JlV=E(xtx~ z9oi5BxY0kYu6ocYMD~}BZVffEU1RCAETcdei;A0q@R7`!d62lVRZ<=*z&}I(|JN63T z<8-_k*U)C1#)r8lHfB7C$!EI#xjmzwZTg8n97>lFb9`uh%paXm#Ico8{I^01|5Z3Y m^XEj^W09%Wm7f*)|MeG&CZon#`=kW`0000 Date: Wed, 17 Jun 2020 11:16:31 -0700 Subject: [PATCH 02/44] add resource --- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 56 +++++++++++++-------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index ce32b4c51b..ff7487a2e2 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -17,10 +17,12 @@ export class Zoom implements INodeType { group: ['input'], version: 1, description: 'Consume Zoom API', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', defaults: { name: 'Zoom', color: '#772244' }, + icon: 'file:zoom.png', inputs: ['main'], outputs: ['main'], credentials: [ @@ -60,6 +62,19 @@ export class Zoom implements INodeType { ], default: 'accessToken', description: 'The resource to operate on.' + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'meeting', + value: 'meeting' + } + ], + default: 'meeting', + description: 'The resource to operate on.' } ] }; @@ -72,26 +87,27 @@ export class Zoom implements INodeType { let responseData; const authentication = this.getNodeParameter('authentication', 0) as string; const resource = this.getNodeParameter('resource', 0) as string; - const operation = this.getNodeParameter('operation', 0) as string; - for (let i = 0; i < length; i++) { - qs = {}; - if (resource === 'channel') { - //https://api.slack.com/methods/conversations.archive - if (operation === 'archive') { - const channel = this.getNodeParameter('channelId', i) as string; - const body: IDataObject = { - channel - }; - responseData = await zoomApiRequest.call( - this, - 'POST', - '/conversations.archive', - body, - qs - ); - } - } - } + // const operation = this.getNodeParameter('operation', 0) as string; + console.log(this.getCredentials('zoomOAuth2Api')); + // for (let i = 0; i < length; i++) { + // qs = {}; + // if (resource === 'channel') { + // //https://api.slack.com/methods/conversations.archive + // if (operation === 'archive') { + // const channel = this.getNodeParameter('channelId', i) as string; + // const body: IDataObject = { + // channel + // }; + // responseData = await zoomApiRequest.call( + // this, + // 'POST', + // '/conversations.archive', + // body, + // qs + // ); + // } + // } + // } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { From b95e3464a45112f7202cb36829226746d571084e Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Mon, 22 Jun 2020 11:51:15 -0700 Subject: [PATCH 03/44] add meeting functionality --- CONTRIBUTING.md | 10 +- LICENSE.md | 2 +- packages/cli/LICENSE.md | 2 +- packages/core/LICENSE.md | 2 +- packages/editor-ui/LICENSE.md | 2 +- packages/node-dev/LICENSE.md | 2 +- packages/nodes-base/LICENSE.md | 2 +- .../nodes/Salesforce/GenericFunctions.ts | 2 +- packages/nodes-base/nodes/Slack/Slack.node.ts | 114 +-- .../nodes-base/nodes/Zoom/GenericFunctions.ts | 12 +- .../nodes/Zoom/MeetingDescription.ts | 751 ++++++++++++++++++ .../Zoom/MeetingRegistrantDescription.ts | 407 ++++++++++ packages/nodes-base/nodes/Zoom/Zoom.node.ts | 355 ++++++++- .../nodes-base/nodes/Zoom/ZoomOperations.ts | 71 -- packages/workflow/LICENSE.md | 2 +- 15 files changed, 1561 insertions(+), 175 deletions(-) create mode 100644 packages/nodes-base/nodes/Zoom/MeetingDescription.ts create mode 100644 packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts delete mode 100644 packages/nodes-base/nodes/Zoom/ZoomOperations.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 568f8fc9a7..fcc046beef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ The most important directories: execution, active webhooks and workflows - [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor - - [/packages/node-dev](/packages/node-dev) - Simple CLI to create new n8n-nodes + - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes - [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes - [/packages/workflow](/packages/workflow) - Workflow code with interfaces which get used by front- & backend @@ -159,7 +159,7 @@ tests of all packages. ## Create Custom Nodes -It is very easy to create own nodes for n8n. More information about that can +It is very straightforward to create your own nodes for n8n. More information about that can be found in the documentation of "n8n-node-dev" which is a small CLI which helps with n8n-node-development. @@ -177,9 +177,9 @@ If you want to create a node which should be added to n8n follow these steps: 1. Create a new folder for the new node. For a service named "Example" the folder would be called: `/packages/nodes-base/nodes/Example` - 1. If there is already a similar node simply copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder. + 1. If there is already a similar node, copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder. - 1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to simply copy existing similar ones. + 1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to copy existing similar ones. 1. Add the path to the new node (and optionally credentials) to package.json of `nodes-base`. It already contains a property `n8n` with its own keys `credentials` and `nodes`. @@ -236,6 +236,6 @@ docsify serve ./docs That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button. -We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally just a few lines long. +We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally only a few lines long. A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in. diff --git a/LICENSE.md b/LICENSE.md index aac54547eb..24a7d38fc9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/cli/LICENSE.md b/packages/cli/LICENSE.md index aac54547eb..24a7d38fc9 100644 --- a/packages/cli/LICENSE.md +++ b/packages/cli/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/core/LICENSE.md b/packages/core/LICENSE.md index aac54547eb..24a7d38fc9 100644 --- a/packages/core/LICENSE.md +++ b/packages/core/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/editor-ui/LICENSE.md b/packages/editor-ui/LICENSE.md index aac54547eb..24a7d38fc9 100644 --- a/packages/editor-ui/LICENSE.md +++ b/packages/editor-ui/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/node-dev/LICENSE.md b/packages/node-dev/LICENSE.md index aac54547eb..24a7d38fc9 100644 --- a/packages/node-dev/LICENSE.md +++ b/packages/node-dev/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/nodes-base/LICENSE.md b/packages/nodes-base/LICENSE.md index aac54547eb..24a7d38fc9 100644 --- a/packages/nodes-base/LICENSE.md +++ b/packages/nodes-base/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index 648735feb2..4c81432238 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -30,7 +30,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin } } -export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 57fe569d22..bd3a7b3c21 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -289,7 +289,7 @@ export class Slack implements INodeType { if (operation === 'get') { const channel = this.getNodeParameter('channelId', i) as string; qs.channel = channel, - responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs); + responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs); responseData = responseData.channel; } //https://api.slack.com/methods/conversations.list @@ -456,7 +456,7 @@ export class Slack implements INodeType { if (!jsonParameters) { const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; - const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[]; + const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[]; // The node does save the fields data differently than the API // expects so fix the data befre we send the request @@ -482,7 +482,7 @@ export class Slack implements INodeType { block.block_id = blockUi.blockId as string; block.type = blockUi.type as string; if (block.type === 'actions') { - const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[]; + const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[]; if (elementsUi) { for (const elementUi of elementsUi) { const element: Element = {}; @@ -498,7 +498,7 @@ export class Slack implements INodeType { text: elementUi.text as string, type: 'plain_text', emoji: elementUi.emoji as boolean, - }; + }; if (elementUi.url) { element.url = elementUi.url as string; } @@ -508,13 +508,13 @@ export class Slack implements INodeType { if (elementUi.style !== 'default') { element.style = elementUi.style as string; } - const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject; - if (confirmUi) { + const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject; + if (confirmUi) { const confirm: Confirm = {}; - const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; - const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; - const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; - const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; + const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; + const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; + const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; + const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; const style = confirmUi.style as string; if (titleUi) { confirm.title = { @@ -548,13 +548,13 @@ export class Slack implements INodeType { confirm.style = style as string; } element.confirm = confirm; - } - elements.push(element); + } + elements.push(element); } block.elements = elements; } } else if (block.type === 'section') { - const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject; + const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject; if (textUi) { const text: Text = {}; if (textUi.type === 'plainText') { @@ -569,7 +569,7 @@ export class Slack implements INodeType { } else { throw new Error('Property text must be defined'); } - const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[]; if (fieldsUi) { const fields: Text[] = []; for (const fieldUi of fieldsUi) { @@ -589,7 +589,7 @@ export class Slack implements INodeType { block.fields = fields; } } - const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject; + const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject; if (accessoryUi) { const accessory: Element = {}; if (accessoryUi.type === 'button') { @@ -608,46 +608,46 @@ export class Slack implements INodeType { if (accessoryUi.style !== 'default') { accessory.style = accessoryUi.style as string; } - const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject; + const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject; if (confirmUi) { - const confirm: Confirm = {}; - const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; - const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; - const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; - const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; - const style = confirmUi.style as string; - if (titleUi) { - confirm.title = { - type: 'plain_text', - text: titleUi.text as string, - emoji: titleUi.emoji as boolean, - }; - } - if (textUi) { - confirm.text = { - type: 'plain_text', - text: textUi.text as string, - emoji: textUi.emoji as boolean, - }; - } - if (confirmTextUi) { - confirm.confirm = { - type: 'plain_text', - text: confirmTextUi.text as string, - emoji: confirmTextUi.emoji as boolean, - }; - } - if (denyUi) { - confirm.deny = { - type: 'plain_text', - text: denyUi.text as string, - emoji: denyUi.emoji as boolean, - }; - } - if (style !== 'default') { - confirm.style = style as string; - } - accessory.confirm = confirm; + const confirm: Confirm = {}; + const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; + const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; + const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; + const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; + const style = confirmUi.style as string; + if (titleUi) { + confirm.title = { + type: 'plain_text', + text: titleUi.text as string, + emoji: titleUi.emoji as boolean, + }; + } + if (textUi) { + confirm.text = { + type: 'plain_text', + text: textUi.text as string, + emoji: textUi.emoji as boolean, + }; + } + if (confirmTextUi) { + confirm.confirm = { + type: 'plain_text', + text: confirmTextUi.text as string, + emoji: confirmTextUi.emoji as boolean, + }; + } + if (denyUi) { + confirm.deny = { + type: 'plain_text', + text: denyUi.text as string, + emoji: denyUi.emoji as boolean, + }; + } + if (style !== 'default') { + confirm.style = style as string; + } + accessory.confirm = confirm; } } block.accessory = accessory; @@ -790,8 +790,8 @@ export class Slack implements INodeType { if (binaryData) { const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; if (items[i].binary === undefined - //@ts-ignore - || items[i].binary[binaryPropertyName] === undefined) { + //@ts-ignore + || items[i].binary[binaryPropertyName] === undefined) { throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); } body.file = { @@ -804,7 +804,7 @@ export class Slack implements INodeType { contentType: items[i].binary[binaryPropertyName].mimeType, } }; - responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body }); + responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body }); responseData = responseData.file; } else { const fileContent = this.getNodeParameter('fileContent', i) as string; diff --git a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts index e8e0247461..9cde3e67e2 100644 --- a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -40,21 +40,11 @@ export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFun throw new Error('No credentials got returned!'); } options.headers!.Authorization = `Bearer ${credentials.accessToken}`; - console.log("options if"); - console.log(options); + //@ts-ignore return await this.helpers.request(options); } else { - console.log("options else"); - console.log(options); - let credentials = this.getCredentials('zoomOAuth2Api'); - // let oauthtoken1 = credentials!.oauthTokenData; - - - console.log(credentials); - console.log("credss"); - //@ts-ignore return await this.helpers.requestOAuth2.call(this, 'zoomOAuth2Api', options); diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts new file mode 100644 index 0000000000..bd4b8ce271 --- /dev/null +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -0,0 +1,751 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const meetingOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'meeting', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a meeting', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a meeting', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a meeting', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all meetings', + }, + { + name: 'Update', + value: 'update', + description: 'Update a meeting', + } + ], + default: 'create', + description: 'The operation to perform.', + } +] as INodeProperties[]; + +export const meetingFields = [ + /* -------------------------------------------------------------------------- */ + /* meeting:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Id', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + + + ], + resource: [ + 'meeting', + ], + }, + }, + description: 'User ID.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + + ], + resource: [ + 'meeting', + ], + } + }, + options: [ + { + displayName: 'Meeting topic', + name: 'topic', + type: 'string', + default: '', + description: `Meeting topic.`, + }, + { + displayName: 'Meeting type', + name: 'type', + type: 'options', + options: [ + { + name: 'Instant Meeting', + value: 1, + }, + { + name: 'Scheduled Meeting', + value: 2, + }, + { + name: 'Recurring meeting with no fixed time', + value: 3, + }, + { + name: 'Recurring meeting with no fixed time', + value: 8, + }, + + ], + default: 2, + description: 'Meeting type.' + }, + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring meetings with fixed time', + }, + { + displayName: 'Duration', + name: 'duration', + type: 'number', + default: '', + description: 'Duration.', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Schedule for', + name: 'scheduleFor', + type: 'string', + default: '', + description: 'Schedule meeting for someone else from your account, provide their email id.', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password to join the meeting with maximum 10 characters.', + }, + { + displayName: 'Agenda', + name: 'agenda', + type: 'string', + default: '', + description: 'Meeting agenda.', + }, + { + displayName: 'Host Meeting in China', + name: 'cn_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in China.', + }, + { + displayName: 'Host Meeting in India', + name: 'in_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in India.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the meeting.', + }, + { + displayName: 'Participant Video', + name: 'participant_video', + type: 'boolean', + default: false, + description: 'Start video when participant joins the meeting.', + }, + { + displayName: 'Join before Host', + name: 'join_before_host', + type: 'boolean', + default: false, + description: 'Allow participants to join the meeting before host starts it.', + }, + { + displayName: 'Muting before entry', + name: 'mute_upon_entry', + type: 'boolean', + default: false, + description: 'Mute participants upon entry.', + }, + { + displayName: 'Watermark', + name: 'watermark', + type: 'boolean', + default: false, + description: 'Adds watermark when viewing a shared screen.', + }, + { + displayName: 'Alternative Hosts', + name: 'alternative_hosts', + type: 'string', + default: '', + description: 'Alternative hosts email ids.', + }, + { + displayName: 'Auto recording', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Record on local', + value: 'local', + }, + { + name: 'Record on cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + + + ], + default: 'none', + description: 'Auto recording.', + }, + { + displayName: 'Audio', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Both Telephony and VoiP', + value: 'both', + }, + { + name: 'Telephony', + value: 'telephony', + }, + { + name: 'VOIP', + value: 'voip', + }, + + + ], + default: 'both', + description: 'Determine how participants can join audio portion of the meeting.', + }, + { + displayName: 'Registration type', + name: 'registration_type', + type: 'options', + options: [ + { + name: 'Attendees register once and can attend any of the occurences', + value: 1, + }, + { + name: 'Attendees need to register for every occurence', + value: 2, + }, + { + name: 'Attendees register once and can choose one or more occurences to attend', + value: 3, + }, + + + ], + default: 1, + description: 'Registration type. Used for recurring meetings with fixed time only', + }, + + + ], + }, + /* -------------------------------------------------------------------------- */ + /* meeting:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Id', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'meeting', + ], + }, + }, + description: 'User ID.', + }, + /* -------------------------------------------------------------------------- */ + /* meeting:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Id', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'meeting', + ], + }, + }, + description: 'User ID.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'meeting', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'meeting', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 300 + }, + default: 30, + description: 'How many results to return.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + + ], + resource: [ + 'meeting', + ], + } + }, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Scheduled', + value: 'scheduled', + }, + { + name: 'Live', + value: 'live', + }, + { + name: 'Upcoming', + value: 'upcoming', + }, + + + ], + default: 'live', + description: `Meeting type.`, + }, + ] + }, + /* -------------------------------------------------------------------------- */ + /* meeting:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Meeting Id', + name: 'meetingId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete' + ], + resource: [ + 'meeting', + ], + }, + }, + description: 'Meeting ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'meeting', + ], + }, + }, + options: [ + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + description: 'Meeting occurence Id.', + }, + { + displayName: 'Schedule a reminder', + name: 'scheduleForReminder', + type: 'boolean', + default: false, + description: 'Schedule a reminder via email', + }, + + + + ], + + }, + /* -------------------------------------------------------------------------- */ + /* meeting:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Meeting Id', + name: 'meetingId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + + + ], + resource: [ + 'meeting', + ], + }, + }, + description: 'Meeting ID.', + }, + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + + + ], + resource: [ + 'meeting', + ], + }, + }, + description: 'Occurence ID.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + + 'update', + ], + resource: [ + 'meeting', + ], + } + }, + options: [ + { + displayName: 'Meeting topic', + name: 'topic', + type: 'string', + default: '', + description: `Meeting topic.`, + }, + { + displayName: 'Meeting type', + name: 'type', + type: 'options', + options: [ + { + name: 'Instant Meeting', + value: 1, + }, + { + name: 'Scheduled Meeting', + value: 2, + }, + { + name: 'Recurring meeting with no fixed time', + value: 3, + }, + { + name: 'Recurring meeting with no fixed time', + value: 8, + }, + + ], + default: 2, + description: 'Meeting type.' + }, + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring meetings with fixed time', + }, + { + displayName: 'Duration', + name: 'duration', + type: 'number', + default: '', + description: 'Duration.', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Schedule for', + name: 'scheduleFor', + type: 'string', + default: '', + description: 'Schedule meeting for someone else from your account, provide their email id.', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password to join the meeting with maximum 10 characters.', + }, + { + displayName: 'Agenda', + name: 'agenda', + type: 'string', + default: '', + description: 'Meeting agenda.', + }, + { + displayName: 'Host Meeting in China', + name: 'cn_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in China.', + }, + { + displayName: 'Host Meeting in India', + name: 'in_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in India.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the meeting.', + }, + { + displayName: 'Participant Video', + name: 'participant_video', + type: 'boolean', + default: false, + description: 'Start video when participant joins the meeting.', + }, + { + displayName: 'Join before Host', + name: 'join_before_host', + type: 'boolean', + default: false, + description: 'Allow participants to join the meeting before host starts it.', + }, + { + displayName: 'Muting before entry', + name: 'mute_upon_entry', + type: 'boolean', + default: false, + description: 'Mute participants upon entry.', + }, + { + displayName: 'Watermark', + name: 'watermark', + type: 'boolean', + default: false, + description: 'Adds watermark when viewing a shared screen.', + }, + { + displayName: 'Alternative Hosts', + name: 'alternative_hosts', + type: 'string', + default: '', + description: 'Alternative hosts email ids.', + }, + { + displayName: 'Auto recording', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Record on local', + value: 'local', + }, + { + name: 'Record on cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + + + ], + default: 'none', + description: 'Auto recording.', + }, + { + displayName: 'Audio', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Both Telephony and VoiP', + value: 'both', + }, + { + name: 'Telephony', + value: 'telephony', + }, + { + name: 'VOIP', + value: 'voip', + }, + + + ], + default: 'both', + description: 'Determine how participants can join audio portion of the meeting.', + }, + { + displayName: 'Registration type', + name: 'registration_type', + type: 'options', + options: [ + { + name: 'Attendees register once and can attend any of the occurences', + value: 1, + }, + { + name: 'Attendees need to register for every occurence', + value: 2, + }, + { + name: 'Attendees register once and can choose one or more occurences to attend', + value: 3, + }, + + + ], + default: 1, + description: 'Registration type. Used for recurring meetings with fixed time only', + }, + + + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts new file mode 100644 index 0000000000..b062786892 --- /dev/null +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -0,0 +1,407 @@ +import { + INodeProperties, +} from 'n8n-workflow'; +export const meetingRegistrantOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'meetingRegistrants', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create Meeting Registrants', + }, + { + name: 'Update', + value: 'update', + description: 'Update Meeting Registrant status', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all meeting registrants', + }, + + ], + default: 'create', + description: 'The operation to perform.', + } +] as INodeProperties[]; + + + +export const meetingRegistrantFields = [ + /* -------------------------------------------------------------------------- */ + /* meetingRegistrants:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Meeting Id', + name: 'meetingId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + + + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, + description: 'Meeting ID.', + }, + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + + + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, + description: 'Occurence ID.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'Valid email-id of registrant.', + }, + { + displayName: 'First name', + name: 'firstName', + required: true, + type: 'string', + default: '', + description: 'First Name.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + + ], + resource: [ + 'meetingRegistrants', + ], + } + }, + options: [ + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last Name.', + }, + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + description: 'Valid address of registrant.', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'Valid city of registrant.', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'Valid state of registrant.', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Valid country of registrant.', + }, + { + displayName: 'Zip code', + name: 'zip', + type: 'string', + default: '', + description: 'Valid zip-code of registrant.', + }, + { + displayName: 'Phone Number', + name: 'phone', + type: 'string', + default: '', + description: 'Valid phone number of registrant.', + }, + { + displayName: 'Comments', + name: 'comments', + type: 'string', + default: '', + description: 'Allows registrants to provide any questions they have.', + }, + { + displayName: 'Organization', + name: 'org', + type: 'string', + default: '', + description: 'Organization of registrant.', + }, + { + displayName: 'Job title', + name: 'job_title', + type: 'string', + default: '', + description: 'Job title of registrant.', + }, + { + displayName: 'Purchasing time frame', + name: 'purchasing_time_frame', + type: 'options', + options: [ + { + name: 'Within a month', + value: 'Within a month', + }, + { + name: '1-3 months', + value: '1-3 months', + }, + { + name: '4-6 months', + value: '4-6 months', + }, + { + name: 'More than 6 months', + value: 'More than 6 months', + }, + { + name: 'No timeframe', + value: 'No timeframe', + }, + + + ], + default: '', + description: 'Meeting type.' + }, + { + displayName: 'Role in purchase process', + name: 'role_in_purchase_process', + type: 'options', + options: [ + { + name: 'Decision Maker', + value: 'Decision Maker', + }, + { + name: 'Evaluator/Recommender', + value: 'Evaluator/Recommender', + }, + { + name: 'Influener', + value: 'Influener', + }, + { + name: 'Not Involved', + value: 'Not Involved', + }, + + ], + default: '', + description: 'Role in purchase process.' + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* meetingRegistrants:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Meeting Id', + name: 'meetingId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + + + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, + description: 'Meeting ID.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'meetingRegistrants', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 300 + }, + default: 30, + description: 'How many results to return.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + + ], + resource: [ + 'meetingRegistrants', + ], + } + }, + options: [ + { + displayName: 'Occurence Id', + name: 'occurence_id', + type: 'string', + default: '', + description: `Occurence Id.`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Approved', + value: 'approved', + }, + { + name: 'Denied', + value: 'denied', + }, + + + ], + default: '', + description: `Registrant Status.`, + }, + + ] + }, + /* -------------------------------------------------------------------------- */ + /* meetingRegistrants:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Meeting Id', + name: 'meetingId', + type: 'number', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + + + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, + description: 'Meeting ID.', + }, + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + + + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, + description: 'Occurence ID.', + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 524fc08440..c1915d4ab4 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -3,7 +3,9 @@ import { IDataObject, INodeExecutionData, INodeType, + ILoadOptionsFunctions, INodeTypeDescription, + INodePropertyOptions, } from 'n8n-workflow'; import { zoomApiRequest, @@ -14,7 +16,30 @@ import { import { meetingOperations, meetingFields, -} from './ZoomOperations'; +} from './MeetingDescription'; + +import { + meetingRegistrantOperations, + meetingRegistrantFields, + +} from './MeetingRegistrantDescription'; + +import * as moment from 'moment-timezone'; + +interface Settings { + host_video?: boolean; + participant_video?: boolean; + cn_meeting?: boolean; + in_meeting?: boolean; + join_before_host?: boolean; + mute_upon_entry?: boolean; + watermark?: boolean; + audio?: string; + alternative_hosts?: string; + auto_recording?: string; + registration_type?: number; + +} export class Zoom implements INodeType { description: INodeTypeDescription = { displayName: 'Zoom', @@ -25,31 +50,11 @@ export class Zoom implements INodeType { subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', defaults: { name: 'Zoom', - color: '#772244' + color: '#0B6CF9' }, icon: 'file:zoom.png', inputs: ['main'], outputs: ['main'], - // credentials: [ - // { - // name: 'zoomApi', - // required: true, - // displayOptions: { - // show: { - // authentication: ['accessToken'] - // } - // } - // }, - // { - // name: 'zoomOAuth2Api', - // required: true, - // displayOptions: { - // show: { - // authentication: ['oAuth2'] - // } - // } - // } - // ], credentials: [ { name: 'zoomApi', @@ -100,16 +105,41 @@ export class Zoom implements INodeType { { name: 'Meeting', value: 'meeting' + }, + { + name: 'Meeting Registrants', + value: 'meetingRegistrants' } ], default: 'meeting', description: 'The resource to operate on.' }, ...meetingOperations, - ...meetingFields + ...meetingFields, + ...meetingRegistrantOperations, + ...meetingRegistrantFields, ] }; + methods = { + loadOptions: { + // Get all the timezones to display them to user so that he can select them easily + async getTimezones( + this: ILoadOptionsFunctions + ): Promise { + const returnData: INodePropertyOptions[] = []; + for (const timezone of moment.tz.names()) { + const timezoneName = timezone; + const timezoneId = timezone; + returnData.push({ + name: timezoneName, + value: timezoneId + }); + } + return returnData; + } + } + }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); @@ -120,11 +150,13 @@ export class Zoom implements INodeType { const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; console.log(this.getCredentials('zoomOAuth2Api')); + let body: IDataObject = {}; for (let i = 0; i < length; i++) { qs = {}; if (resource === 'meeting') { if (operation === 'get') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meeting const userId = this.getNodeParameter('userId', i) as string; responseData = await zoomApiRequest.call( @@ -135,6 +167,283 @@ export class Zoom implements INodeType { qs ); } + if (operation === 'getAll') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetings + const userId = this.getNodeParameter('userId', i) as string; + + responseData = await zoomApiRequest.call( + this, + 'GET', + `/users/${userId}/meetings`, + {}, + qs + ); + } + if (operation === 'delete') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingdelete + const meetingId = this.getNodeParameter('meetingId', i) as string; + responseData = await zoomApiRequest.call( + this, + 'DELETE', + `/meetings/${meetingId}`, + {}, + qs + ); + responseData = { success: true }; + } + if (operation === 'create') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate + const userId = this.getNodeParameter('userId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + const settings: Settings = {}; + if (additionalFields.cn_meeting) { + settings.cn_meeting = additionalFields.cn_meeting as boolean; + + } + + if (additionalFields.in_meeting) { + settings.in_meeting = additionalFields.in_meeting as boolean; + + } + + if (additionalFields.join_before_host) { + settings.join_before_host = additionalFields.join_before_host as boolean; + + } + + if (additionalFields.mute_upon_entry) { + settings.mute_upon_entry = additionalFields.mute_upon_entry as boolean; + + } + + if (additionalFields.watermark) { + settings.watermark = additionalFields.watermark as boolean; + + } + + if (additionalFields.audio) { + settings.audio = additionalFields.audio as string; + + } + + if (additionalFields.alternative_hosts) { + settings.alternative_hosts = additionalFields.alternative_hosts as string; + + } + + if (additionalFields.participant_video) { + settings.participant_video = additionalFields.participant_video as boolean; + + } + + if (additionalFields.host_video) { + settings.host_video = additionalFields.host_video as boolean; + + } + + if (additionalFields.auto_recording) { + settings.auto_recording = additionalFields.auto_recording as string; + + } + + if (additionalFields.registration_type) { + settings.registration_type = additionalFields.registration_type as number; + + } + + body = { + settings, + }; + + if (additionalFields.topic) { + body.topic = additionalFields.topic as string; + + } + + if (additionalFields.type) { + body.type = additionalFields.type as string; + + } + + if (additionalFields.startTime) { + body.start_time = additionalFields.startTime as string; + + } + + if (additionalFields.duration) { + body.duration = additionalFields.duration as number; + + } + + if (additionalFields.scheduleFor) { + body.schedule_for = additionalFields.scheduleFor as string; + + } + + if (additionalFields.timeZone) { + body.timezone = additionalFields.timeZone as string; + + } + + if (additionalFields.password) { + body.password = additionalFields.password as string; + + } + + if (additionalFields.agenda) { + body.agenda = additionalFields.agenda as string; + + } + + + + + responseData = await zoomApiRequest.call( + this, + 'POST', + `/users/${userId}/meetings`, + body, + qs + ); + } + if (operation === 'update') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingupdate + const meetingId = this.getNodeParameter('meetingId', i) as string; + qs.occurence_id = this.getNodeParameter('occurenceId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + const settings: Settings = {}; + if (additionalFields.cn_meeting) { + settings.cn_meeting = additionalFields.cn_meeting as boolean; + + } + + if (additionalFields.in_meeting) { + settings.in_meeting = additionalFields.in_meeting as boolean; + + } + + if (additionalFields.join_before_host) { + settings.join_before_host = additionalFields.join_before_host as boolean; + + } + + if (additionalFields.mute_upon_entry) { + settings.mute_upon_entry = additionalFields.mute_upon_entry as boolean; + + } + + if (additionalFields.watermark) { + settings.watermark = additionalFields.watermark as boolean; + + } + + if (additionalFields.audio) { + settings.audio = additionalFields.audio as string; + + } + + if (additionalFields.alternative_hosts) { + settings.alternative_hosts = additionalFields.alternative_hosts as string; + + } + + if (additionalFields.participant_video) { + settings.participant_video = additionalFields.participant_video as boolean; + + } + + if (additionalFields.host_video) { + settings.host_video = additionalFields.host_video as boolean; + + } + + if (additionalFields.auto_recording) { + settings.auto_recording = additionalFields.auto_recording as string; + + } + + if (additionalFields.registration_type) { + settings.registration_type = additionalFields.registration_type as number; + + } + + body = { + settings, + }; + if (additionalFields.topic) { + body.topic = additionalFields.topic as string; + + } + + if (additionalFields.type) { + body.type = additionalFields.type as string; + + } + + if (additionalFields.startTime) { + body.start_time = additionalFields.startTime as string; + + } + + if (additionalFields.duration) { + body.duration = additionalFields.duration as number; + + } + + if (additionalFields.scheduleFor) { + body.schedule_for = additionalFields.scheduleFor as string; + + } + + if (additionalFields.timeZone) { + body.timezone = additionalFields.timeZone as string; + + } + + if (additionalFields.password) { + body.password = additionalFields.password as string; + + } + + if (additionalFields.agenda) { + body.agenda = additionalFields.agenda as string; + + } + + responseData = await zoomApiRequest.call( + this, + 'PATCH', + `/meetings/${meetingId}`, + body, + qs + ); + } + } + if (resource === 'meetingRegistrant') { + if (operation === 'create') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantcreate + const meetingId = this.getNodeParameter('meetingId', i) as string; + qs.occurence_id = this.getNodeParameter('occurenceId', i) as string; + responseData = await zoomApiRequest.call( + this, + 'PATCH', + `/meetings/${meetingId}/registrants`, + body, + qs + ); + } + if (operation === 'getAll') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrants + } + if (operation === 'update') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantstatus + } } } if (Array.isArray(responseData)) { diff --git a/packages/nodes-base/nodes/Zoom/ZoomOperations.ts b/packages/nodes-base/nodes/Zoom/ZoomOperations.ts deleted file mode 100644 index 58f1825435..0000000000 --- a/packages/nodes-base/nodes/Zoom/ZoomOperations.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - INodeProperties, -} from 'n8n-workflow'; - -export const meetingOperations = [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - resource: [ - 'meeting', - ], - }, - }, - options: [ - { - name: 'Create', - value: 'create', - description: 'Create a meeting', - }, - { - name: 'Delete', - value: 'delete', - description: 'Delete a meeting', - }, - { - name: 'Get', - value: 'get', - description: 'Retrieve a meeting', - }, - { - name: 'Get All', - value: 'getAll', - description: 'Retrieve all meetings', - }, - { - name: 'Update', - value: 'update', - description: 'Update a meeting', - } - ], - default: 'create', - description: 'The operation to perform.', - } -] as INodeProperties[]; - -export const meetingFields = [ - /* -------------------------------------------------------------------------- */ - /* meeting:get */ - /* -------------------------------------------------------------------------- */ - { - displayName: 'User Id', - name: 'userId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'get', - ], - resource: [ - 'meeting', - ], - }, - }, - description: 'User ID.', - }, -] as INodeProperties[]; diff --git a/packages/workflow/LICENSE.md b/packages/workflow/LICENSE.md index aac54547eb..24a7d38fc9 100644 --- a/packages/workflow/LICENSE.md +++ b/packages/workflow/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 n8n GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 815f823f5ac6371d2796d30da68792d473f20baf Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Mon, 22 Jun 2020 18:39:21 -0700 Subject: [PATCH 04/44] add webinars and registrants resource --- .../nodes-base/nodes/Zoom/GenericFunctions.ts | 34 +- .../nodes/Zoom/MeetingDescription.ts | 107 ++- .../Zoom/MeetingRegistrantDescription.ts | 104 ++- .../nodes/Zoom/WebinarDescription.ts | 685 ++++++++++++++++++ packages/nodes-base/nodes/Zoom/Zoom.node.ts | 405 ++++++++++- 5 files changed, 1198 insertions(+), 137 deletions(-) create mode 100644 packages/nodes-base/nodes/Zoom/WebinarDescription.ts diff --git a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts index 9cde3e67e2..d361a9761a 100644 --- a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -31,8 +31,6 @@ export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFun if (Object.keys(query).length === 0) { delete options.qs; } - console.log("options"); - console.log(options); try { if (authenticationMethod === 'accessToken') { const credentials = this.getCredentials('zoomApi'); @@ -42,7 +40,6 @@ export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFun options.headers!.Authorization = `Bearer ${credentials.accessToken}`; //@ts-ignore - return await this.helpers.request(options); } else { //@ts-ignore @@ -78,9 +75,10 @@ export async function zoomApiRequestAllItems( ): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; + let responseData; - query.page = 1; - query.count = 100; + //query.maxResults = 300; + do { responseData = await zoomApiRequest.call( this, @@ -89,32 +87,14 @@ export async function zoomApiRequestAllItems( body, query ); - query.cursor = encodeURIComponent( - _.get(responseData, 'response_metadata.next_cursor') - ); - query.page++; + query.page_number = responseData['page_number']; returnData.push.apply(returnData, responseData[propertyName]); } while ( - (responseData.response_metadata !== undefined && - responseData.response_metadata.mext_cursor !== undefined && - responseData.response_metadata.next_cursor !== '' && - responseData.response_metadata.next_cursor !== null) || - (responseData.paging !== undefined && - responseData.paging.pages !== undefined && - responseData.paging.page !== undefined && - responseData.paging.page < responseData.paging.pages) + responseData['page_number'] !== undefined && + responseData['page_number'] !== '' ); return returnData; } -export function validateJSON(json: string | undefined): any { - // tslint:disable-line:no-any - let result; - try { - result = JSON.parse(json!); - } catch (exception) { - result = undefined; - } - return result; -} + diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index bd4b8ce271..274b48ff97 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -51,7 +51,7 @@ export const meetingFields = [ /* meeting:create */ /* -------------------------------------------------------------------------- */ { - displayName: 'Id', + displayName: 'User Id', name: 'userId', type: 'string', default: '', @@ -60,15 +60,13 @@ export const meetingFields = [ show: { operation: [ 'create', - - ], resource: [ 'meeting', ], }, }, - description: 'User ID.', + description: 'User ID or email address of user.', }, { displayName: 'Additional settings', @@ -239,15 +237,13 @@ export const meetingFields = [ name: 'Disabled', value: 'none', }, - - ], default: 'none', description: 'Auto recording.', }, { displayName: 'Audio', - name: 'auto_recording', + name: 'audio', type: 'options', options: [ { @@ -263,7 +259,6 @@ export const meetingFields = [ value: 'voip', }, - ], default: 'both', description: 'Determine how participants can join audio portion of the meeting.', @@ -285,22 +280,19 @@ export const meetingFields = [ name: 'Attendees register once and can choose one or more occurences to attend', value: 3, }, - - ], default: 1, description: 'Registration type. Used for recurring meetings with fixed time only', }, - ], }, /* -------------------------------------------------------------------------- */ /* meeting:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Id', - name: 'userId', + displayName: 'Meeting Id', + name: 'meetingId', type: 'string', default: '', required: true, @@ -314,13 +306,47 @@ export const meetingFields = [ ], }, }, - description: 'User ID.', + description: 'Meeting ID.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + + ], + resource: [ + 'meeting', + ], + }, + }, + options: [ + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + description: 'To view meeting details of a particular occurence of the recurring meeting.', + }, + { + displayName: 'Show Previous Occurences', + name: 'showPreviousOccurences', + type: 'boolean', + default: '', + description: 'To view meeting details of all previous occurences of the recurring meeting.', + }, + ], }, /* -------------------------------------------------------------------------- */ /* meeting:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'Id', + displayName: 'User Id', name: 'userId', type: 'string', default: '', @@ -335,7 +361,7 @@ export const meetingFields = [ ], }, }, - description: 'User ID.', + description: 'User ID or email-id.', }, { displayName: 'Return All', @@ -393,7 +419,7 @@ export const meetingFields = [ resource: [ 'meeting', ], - } + }, }, options: [ { @@ -413,8 +439,6 @@ export const meetingFields = [ name: 'Upcoming', value: 'upcoming', }, - - ], default: 'live', description: `Meeting type.`, @@ -471,11 +495,8 @@ export const meetingFields = [ name: 'scheduleForReminder', type: 'boolean', default: false, - description: 'Schedule a reminder via email', + description: 'Notify hosts and alternative hosts about meeting cancellation via email', }, - - - ], }, @@ -492,8 +513,6 @@ export const meetingFields = [ show: { operation: [ 'update', - - ], resource: [ 'meeting', @@ -502,26 +521,6 @@ export const meetingFields = [ }, description: 'Meeting ID.', }, - { - displayName: 'Occurence Id', - name: 'occurenceId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'update', - - - ], - resource: [ - 'meeting', - ], - }, - }, - description: 'Occurence ID.', - }, { displayName: 'Additional settings', name: 'additionalFields', @@ -531,15 +530,21 @@ export const meetingFields = [ displayOptions: { show: { operation: [ - 'update', ], resource: [ 'meeting', ], - } + }, }, options: [ + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + description: 'Occurence ID.', + }, { displayName: 'Meeting topic', name: 'topic', @@ -691,8 +696,6 @@ export const meetingFields = [ name: 'Disabled', value: 'none', }, - - ], default: 'none', description: 'Auto recording.', @@ -714,8 +717,6 @@ export const meetingFields = [ name: 'VOIP', value: 'voip', }, - - ], default: 'both', description: 'Determine how participants can join audio portion of the meeting.', @@ -737,14 +738,10 @@ export const meetingFields = [ name: 'Attendees register once and can choose one or more occurences to attend', value: 3, }, - - ], default: 1, description: 'Registration type. Used for recurring meetings with fixed time only', }, - - ], }, diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index b062786892..592c7a285a 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -52,8 +52,6 @@ export const meetingRegistrantFields = [ show: { operation: [ 'create', - - ], resource: [ 'meetingRegistrants', @@ -63,31 +61,21 @@ export const meetingRegistrantFields = [ description: 'Meeting ID.', }, { - displayName: 'Occurence Id', - name: 'occurenceId', + displayName: 'Email', + name: 'email', type: 'string', - default: '', required: true, + default: '', displayOptions: { show: { operation: [ 'create', - - ], resource: [ 'meetingRegistrants', ], }, }, - description: 'Occurence ID.', - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - required: true, - default: '', description: 'Valid email-id of registrant.', }, { @@ -96,6 +84,16 @@ export const meetingRegistrantFields = [ required: true, type: 'string', default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, description: 'First Name.', }, { @@ -107,7 +105,7 @@ export const meetingRegistrantFields = [ displayOptions: { show: { operation: [ - 'get', + 'create', ], resource: [ @@ -116,6 +114,13 @@ export const meetingRegistrantFields = [ } }, options: [ + { + displayName: 'Occurence Ids', + name: 'occurenceId', + type: 'string', + default: '', + description: 'Occurence IDs separated by comma.', + }, { displayName: 'Last Name', name: 'lastName', @@ -211,8 +216,6 @@ export const meetingRegistrantFields = [ name: 'No timeframe', value: 'No timeframe', }, - - ], default: '', description: 'Meeting type.' @@ -258,8 +261,6 @@ export const meetingRegistrantFields = [ show: { operation: [ 'getAll', - - ], resource: [ 'meetingRegistrants', @@ -318,7 +319,7 @@ export const meetingRegistrantFields = [ displayOptions: { show: { operation: [ - 'get', + 'getAll', ], resource: [ @@ -351,10 +352,8 @@ export const meetingRegistrantFields = [ name: 'Denied', value: 'denied', }, - - ], - default: '', + default: 'approved', description: `Registrant Status.`, }, @@ -366,15 +365,13 @@ export const meetingRegistrantFields = [ { displayName: 'Meeting Id', name: 'meetingId', - type: 'number', + type: 'string', default: '', required: true, displayOptions: { show: { operation: [ 'update', - - ], resource: [ 'meetingRegistrants', @@ -384,24 +381,63 @@ export const meetingRegistrantFields = [ description: 'Meeting ID.', }, { - displayName: 'Occurence Id', - name: 'occurenceId', - type: 'string', - default: '', + displayName: 'Action', + name: 'action', + type: 'options', required: true, displayOptions: { show: { operation: [ 'update', - - ], resource: [ 'meetingRegistrants', ], }, }, - description: 'Occurence ID.', + options: [ + { + name: 'Cancel', + value: 'cancel', + }, + { + name: 'Approved', + value: 'approve', + }, + { + name: 'Deny', + value: 'deny', + }, + ], + default: '', + description: `Registrant Status.`, }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'meetingRegistrants', + ], + }, + }, + options: [ + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + description: 'Occurence ID.', + }, + + ], + } ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts new file mode 100644 index 0000000000..5cefbdace8 --- /dev/null +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -0,0 +1,685 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const webinarOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a webinar', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a webinar', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a webinar', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all webinars', + }, + { + name: 'Update', + value: 'update', + description: 'Update a webinar', + } + ], + default: 'create', + description: 'The operation to perform.', + } +] as INodeProperties[]; + +export const webinarFields = [ + /* -------------------------------------------------------------------------- */ + /* webinar:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User Id', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'webinar', + ], + }, + }, + description: 'User ID or email address of user.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + + ], + resource: [ + 'webinar', + ], + } + }, + options: [ + { + displayName: 'Webinar topic', + name: 'topic', + type: 'string', + default: '', + description: `Webinar topic.`, + }, + { + displayName: 'Webinar type', + name: 'type', + type: 'options', + options: [ + { + name: 'Webinar', + value: 5, + }, + { + name: 'Recurring webinar with no fixed time', + value: 6, + }, + { + name: 'Recurring webinar with fixed time', + value: 9, + }, + ], + default: 5, + description: 'Webinar type.' + }, + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring webinar with fixed time', + }, + { + displayName: 'Duration', + name: 'duration', + type: 'string', + default: '', + description: 'Duration.', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password to join the webinar with maximum 10 characters.', + }, + { + displayName: 'Agenda', + name: 'agenda', + type: 'string', + default: '', + description: 'Webinar agenda.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the webinar.', + }, + { + displayName: 'Panelists Video', + name: 'panelists_video', + type: 'boolean', + default: false, + description: 'Start video when panelists joins the webinar.', + }, + { + displayName: 'Practice Session', + name: 'practice_session', + type: 'boolean', + default: false, + description: 'Enable Practice session.', + }, + { + displayName: 'Alternative Hosts', + name: 'alternative_hosts', + type: 'string', + default: '', + description: 'Alternative hosts email ids.', + }, + { + displayName: 'Approval type', + name: 'approval_type', + type: 'options', + options: [ + { + name: 'Automatically approve', + value: 0, + }, + { + name: 'Manually approve', + value: 1, + }, + { + name: 'No registration required', + value: 2, + }, + ], + default: 2, + description: 'Approval type.', + }, + { + displayName: 'Auto recording', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Record on local', + value: 'local', + }, + { + name: 'Record on cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + ], + default: 'none', + description: 'Auto recording.', + }, + { + displayName: 'Audio', + name: 'audio', + type: 'options', + options: [ + { + name: 'Both Telephony and VoiP', + value: 'both', + }, + { + name: 'Telephony', + value: 'telephony', + }, + { + name: 'VOIP', + value: 'voip', + }, + + ], + default: 'both', + description: 'Determine how participants can join audio portion of the webinar.', + }, + { + displayName: 'Registration type', + name: 'registration_type', + type: 'options', + options: [ + { + name: 'Attendees register once and can attend any of the occurences', + value: 1, + }, + { + name: 'Attendees need to register for every occurence', + value: 2, + }, + { + name: 'Attendees register once and can choose one or more occurences to attend', + value: 3, + }, + ], + default: 1, + description: 'Registration type. Used for recurring webinar with fixed time only', + }, + + ], + }, + /* -------------------------------------------------------------------------- */ + /* webinar:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Webinar Id', + name: 'webinarId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'webinar', + ], + }, + }, + description: 'Webinar ID.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + + ], + resource: [ + 'webinar', + ], + }, + }, + options: [ + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + description: 'To view webinar details of a particular occurence of the recurring webinar.', + }, + { + displayName: 'Show Previous Occurences', + name: 'showPreviousOccurences', + type: 'boolean', + default: '', + description: 'To view webinar details of all previous occurences of the recurring webinar.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* webinar:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User Id', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'webinar', + ], + }, + }, + description: 'User ID or email-id.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'webinar', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'webinar', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 300 + }, + default: 30, + description: 'How many results to return.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + + ], + resource: [ + 'webinar', + ], + }, + }, + + }, + /* -------------------------------------------------------------------------- */ + /* webina:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Webinar Id', + name: 'webinarId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete' + ], + resource: [ + 'webinarId', + ], + }, + }, + description: 'WebinarId ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'webinar', + ], + }, + }, + options: [ + { + displayName: 'Occurence Id', + name: 'occurenceId', + type: 'string', + default: '', + description: 'Webinar occurence Id.', + }, + + ], + + }, + /* -------------------------------------------------------------------------- */ + /* webinar:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User Id', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'webinar', + ], + }, + }, + description: 'User ID or email address of user.', + }, + { + displayName: 'Additional settings', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + + ], + resource: [ + 'webinar', + ], + } + }, + options: [ + { + displayName: 'Occurence Id', + name: 'occurence_id', + type: 'string', + default: '', + description: `Webinar occurence Id.`, + }, + { + displayName: 'Webinar topic', + name: 'topic', + type: 'string', + default: '', + description: `Webinar topic.`, + }, + { + displayName: 'Webinar type', + name: 'type', + type: 'options', + options: [ + { + name: 'Webinar', + value: 5, + }, + { + name: 'Recurring webinar with no fixed time', + value: 6, + }, + { + name: 'Recurring webinar with fixed time', + value: 9, + }, + ], + default: 5, + description: 'Webinar type.' + }, + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring webinar with fixed time', + }, + { + displayName: 'Duration', + name: 'duration', + type: 'string', + default: '', + description: 'Duration.', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password to join the webinar with maximum 10 characters.', + }, + { + displayName: 'Agenda', + name: 'agenda', + type: 'string', + default: '', + description: 'Webinar agenda.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the webinar.', + }, + { + displayName: 'Panelists Video', + name: 'panelists_video', + type: 'boolean', + default: false, + description: 'Start video when panelists joins the webinar.', + }, + { + displayName: 'Practice Session', + name: 'practice_session', + type: 'boolean', + default: false, + description: 'Enable Practice session.', + }, + { + displayName: 'Alternative Hosts', + name: 'alternative_hosts', + type: 'string', + default: '', + description: 'Alternative hosts email ids.', + }, + { + displayName: 'Approval type', + name: 'approval_type', + type: 'options', + options: [ + { + name: 'Automatically approve', + value: 0, + }, + { + name: 'Manually approve', + value: 1, + }, + { + name: 'No registration required', + value: 2, + }, + ], + default: 2, + description: 'Approval type.', + }, + { + displayName: 'Auto recording', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Record on local', + value: 'local', + }, + { + name: 'Record on cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + ], + default: 'none', + description: 'Auto recording.', + }, + { + displayName: 'Audio', + name: 'audio', + type: 'options', + options: [ + { + name: 'Both Telephony and VoiP', + value: 'both', + }, + { + name: 'Telephony', + value: 'telephony', + }, + { + name: 'VOIP', + value: 'voip', + }, + + ], + default: 'both', + description: 'Determine how participants can join audio portion of the webinar.', + }, + { + displayName: 'Registration type', + name: 'registration_type', + type: 'options', + options: [ + { + name: 'Attendees register once and can attend any of the occurences', + value: 1, + }, + { + name: 'Attendees need to register for every occurence', + value: 2, + }, + { + name: 'Attendees register once and can choose one or more occurences to attend', + value: 3, + }, + ], + default: 1, + description: 'Registration type. Used for recurring webinars with fixed time only', + }, + + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index c1915d4ab4..fb3b7565f7 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -1,4 +1,6 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, +} from 'n8n-core'; import { IDataObject, INodeExecutionData, @@ -10,7 +12,6 @@ import { import { zoomApiRequest, zoomApiRequestAllItems, - validateJSON, } from './GenericFunctions'; import { @@ -24,11 +25,16 @@ import { } from './MeetingRegistrantDescription'; +import { + webinarOperations, + webinarFields, +} from './WebinarDescription'; import * as moment from 'moment-timezone'; interface Settings { host_video?: boolean; participant_video?: boolean; + panelists_video?: boolean; cn_meeting?: boolean; in_meeting?: boolean; join_before_host?: boolean; @@ -38,6 +44,9 @@ interface Settings { alternative_hosts?: string; auto_recording?: string; registration_type?: number; + approval_type?: number; + practice_session?: boolean; + } export class Zoom implements INodeType { @@ -107,17 +116,28 @@ export class Zoom implements INodeType { value: 'meeting' }, { - name: 'Meeting Registrants', + name: 'Meeting Registrant', value: 'meetingRegistrants' + }, + { + name: 'Webinar', + value: 'webinar' } ], default: 'meeting', description: 'The resource to operate on.' }, + //MEETINGS ...meetingOperations, ...meetingFields, + + //MEETING REGISTRANTS ...meetingRegistrantOperations, ...meetingRegistrantFields, + + //WEBINARS + ...webinarOperations, + ...webinarFields, ] }; @@ -149,20 +169,30 @@ export class Zoom implements INodeType { let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; - console.log(this.getCredentials('zoomOAuth2Api')); let body: IDataObject = {}; for (let i = 0; i < length; i++) { qs = {}; + //https://marketplace.zoom.us/docs/api-reference/zoom-api/ if (resource === 'meeting') { if (operation === 'get') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meeting - const userId = this.getNodeParameter('userId', i) as string; + const meetingId = this.getNodeParameter('meetingId', i) as string; + + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.showPreviousOccurences) + qs.show_previous_occurences = additionalFields.showPreviousOccurences as boolean; + + if (additionalFields.occurenceId) + qs.occurence_id = additionalFields.occurenceId as string; responseData = await zoomApiRequest.call( this, 'GET', - `/meetings/${userId}`, + `/meetings/${meetingId}`, {}, qs ); @@ -170,18 +200,30 @@ export class Zoom implements INodeType { if (operation === 'getAll') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetings const userId = this.getNodeParameter('userId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll) { + responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/users/${userId}/meetings`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.page_size = limit; + responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/meetings`, {}, qs); + responseData = responseData.results; + } - responseData = await zoomApiRequest.call( - this, - 'GET', - `/users/${userId}/meetings`, - {}, - qs - ); } if (operation === 'delete') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingdelete const meetingId = this.getNodeParameter('meetingId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.scheduleForReminder) + qs.schedule_for_reminder = additionalFields.scheduleForReminder as boolean; + + if (additionalFields.occurenceId) + qs.occurence_id = additionalFields.occurenceId; + responseData = await zoomApiRequest.call( this, 'DELETE', @@ -297,10 +339,6 @@ export class Zoom implements INodeType { body.agenda = additionalFields.agenda as string; } - - - - responseData = await zoomApiRequest.call( this, 'POST', @@ -312,12 +350,16 @@ export class Zoom implements INodeType { if (operation === 'update') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingupdate const meetingId = this.getNodeParameter('meetingId', i) as string; - qs.occurence_id = this.getNodeParameter('occurenceId', i) as string; + const settings: Settings = {}; const additionalFields = this.getNodeParameter( 'additionalFields', i ) as IDataObject; - const settings: Settings = {}; + + if (additionalFields.occurenceId) { + qs.occurence_id = additionalFields.occurenceId as string; + } + if (additionalFields.cn_meeting) { settings.cn_meeting = additionalFields.cn_meeting as boolean; @@ -423,16 +465,64 @@ export class Zoom implements INodeType { body, qs ); + responseData = { updated: true }; + } } if (resource === 'meetingRegistrant') { if (operation === 'create') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantcreate const meetingId = this.getNodeParameter('meetingId', i) as string; - qs.occurence_id = this.getNodeParameter('occurenceId', i) as string; + const emailId = this.getNodeParameter('email', i) as string; + body.email = emailId; + const firstName = this.getNodeParameter('firstName', i) as string; + body.first_name = firstName; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.occurenceId) { + qs.occurence_ids = additionalFields.occurenceId as string; + } + if (additionalFields.lastName) { + body.last_name = additionalFields.lastName as string; + } + if (additionalFields.address) { + body.address = additionalFields.address as string; + } + if (additionalFields.city) { + body.city = additionalFields.city as string; + } + if (additionalFields.state) { + body.state = additionalFields.state as string; + } + if (additionalFields.country) { + body.country = additionalFields.country as string; + } + if (additionalFields.zip) { + body.zip = additionalFields.zip as string; + } + if (additionalFields.phone) { + body.phone = additionalFields.phone as string; + } + if (additionalFields.comments) { + body.comments = additionalFields.comments as string; + } + if (additionalFields.org) { + body.org = additionalFields.org as string; + } + if (additionalFields.job_title) { + body.job_title = additionalFields.job_title as string; + } + if (additionalFields.purchasing_time_frame) { + body.purchasing_time_frame = additionalFields.purchasing_time_frame as string; + } + if (additionalFields.role_in_purchase_process) { + body.role_in_purchase_process = additionalFields.role_in_purchase_process as string; + } responseData = await zoomApiRequest.call( this, - 'PATCH', + 'POST', `/meetings/${meetingId}/registrants`, body, qs @@ -440,9 +530,282 @@ export class Zoom implements INodeType { } if (operation === 'getAll') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrants + const meetingId = this.getNodeParameter('meetingId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.occurenceId) { + qs.occurence_id = additionalFields.occurenceId as string; + } + if (additionalFields.status) { + qs.status = additionalFields.status as string; + } + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll) { + responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/meetings/${meetingId}/registrants`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.page_size = limit; + responseData = await zoomApiRequest.call(this, 'GET', `/meetings/${meetingId}/registrants`, {}, qs); + responseData = responseData.results; + } + } if (operation === 'update') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantstatus + const meetingId = this.getNodeParameter('meetingId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.occurenceId) { + qs.occurence_id = additionalFields.occurenceId as string; + } + responseData = await zoomApiRequest.call( + this, + 'PUT', + `/meetings/${meetingId}/registrants/status`, + body, + qs + ); + } + } + if (resource === 'webinar') { + if (operation === 'create') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarcreate + const userId = this.getNodeParameter('userId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + const settings: Settings = {}; + + + if (additionalFields.audio) { + settings.audio = additionalFields.audio as string; + + } + + if (additionalFields.alternative_hosts) { + settings.alternative_hosts = additionalFields.alternative_hosts as string; + + } + + if (additionalFields.panelists_video) { + settings.panelists_video = additionalFields.panelists_video as boolean; + + } + if (additionalFields.practice_session) { + settings.practice_session = additionalFields.practice_session as boolean; + + } + if (additionalFields.auto_recording) { + settings.auto_recording = additionalFields.auto_recording as string; + + } + + if (additionalFields.registration_type) { + settings.registration_type = additionalFields.registration_type as number; + + } + if (additionalFields.approval_type) { + settings.approval_type = additionalFields.approval_type as number; + + } + + body = { + settings, + }; + + if (additionalFields.topic) { + body.topic = additionalFields.topic as string; + + } + + if (additionalFields.type) { + body.type = additionalFields.type as string; + + } + + if (additionalFields.startTime) { + body.start_time = additionalFields.startTime as string; + + } + + if (additionalFields.duration) { + body.duration = additionalFields.duration as number; + + } + + + if (additionalFields.timeZone) { + body.timezone = additionalFields.timeZone as string; + + } + + if (additionalFields.password) { + body.password = additionalFields.password as string; + + } + + if (additionalFields.agenda) { + body.agenda = additionalFields.agenda as string; + + } + responseData = await zoomApiRequest.call( + this, + 'POST', + `/users/${userId}/webinars`, + body, + qs + ); + } + if (operation === 'get') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinar + const webinarId = this.getNodeParameter('webinarId', i) as string; + + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.showPreviousOccurences) + qs.show_previous_occurences = additionalFields.showPreviousOccurences as boolean; + + if (additionalFields.occurenceId) + qs.occurence_id = additionalFields.occurenceId as string; + + responseData = await zoomApiRequest.call( + this, + 'GET', + `/webinars/${webinarId}`, + {}, + qs + ); + } + if (operation === 'getAll') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinars + const userId = this.getNodeParameter('userId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll) { + responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/users/${userId}/webinars`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.page_size = limit; + responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/webinars`, {}, qs); + responseData = responseData.results; + } + } + if (operation === 'delete') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinardelete + const webinarId = this.getNodeParameter('webinarId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + + + if (additionalFields.occurenceId) + qs.occurence_id = additionalFields.occurenceId; + + responseData = await zoomApiRequest.call( + this, + 'DELETE', + `/webinars/${webinarId}`, + {}, + qs + ); + responseData = { success: true }; + } + if (operation === 'update') { + //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarupdate + const webinarId = this.getNodeParameter('webinarId', i) as string; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.occurence_id) { + qs.occurence_id = additionalFields.occurence_id as string; + + } + const settings: Settings = {}; + if (additionalFields.audio) { + settings.audio = additionalFields.audio as string; + + } + if (additionalFields.alternative_hosts) { + settings.alternative_hosts = additionalFields.alternative_hosts as string; + + } + + if (additionalFields.panelists_video) { + settings.panelists_video = additionalFields.panelists_video as boolean; + + } + if (additionalFields.practice_session) { + settings.practice_session = additionalFields.practice_session as boolean; + + } + if (additionalFields.auto_recording) { + settings.auto_recording = additionalFields.auto_recording as string; + + } + + if (additionalFields.registration_type) { + settings.registration_type = additionalFields.registration_type as number; + + } + if (additionalFields.approval_type) { + settings.approval_type = additionalFields.approval_type as number; + + } + + body = { + settings, + }; + + if (additionalFields.topic) { + body.topic = additionalFields.topic as string; + + } + + if (additionalFields.type) { + body.type = additionalFields.type as string; + + } + + if (additionalFields.startTime) { + body.start_time = additionalFields.startTime as string; + + } + + if (additionalFields.duration) { + body.duration = additionalFields.duration as number; + + } + + + if (additionalFields.timeZone) { + body.timezone = additionalFields.timeZone as string; + + } + + if (additionalFields.password) { + body.password = additionalFields.password as string; + + } + + if (additionalFields.agenda) { + body.agenda = additionalFields.agenda as string; + + } + responseData = await zoomApiRequest.call( + this, + 'PATCH', + `/users/${webinarId}/webinars`, + body, + qs + ); } } } From 807db166fd6ec8b46ed987d15cc254606307eb2b Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 23 Jun 2020 12:14:04 -0700 Subject: [PATCH 05/44] refactor code --- .../nodes/Zoom/MeetingDescription.ts | 498 +++++++++--------- .../Zoom/MeetingRegistrantDescription.ts | 59 ++- .../nodes/Zoom/WebinarDescription.ts | 396 +++++++------- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 28 +- 4 files changed, 489 insertions(+), 492 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index 274b48ff97..b9a1c74cb5 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -86,77 +86,6 @@ export const meetingFields = [ } }, options: [ - { - displayName: 'Meeting topic', - name: 'topic', - type: 'string', - default: '', - description: `Meeting topic.`, - }, - { - displayName: 'Meeting type', - name: 'type', - type: 'options', - options: [ - { - name: 'Instant Meeting', - value: 1, - }, - { - name: 'Scheduled Meeting', - value: 2, - }, - { - name: 'Recurring meeting with no fixed time', - value: 3, - }, - { - name: 'Recurring meeting with no fixed time', - value: 8, - }, - - ], - default: 2, - description: 'Meeting type.' - }, - { - displayName: 'Start time', - name: 'startTime', - type: 'dateTime', - default: '', - description: 'Start time should be used only for scheduled or recurring meetings with fixed time', - }, - { - displayName: 'Duration', - name: 'duration', - type: 'number', - default: '', - description: 'Duration.', - }, - { - displayName: 'Timezone', - name: 'timeZone', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getTimezones', - }, - default: '', - description: `Time zone used in the response. The default is the time zone of the calendar.`, - }, - { - displayName: 'Schedule for', - name: 'scheduleFor', - type: 'string', - default: '', - description: 'Schedule meeting for someone else from your account, provide their email id.', - }, - { - displayName: 'Password', - name: 'password', - type: 'string', - default: '', - description: 'Password to join the meeting with maximum 10 characters.', - }, { displayName: 'Agenda', name: 'agenda', @@ -164,55 +93,6 @@ export const meetingFields = [ default: '', description: 'Meeting agenda.', }, - { - displayName: 'Host Meeting in China', - name: 'cn_meeting', - type: 'boolean', - default: false, - description: 'Host Meeting in China.', - }, - { - displayName: 'Host Meeting in India', - name: 'in_meeting', - type: 'boolean', - default: false, - description: 'Host Meeting in India.', - }, - { - displayName: 'Host Video', - name: 'host_video', - type: 'boolean', - default: false, - description: 'Start video when host joins the meeting.', - }, - { - displayName: 'Participant Video', - name: 'participant_video', - type: 'boolean', - default: false, - description: 'Start video when participant joins the meeting.', - }, - { - displayName: 'Join before Host', - name: 'join_before_host', - type: 'boolean', - default: false, - description: 'Allow participants to join the meeting before host starts it.', - }, - { - displayName: 'Muting before entry', - name: 'mute_upon_entry', - type: 'boolean', - default: false, - description: 'Mute participants upon entry.', - }, - { - displayName: 'Watermark', - name: 'watermark', - type: 'boolean', - default: false, - description: 'Adds watermark when viewing a shared screen.', - }, { displayName: 'Alternative Hosts', name: 'alternative_hosts', @@ -263,6 +143,95 @@ export const meetingFields = [ default: 'both', description: 'Determine how participants can join audio portion of the meeting.', }, + { + displayName: 'Duration', + name: 'duration', + type: 'number', + default: '', + description: 'Duration.', + }, + { + displayName: 'Host Meeting in China', + name: 'cn_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in China.', + }, + { + displayName: 'Host Meeting in India', + name: 'in_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in India.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the meeting.', + }, + { + displayName: 'Join before Host', + name: 'join_before_host', + type: 'boolean', + default: false, + description: 'Allow participants to join the meeting before host starts it.', + }, + { + displayName: 'Meeting topic', + name: 'topic', + type: 'string', + default: '', + description: `Meeting topic.`, + }, + { + displayName: 'Meeting type', + name: 'type', + type: 'options', + options: [ + { + name: 'Instant Meeting', + value: 1, + }, + { + name: 'Scheduled Meeting', + value: 2, + }, + { + name: 'Recurring meeting with no fixed time', + value: 3, + }, + { + name: 'Recurring meeting with no fixed time', + value: 8, + }, + + ], + default: 2, + description: 'Meeting type.' + }, + { + displayName: 'Muting before entry', + name: 'mute_upon_entry', + type: 'boolean', + default: false, + description: 'Mute participants upon entry.', + }, + { + displayName: 'Participant Video', + name: 'participant_video', + type: 'boolean', + default: false, + description: 'Start video when participant joins the meeting.', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password to join the meeting with maximum 10 characters.', + }, { displayName: 'Registration type', name: 'registration_type', @@ -284,7 +253,37 @@ export const meetingFields = [ default: 1, description: 'Registration type. Used for recurring meetings with fixed time only', }, - + { + displayName: 'Schedule for', + name: 'scheduleFor', + type: 'string', + default: '', + description: 'Schedule meeting for someone else from your account, provide their email id.', + }, + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring meetings with fixed time', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Watermark', + name: 'watermark', + type: 'boolean', + default: false, + description: 'Adds watermark when viewing a shared screen.', + }, ], }, /* -------------------------------------------------------------------------- */ @@ -538,6 +537,98 @@ export const meetingFields = [ }, }, options: [ + { + displayName: 'Agenda', + name: 'agenda', + type: 'string', + default: '', + description: 'Meeting agenda.', + }, + { + displayName: 'Alternative Hosts', + name: 'alternative_hosts', + type: 'string', + default: '', + description: 'Alternative hosts email ids.', + }, + { + displayName: 'Audio', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Both Telephony and VoiP', + value: 'both', + }, + { + name: 'Telephony', + value: 'telephony', + }, + { + name: 'VOIP', + value: 'voip', + }, + ], + default: 'both', + description: 'Determine how participants can join audio portion of the meeting.', + }, + { + displayName: 'Auto recording', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Record on local', + value: 'local', + }, + { + name: 'Record on cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + ], + default: 'none', + description: 'Auto recording.', + }, + + { + displayName: 'Duration', + name: 'duration', + type: 'number', + default: '', + description: 'Duration.', + }, + { + displayName: 'Join before Host', + name: 'join_before_host', + type: 'boolean', + default: false, + description: 'Allow participants to join the meeting before host starts it.', + }, + { + displayName: 'Host Meeting in China', + name: 'cn_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in China.', + }, + { + displayName: 'Host Meeting in India', + name: 'in_meeting', + type: 'boolean', + default: false, + description: 'Host Meeting in India.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the meeting.', + }, { displayName: 'Occurence Id', name: 'occurenceId', @@ -579,35 +670,11 @@ export const meetingFields = [ description: 'Meeting type.' }, { - displayName: 'Start time', - name: 'startTime', - type: 'dateTime', - default: '', - description: 'Start time should be used only for scheduled or recurring meetings with fixed time', - }, - { - displayName: 'Duration', - name: 'duration', - type: 'number', - default: '', - description: 'Duration.', - }, - { - displayName: 'Timezone', - name: 'timeZone', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getTimezones', - }, - default: '', - description: `Time zone used in the response. The default is the time zone of the calendar.`, - }, - { - displayName: 'Schedule for', - name: 'scheduleFor', - type: 'string', - default: '', - description: 'Schedule meeting for someone else from your account, provide their email id.', + displayName: 'Muting before entry', + name: 'mute_upon_entry', + type: 'boolean', + default: false, + description: 'Mute participants upon entry.', }, { displayName: 'Password', @@ -616,34 +683,6 @@ export const meetingFields = [ default: '', description: 'Password to join the meeting with maximum 10 characters.', }, - { - displayName: 'Agenda', - name: 'agenda', - type: 'string', - default: '', - description: 'Meeting agenda.', - }, - { - displayName: 'Host Meeting in China', - name: 'cn_meeting', - type: 'boolean', - default: false, - description: 'Host Meeting in China.', - }, - { - displayName: 'Host Meeting in India', - name: 'in_meeting', - type: 'boolean', - default: false, - description: 'Host Meeting in India.', - }, - { - displayName: 'Host Video', - name: 'host_video', - type: 'boolean', - default: false, - description: 'Start video when host joins the meeting.', - }, { displayName: 'Participant Video', name: 'participant_video', @@ -651,76 +690,6 @@ export const meetingFields = [ default: false, description: 'Start video when participant joins the meeting.', }, - { - displayName: 'Join before Host', - name: 'join_before_host', - type: 'boolean', - default: false, - description: 'Allow participants to join the meeting before host starts it.', - }, - { - displayName: 'Muting before entry', - name: 'mute_upon_entry', - type: 'boolean', - default: false, - description: 'Mute participants upon entry.', - }, - { - displayName: 'Watermark', - name: 'watermark', - type: 'boolean', - default: false, - description: 'Adds watermark when viewing a shared screen.', - }, - { - displayName: 'Alternative Hosts', - name: 'alternative_hosts', - type: 'string', - default: '', - description: 'Alternative hosts email ids.', - }, - { - displayName: 'Auto recording', - name: 'auto_recording', - type: 'options', - options: [ - { - name: 'Record on local', - value: 'local', - }, - { - name: 'Record on cloud', - value: 'cloud', - }, - { - name: 'Disabled', - value: 'none', - }, - ], - default: 'none', - description: 'Auto recording.', - }, - { - displayName: 'Audio', - name: 'auto_recording', - type: 'options', - options: [ - { - name: 'Both Telephony and VoiP', - value: 'both', - }, - { - name: 'Telephony', - value: 'telephony', - }, - { - name: 'VOIP', - value: 'voip', - }, - ], - default: 'both', - description: 'Determine how participants can join audio portion of the meeting.', - }, { displayName: 'Registration type', name: 'registration_type', @@ -742,6 +711,39 @@ export const meetingFields = [ default: 1, description: 'Registration type. Used for recurring meetings with fixed time only', }, + { + displayName: 'Schedule for', + name: 'scheduleFor', + type: 'string', + default: '', + description: 'Schedule meeting for someone else from your account, provide their email id.', + }, + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring meetings with fixed time', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Watermark', + name: 'watermark', + type: 'boolean', + default: false, + description: 'Adds watermark when viewing a shared screen.', + }, + + ], }, diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index 592c7a285a..232f8cf42c 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -114,20 +114,6 @@ export const meetingRegistrantFields = [ } }, options: [ - { - displayName: 'Occurence Ids', - name: 'occurenceId', - type: 'string', - default: '', - description: 'Occurence IDs separated by comma.', - }, - { - displayName: 'Last Name', - name: 'lastName', - type: 'string', - default: '', - description: 'Last Name.', - }, { displayName: 'Address', name: 'address', @@ -143,11 +129,11 @@ export const meetingRegistrantFields = [ description: 'Valid city of registrant.', }, { - displayName: 'State', - name: 'state', + displayName: 'Comments', + name: 'comments', type: 'string', default: '', - description: 'Valid state of registrant.', + description: 'Allows registrants to provide any questions they have.', }, { displayName: 'Country', @@ -157,25 +143,25 @@ export const meetingRegistrantFields = [ description: 'Valid country of registrant.', }, { - displayName: 'Zip code', - name: 'zip', + displayName: 'Job title', + name: 'job_title', type: 'string', default: '', - description: 'Valid zip-code of registrant.', + description: 'Job title of registrant.', }, { - displayName: 'Phone Number', - name: 'phone', + displayName: 'Last Name', + name: 'lastName', type: 'string', default: '', - description: 'Valid phone number of registrant.', + description: 'Last Name.', }, { - displayName: 'Comments', - name: 'comments', + displayName: 'Occurence Ids', + name: 'occurenceId', type: 'string', default: '', - description: 'Allows registrants to provide any questions they have.', + description: 'Occurence IDs separated by comma.', }, { displayName: 'Organization', @@ -185,11 +171,11 @@ export const meetingRegistrantFields = [ description: 'Organization of registrant.', }, { - displayName: 'Job title', - name: 'job_title', + displayName: 'Phone Number', + name: 'phone', type: 'string', default: '', - description: 'Job title of registrant.', + description: 'Valid phone number of registrant.', }, { displayName: 'Purchasing time frame', @@ -246,6 +232,21 @@ export const meetingRegistrantFields = [ default: '', description: 'Role in purchase process.' }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'Valid state of registrant.', + }, + { + displayName: 'Zip code', + name: 'zip', + type: 'string', + default: '', + description: 'Valid zip-code of registrant.', + }, + ], }, /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts index 5cefbdace8..4bbec11fd4 100644 --- a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -86,65 +86,6 @@ export const webinarFields = [ } }, options: [ - { - displayName: 'Webinar topic', - name: 'topic', - type: 'string', - default: '', - description: `Webinar topic.`, - }, - { - displayName: 'Webinar type', - name: 'type', - type: 'options', - options: [ - { - name: 'Webinar', - value: 5, - }, - { - name: 'Recurring webinar with no fixed time', - value: 6, - }, - { - name: 'Recurring webinar with fixed time', - value: 9, - }, - ], - default: 5, - description: 'Webinar type.' - }, - { - displayName: 'Start time', - name: 'startTime', - type: 'dateTime', - default: '', - description: 'Start time should be used only for scheduled or recurring webinar with fixed time', - }, - { - displayName: 'Duration', - name: 'duration', - type: 'string', - default: '', - description: 'Duration.', - }, - { - displayName: 'Timezone', - name: 'timeZone', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getTimezones', - }, - default: '', - description: `Time zone used in the response. The default is the time zone of the calendar.`, - }, - { - displayName: 'Password', - name: 'password', - type: 'string', - default: '', - description: 'Password to join the webinar with maximum 10 characters.', - }, { displayName: 'Agenda', name: 'agenda', @@ -152,27 +93,6 @@ export const webinarFields = [ default: '', description: 'Webinar agenda.', }, - { - displayName: 'Host Video', - name: 'host_video', - type: 'boolean', - default: false, - description: 'Start video when host joins the webinar.', - }, - { - displayName: 'Panelists Video', - name: 'panelists_video', - type: 'boolean', - default: false, - description: 'Start video when panelists joins the webinar.', - }, - { - displayName: 'Practice Session', - name: 'practice_session', - type: 'boolean', - default: false, - description: 'Enable Practice session.', - }, { displayName: 'Alternative Hosts', name: 'alternative_hosts', @@ -201,27 +121,6 @@ export const webinarFields = [ default: 2, description: 'Approval type.', }, - { - displayName: 'Auto recording', - name: 'auto_recording', - type: 'options', - options: [ - { - name: 'Record on local', - value: 'local', - }, - { - name: 'Record on cloud', - value: 'cloud', - }, - { - name: 'Disabled', - value: 'none', - }, - ], - default: 'none', - description: 'Auto recording.', - }, { displayName: 'Audio', name: 'audio', @@ -244,6 +143,62 @@ export const webinarFields = [ default: 'both', description: 'Determine how participants can join audio portion of the webinar.', }, + { + displayName: 'Auto recording', + name: 'auto_recording', + type: 'options', + options: [ + { + name: 'Record on local', + value: 'local', + }, + { + name: 'Record on cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + ], + default: 'none', + description: 'Auto recording.', + }, + { + displayName: 'Duration', + name: 'duration', + type: 'string', + default: '', + description: 'Duration.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the webinar.', + }, + { + displayName: 'Panelists Video', + name: 'panelists_video', + type: 'boolean', + default: false, + description: 'Start video when panelists joins the webinar.', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password to join the webinar with maximum 10 characters.', + }, + { + displayName: 'Practice Session', + name: 'practice_session', + type: 'boolean', + default: false, + description: 'Enable Practice session.', + }, { displayName: 'Registration type', name: 'registration_type', @@ -265,6 +220,51 @@ export const webinarFields = [ default: 1, description: 'Registration type. Used for recurring webinar with fixed time only', }, + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring webinar with fixed time', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Webinar topic', + name: 'topic', + type: 'string', + default: '', + description: `Webinar topic.`, + }, + { + displayName: 'Webinar type', + name: 'type', + type: 'options', + options: [ + { + name: 'Webinar', + value: 5, + }, + { + name: 'Recurring webinar with no fixed time', + value: 6, + }, + { + name: 'Recurring webinar with fixed time', + value: 9, + }, + ], + default: 5, + description: 'Webinar type.' + }, ], }, @@ -385,25 +385,6 @@ export const webinarFields = [ default: 30, description: 'How many results to return.', }, - { - displayName: 'Additional settings', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - operation: [ - 'getAll', - - ], - resource: [ - 'webinar', - ], - }, - }, - - }, /* -------------------------------------------------------------------------- */ /* webina:delete */ /* -------------------------------------------------------------------------- */ @@ -492,72 +473,6 @@ export const webinarFields = [ } }, options: [ - { - displayName: 'Occurence Id', - name: 'occurence_id', - type: 'string', - default: '', - description: `Webinar occurence Id.`, - }, - { - displayName: 'Webinar topic', - name: 'topic', - type: 'string', - default: '', - description: `Webinar topic.`, - }, - { - displayName: 'Webinar type', - name: 'type', - type: 'options', - options: [ - { - name: 'Webinar', - value: 5, - }, - { - name: 'Recurring webinar with no fixed time', - value: 6, - }, - { - name: 'Recurring webinar with fixed time', - value: 9, - }, - ], - default: 5, - description: 'Webinar type.' - }, - { - displayName: 'Start time', - name: 'startTime', - type: 'dateTime', - default: '', - description: 'Start time should be used only for scheduled or recurring webinar with fixed time', - }, - { - displayName: 'Duration', - name: 'duration', - type: 'string', - default: '', - description: 'Duration.', - }, - { - displayName: 'Timezone', - name: 'timeZone', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getTimezones', - }, - default: '', - description: `Time zone used in the response. The default is the time zone of the calendar.`, - }, - { - displayName: 'Password', - name: 'password', - type: 'string', - default: '', - description: 'Password to join the webinar with maximum 10 characters.', - }, { displayName: 'Agenda', name: 'agenda', @@ -565,27 +480,6 @@ export const webinarFields = [ default: '', description: 'Webinar agenda.', }, - { - displayName: 'Host Video', - name: 'host_video', - type: 'boolean', - default: false, - description: 'Start video when host joins the webinar.', - }, - { - displayName: 'Panelists Video', - name: 'panelists_video', - type: 'boolean', - default: false, - description: 'Start video when panelists joins the webinar.', - }, - { - displayName: 'Practice Session', - name: 'practice_session', - type: 'boolean', - default: false, - description: 'Enable Practice session.', - }, { displayName: 'Alternative Hosts', name: 'alternative_hosts', @@ -657,6 +551,48 @@ export const webinarFields = [ default: 'both', description: 'Determine how participants can join audio portion of the webinar.', }, + { + displayName: 'Duration', + name: 'duration', + type: 'string', + default: '', + description: 'Duration.', + }, + { + displayName: 'Host Video', + name: 'host_video', + type: 'boolean', + default: false, + description: 'Start video when host joins the webinar.', + }, + { + displayName: 'Occurence Id', + name: 'occurence_id', + type: 'string', + default: '', + description: `Webinar occurence Id.`, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password to join the webinar with maximum 10 characters.', + }, + { + displayName: 'Panelists Video', + name: 'panelists_video', + type: 'boolean', + default: false, + description: 'Start video when panelists joins the webinar.', + }, + { + displayName: 'Practice Session', + name: 'practice_session', + type: 'boolean', + default: false, + description: 'Enable Practice session.', + }, { displayName: 'Registration type', name: 'registration_type', @@ -678,7 +614,51 @@ export const webinarFields = [ default: 1, description: 'Registration type. Used for recurring webinars with fixed time only', }, - + { + displayName: 'Start time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'Start time should be used only for scheduled or recurring webinar with fixed time', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Webinar topic', + name: 'topic', + type: 'string', + default: '', + description: `Webinar topic.`, + }, + { + displayName: 'Webinar type', + name: 'type', + type: 'options', + options: [ + { + name: 'Webinar', + value: 5, + }, + { + name: 'Recurring webinar with no fixed time', + value: 6, + }, + { + name: 'Recurring webinar with fixed time', + value: 9, + }, + ], + default: 5, + description: 'Webinar type.' + }, ], }, diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index fb3b7565f7..8e6eb10d2b 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -183,12 +183,16 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.showPreviousOccurences) + if (additionalFields.showPreviousOccurences) { qs.show_previous_occurences = additionalFields.showPreviousOccurences as boolean; - if (additionalFields.occurenceId) + } + + if (additionalFields.occurenceId) { qs.occurence_id = additionalFields.occurenceId as string; + } + responseData = await zoomApiRequest.call( this, 'GET', @@ -218,12 +222,16 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.scheduleForReminder) + if (additionalFields.scheduleForReminder) { qs.schedule_for_reminder = additionalFields.scheduleForReminder as boolean; - if (additionalFields.occurenceId) + } + + if (additionalFields.occurenceId) { qs.occurence_id = additionalFields.occurenceId; + } + responseData = await zoomApiRequest.call( this, 'DELETE', @@ -669,12 +677,16 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.showPreviousOccurences) + if (additionalFields.showPreviousOccurences) { qs.show_previous_occurences = additionalFields.showPreviousOccurences as boolean; - if (additionalFields.occurenceId) + } + + if (additionalFields.occurenceId) { qs.occurence_id = additionalFields.occurenceId as string; + } + responseData = await zoomApiRequest.call( this, 'GET', @@ -705,9 +717,11 @@ export class Zoom implements INodeType { ) as IDataObject; - if (additionalFields.occurenceId) + if (additionalFields.occurenceId) { qs.occurence_id = additionalFields.occurenceId; + } + responseData = await zoomApiRequest.call( this, 'DELETE', From a6e40aaebe3cbd1b099f65ad4797cace9104ac92 Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 23 Jun 2020 12:27:47 -0700 Subject: [PATCH 06/44] minor fix --- .../nodes-base/nodes/Zoom/GenericFunctions.ts | 16 +++++++--------- .../nodes/Zoom/MeetingRegistrantDescription.ts | 2 -- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 3 +-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts index d361a9761a..d5cad39297 100644 --- a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -1,13 +1,16 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; import { IExecuteFunctions, IExecuteSingleFunctions, - ILoadOptionsFunctions + ILoadOptionsFunctions, } from 'n8n-core'; -import { IDataObject } from 'n8n-workflow'; -import * as _ from 'lodash'; +import { + IDataObject, +} from 'n8n-workflow'; export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}, headers: {} | undefined = undefined, option: {} = {}): Promise { // tslint:disable-line:no-any @@ -60,8 +63,6 @@ export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFun // If that data does not exist for some reason return the actual error throw error; } - - } @@ -75,10 +76,7 @@ export async function zoomApiRequestAllItems( ): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; - let responseData; - //query.maxResults = 300; - do { responseData = await zoomApiRequest.call( this, diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index 232f8cf42c..2d32c55b42 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -36,8 +36,6 @@ export const meetingRegistrantOperations = [ } ] as INodeProperties[]; - - export const meetingRegistrantFields = [ /* -------------------------------------------------------------------------- */ /* meetingRegistrants:create */ diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 8e6eb10d2b..078da4879c 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -29,6 +29,7 @@ import { webinarOperations, webinarFields, } from './WebinarDescription'; + import * as moment from 'moment-timezone'; interface Settings { @@ -46,8 +47,6 @@ interface Settings { registration_type?: number; approval_type?: number; practice_session?: boolean; - - } export class Zoom implements INodeType { description: INodeTypeDescription = { From 37ff6a8f1333b74dea2548e9ca56746fddced9c1 Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 23 Jun 2020 13:40:43 -0700 Subject: [PATCH 07/44] fix spellings --- .../nodes-base/nodes/Zoom/GenericFunctions.ts | 1 - .../nodes/Zoom/MeetingDescription.ts | 40 ++++++++--------- .../Zoom/MeetingRegistrantDescription.ts | 18 ++++---- .../nodes/Zoom/WebinarDescription.ts | 36 ++++++++-------- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 43 +++++++++---------- 5 files changed, 68 insertions(+), 70 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts index d5cad39297..74b5a18d06 100644 --- a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -46,7 +46,6 @@ export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFun return await this.helpers.request(options); } else { //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'zoomOAuth2Api', options); } } catch (error) { diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index b9a1c74cb5..4d8281660d 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -98,7 +98,7 @@ export const meetingFields = [ name: 'alternative_hosts', type: 'string', default: '', - description: 'Alternative hosts email ids.', + description: 'Alternative hosts email IDs.', }, { displayName: 'Auto recording', @@ -242,11 +242,11 @@ export const meetingFields = [ value: 1, }, { - name: 'Attendees need to register for every occurence', + name: 'Attendees need to register for every occurrence', value: 2, }, { - name: 'Attendees register once and can choose one or more occurences to attend', + name: 'Attendees register once and can choose one or more occurrences to attend', value: 3, }, ], @@ -326,18 +326,18 @@ export const meetingFields = [ }, options: [ { - displayName: 'Occurence Id', - name: 'occurenceId', + displayName: 'Occurrence ID', + name: 'occurrenceId', type: 'string', default: '', - description: 'To view meeting details of a particular occurence of the recurring meeting.', + description: 'To view meeting details of a particular occurrence of the recurring meeting.', }, { - displayName: 'Show Previous Occurences', - name: 'showPreviousOccurences', + displayName: 'Show Previous Occurrences', + name: 'showPreviousOccurrences', type: 'boolean', default: '', - description: 'To view meeting details of all previous occurences of the recurring meeting.', + description: 'To view meeting details of all previous occurrences of the recurring meeting.', }, ], }, @@ -360,7 +360,7 @@ export const meetingFields = [ ], }, }, - description: 'User ID or email-id.', + description: 'User ID or email-ID.', }, { displayName: 'Return All', @@ -483,11 +483,11 @@ export const meetingFields = [ }, options: [ { - displayName: 'Occurence Id', - name: 'occurenceId', + displayName: 'Occurence ID', + name: 'occurrenceId', type: 'string', default: '', - description: 'Meeting occurence Id.', + description: 'Meeting occurrence ID.', }, { displayName: 'Schedule a reminder', @@ -503,7 +503,7 @@ export const meetingFields = [ /* meeting:update */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting Id', + displayName: 'Meeting ID', name: 'meetingId', type: 'string', default: '', @@ -630,11 +630,11 @@ export const meetingFields = [ description: 'Start video when host joins the meeting.', }, { - displayName: 'Occurence Id', - name: 'occurenceId', + displayName: 'Occurrence Id', + name: 'occurrenceId', type: 'string', default: '', - description: 'Occurence ID.', + description: 'Occurrence ID.', }, { displayName: 'Meeting topic', @@ -696,15 +696,15 @@ export const meetingFields = [ type: 'options', options: [ { - name: 'Attendees register once and can attend any of the occurences', + name: 'Attendees register once and can attend any of the occurrences', value: 1, }, { - name: 'Attendees need to register for every occurence', + name: 'Attendees need to register for every occurrence', value: 2, }, { - name: 'Attendees register once and can choose one or more occurences to attend', + name: 'Attendees register once and can choose one or more occurrences to attend', value: 3, }, ], diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index 2d32c55b42..8cef3383aa 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -155,11 +155,11 @@ export const meetingRegistrantFields = [ description: 'Last Name.', }, { - displayName: 'Occurence Ids', - name: 'occurenceId', + displayName: 'Occurrence Ids', + name: 'occurrenceId', type: 'string', default: '', - description: 'Occurence IDs separated by comma.', + description: 'Occurrence IDs separated by comma.', }, { displayName: 'Organization', @@ -328,11 +328,11 @@ export const meetingRegistrantFields = [ }, options: [ { - displayName: 'Occurence Id', - name: 'occurence_id', + displayName: 'Occurrence Id', + name: 'occurrence_id', type: 'string', default: '', - description: `Occurence Id.`, + description: `Occurrence Id.`, }, { displayName: 'Status', @@ -429,11 +429,11 @@ export const meetingRegistrantFields = [ }, options: [ { - displayName: 'Occurence Id', - name: 'occurenceId', + displayName: 'Occurrence Id', + name: 'occurrenceId', type: 'string', default: '', - description: 'Occurence ID.', + description: 'Occurrence ID.', }, ], diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts index 4bbec11fd4..b673b77fa9 100644 --- a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -209,11 +209,11 @@ export const webinarFields = [ value: 1, }, { - name: 'Attendees need to register for every occurence', + name: 'Attendees need to register for every occurrence', value: 2, }, { - name: 'Attendees register once and can choose one or more occurences to attend', + name: 'Attendees register once and can choose one or more occurrences to attend', value: 3, }, ], @@ -312,14 +312,14 @@ export const webinarFields = [ name: 'occurenceId', type: 'string', default: '', - description: 'To view webinar details of a particular occurence of the recurring webinar.', + description: 'To view webinar details of a particular occurrence of the recurring webinar.', }, { - displayName: 'Show Previous Occurences', - name: 'showPreviousOccurences', + displayName: 'Show Previous Occurrences', + name: 'showPreviousOccurrences', type: 'boolean', default: '', - description: 'To view webinar details of all previous occurences of the recurring webinar.', + description: 'To view webinar details of all previous occurrences of the recurring webinar.', }, ], }, @@ -386,7 +386,7 @@ export const webinarFields = [ description: 'How many results to return.', }, /* -------------------------------------------------------------------------- */ - /* webina:delete */ + /* webinar:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Webinar Id', @@ -424,11 +424,11 @@ export const webinarFields = [ }, options: [ { - displayName: 'Occurence Id', - name: 'occurenceId', + displayName: 'Occurrence Id', + name: 'occurrenceId', type: 'string', default: '', - description: 'Webinar occurence Id.', + description: 'Webinar occurrence Id.', }, ], @@ -566,11 +566,11 @@ export const webinarFields = [ description: 'Start video when host joins the webinar.', }, { - displayName: 'Occurence Id', - name: 'occurence_id', + displayName: 'Occurrence Id', + name: 'occurrence_id', type: 'string', default: '', - description: `Webinar occurence Id.`, + description: `Webinar occurrence Id.`, }, { displayName: 'Password', @@ -599,27 +599,27 @@ export const webinarFields = [ type: 'options', options: [ { - name: 'Attendees register once and can attend any of the occurences', + name: 'Attendees register once and can attend any of the occurrences', value: 1, }, { - name: 'Attendees need to register for every occurence', + name: 'Attendees need to register for every occurrence', value: 2, }, { - name: 'Attendees register once and can choose one or more occurences to attend', + name: 'Attendees register once and can choose one or more occurrences to attend', value: 3, }, ], default: 1, - description: 'Registration type. Used for recurring webinars with fixed time only', + description: 'Registration type. Used for recurring webinars with fixed time only.', }, { displayName: 'Start time', name: 'startTime', type: 'dateTime', default: '', - description: 'Start time should be used only for scheduled or recurring webinar with fixed time', + description: 'Start time should be used only for scheduled or recurring webinar with fixed time.', }, { displayName: 'Timezone', diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 078da4879c..9e0cf88889 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -177,18 +177,17 @@ export class Zoom implements INodeType { if (operation === 'get') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meeting const meetingId = this.getNodeParameter('meetingId', i) as string; - const additionalFields = this.getNodeParameter( 'additionalFields', i ) as IDataObject; - if (additionalFields.showPreviousOccurences) { - qs.show_previous_occurences = additionalFields.showPreviousOccurences as boolean; + if (additionalFields.showPreviousOccurrences) { + qs.show_previous_occurrences = additionalFields.showPreviousOccurrences as boolean; } - if (additionalFields.occurenceId) { - qs.occurence_id = additionalFields.occurenceId as string; + if (additionalFields.occurrenceId) { + qs.occurrence_id = additionalFields.occurrenceId as string; } @@ -226,8 +225,8 @@ export class Zoom implements INodeType { } - if (additionalFields.occurenceId) { - qs.occurence_id = additionalFields.occurenceId; + if (additionalFields.occurrenceId) { + qs.occurrence_id = additionalFields.occurrenceId; } @@ -363,8 +362,8 @@ export class Zoom implements INodeType { i ) as IDataObject; - if (additionalFields.occurenceId) { - qs.occurence_id = additionalFields.occurenceId as string; + if (additionalFields.occurrenceId) { + qs.occurrence_id = additionalFields.occurrenceId as string; } if (additionalFields.cn_meeting) { @@ -488,8 +487,8 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.occurenceId) { - qs.occurence_ids = additionalFields.occurenceId as string; + if (additionalFields.occurrenceId) { + qs.occurrence_ids = additionalFields.occurrenceId as string; } if (additionalFields.lastName) { body.last_name = additionalFields.lastName as string; @@ -542,8 +541,8 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.occurenceId) { - qs.occurence_id = additionalFields.occurenceId as string; + if (additionalFields.occurrenceId) { + qs.occurrence_id = additionalFields.occurrenceId as string; } if (additionalFields.status) { qs.status = additionalFields.status as string; @@ -567,7 +566,7 @@ export class Zoom implements INodeType { i ) as IDataObject; if (additionalFields.occurenceId) { - qs.occurence_id = additionalFields.occurenceId as string; + qs.occurrence_id = additionalFields.occurrenceId as string; } responseData = await zoomApiRequest.call( this, @@ -676,13 +675,13 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.showPreviousOccurences) { - qs.show_previous_occurences = additionalFields.showPreviousOccurences as boolean; + if (additionalFields.showPreviousOccurrences) { + qs.show_previous_occurrences = additionalFields.showPreviousOccurrences as boolean; } - if (additionalFields.occurenceId) { - qs.occurence_id = additionalFields.occurenceId as string; + if (additionalFields.occurrenceId) { + qs.occurrence_id = additionalFields.occurrenceId as string; } @@ -716,8 +715,8 @@ export class Zoom implements INodeType { ) as IDataObject; - if (additionalFields.occurenceId) { - qs.occurence_id = additionalFields.occurenceId; + if (additionalFields.occurrenceId) { + qs.occurrence_id = additionalFields.occurrenceId; } @@ -737,8 +736,8 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.occurence_id) { - qs.occurence_id = additionalFields.occurence_id as string; + if (additionalFields.occurrence_id) { + qs.occurrence_id = additionalFields.occurrence_id as string; } const settings: Settings = {}; From 83828a19abda3945196523fc698812d2bfa9bd02 Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 23 Jun 2020 14:26:00 -0700 Subject: [PATCH 08/44] follow codebase conventions --- .../nodes-base/nodes/Zoom/GenericFunctions.ts | 1 - .../nodes/Zoom/MeetingDescription.ts | 16 +++++------ .../Zoom/MeetingRegistrantDescription.ts | 12 ++++---- .../nodes/Zoom/WebinarDescription.ts | 28 +++++++++---------- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 8 +++--- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts index 74b5a18d06..500b0a64d9 100644 --- a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -14,7 +14,6 @@ import { export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}, headers: {} | undefined = undefined, option: {} = {}): Promise { // tslint:disable-line:no-any - // tslint:disable-line:no-any const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; let options: OptionsWithUri = { diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index 4d8281660d..829418d12b 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -51,7 +51,7 @@ export const meetingFields = [ /* meeting:create */ /* -------------------------------------------------------------------------- */ { - displayName: 'User Id', + displayName: 'User ID', name: 'userId', type: 'string', default: '', @@ -258,7 +258,7 @@ export const meetingFields = [ name: 'scheduleFor', type: 'string', default: '', - description: 'Schedule meeting for someone else from your account, provide their email id.', + description: 'Schedule meeting for someone else from your account, provide their email ID.', }, { displayName: 'Start time', @@ -290,7 +290,7 @@ export const meetingFields = [ /* meeting:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting Id', + displayName: 'Meeting ID', name: 'meetingId', type: 'string', default: '', @@ -345,7 +345,7 @@ export const meetingFields = [ /* meeting:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'User Id', + displayName: 'User ID', name: 'userId', type: 'string', default: '', @@ -448,7 +448,7 @@ export const meetingFields = [ /* meeting:delete */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting Id', + displayName: 'Meeting ID', name: 'meetingId', type: 'string', default: '', @@ -549,7 +549,7 @@ export const meetingFields = [ name: 'alternative_hosts', type: 'string', default: '', - description: 'Alternative hosts email ids.', + description: 'Alternative hosts email IDs.', }, { displayName: 'Audio', @@ -630,7 +630,7 @@ export const meetingFields = [ description: 'Start video when host joins the meeting.', }, { - displayName: 'Occurrence Id', + displayName: 'Occurrence ID', name: 'occurrenceId', type: 'string', default: '', @@ -716,7 +716,7 @@ export const meetingFields = [ name: 'scheduleFor', type: 'string', default: '', - description: 'Schedule meeting for someone else from your account, provide their email id.', + description: 'Schedule meeting for someone else from your account, provide their email ID.', }, { displayName: 'Start time', diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index 8cef3383aa..b9a5147364 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -155,7 +155,7 @@ export const meetingRegistrantFields = [ description: 'Last Name.', }, { - displayName: 'Occurrence Ids', + displayName: 'Occurrence IDs', name: 'occurrenceId', type: 'string', default: '', @@ -251,7 +251,7 @@ export const meetingRegistrantFields = [ /* meetingRegistrants:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting Id', + displayName: 'Meeting ID', name: 'meetingId', type: 'string', default: '', @@ -328,11 +328,11 @@ export const meetingRegistrantFields = [ }, options: [ { - displayName: 'Occurrence Id', + displayName: 'Occurrence ID', name: 'occurrence_id', type: 'string', default: '', - description: `Occurrence Id.`, + description: `Occurrence ID.`, }, { displayName: 'Status', @@ -362,7 +362,7 @@ export const meetingRegistrantFields = [ /* meetingRegistrants:update */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting Id', + displayName: 'Meeting ID', name: 'meetingId', type: 'string', default: '', @@ -429,7 +429,7 @@ export const meetingRegistrantFields = [ }, options: [ { - displayName: 'Occurrence Id', + displayName: 'Occurrence ID', name: 'occurrenceId', type: 'string', default: '', diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts index b673b77fa9..19796ea51a 100644 --- a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -51,7 +51,7 @@ export const webinarFields = [ /* webinar:create */ /* -------------------------------------------------------------------------- */ { - displayName: 'User Id', + displayName: 'User ID', name: 'userId', type: 'string', default: '', @@ -98,7 +98,7 @@ export const webinarFields = [ name: 'alternative_hosts', type: 'string', default: '', - description: 'Alternative hosts email ids.', + description: 'Alternative hosts email IDs.', }, { displayName: 'Approval type', @@ -272,7 +272,7 @@ export const webinarFields = [ /* webinar:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Webinar Id', + displayName: 'Webinar ID', name: 'webinarId', type: 'string', default: '', @@ -308,7 +308,7 @@ export const webinarFields = [ }, options: [ { - displayName: 'Occurence Id', + displayName: 'Occurence ID', name: 'occurenceId', type: 'string', default: '', @@ -327,7 +327,7 @@ export const webinarFields = [ /* webinar:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'User Id', + displayName: 'User ID', name: 'userId', type: 'string', default: '', @@ -342,7 +342,7 @@ export const webinarFields = [ ], }, }, - description: 'User ID or email-id.', + description: 'User ID or email-ID.', }, { displayName: 'Return All', @@ -389,7 +389,7 @@ export const webinarFields = [ /* webinar:delete */ /* -------------------------------------------------------------------------- */ { - displayName: 'Webinar Id', + displayName: 'Webinar ID', name: 'webinarId', type: 'string', default: '', @@ -404,7 +404,7 @@ export const webinarFields = [ ], }, }, - description: 'WebinarId ID.', + description: 'Webinar ID.', }, { displayName: 'Additional Fields', @@ -424,11 +424,11 @@ export const webinarFields = [ }, options: [ { - displayName: 'Occurrence Id', + displayName: 'Occurrence ID', name: 'occurrenceId', type: 'string', default: '', - description: 'Webinar occurrence Id.', + description: 'Webinar occurrence ID.', }, ], @@ -438,7 +438,7 @@ export const webinarFields = [ /* webinar:update */ /* -------------------------------------------------------------------------- */ { - displayName: 'User Id', + displayName: 'User ID', name: 'userId', type: 'string', default: '', @@ -485,7 +485,7 @@ export const webinarFields = [ name: 'alternative_hosts', type: 'string', default: '', - description: 'Alternative hosts email ids.', + description: 'Alternative hosts email IDs.', }, { displayName: 'Approval type', @@ -566,11 +566,11 @@ export const webinarFields = [ description: 'Start video when host joins the webinar.', }, { - displayName: 'Occurrence Id', + displayName: 'Occurrence ID', name: 'occurrence_id', type: 'string', default: '', - description: `Webinar occurrence Id.`, + description: `Webinar occurrence ID.`, }, { displayName: 'Password', diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 9e0cf88889..56901ac2ce 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -163,13 +163,13 @@ export class Zoom implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - const length = (items.length as unknown) as number; - let qs: IDataObject; + let qs: IDataObject = {}; + let body: IDataObject = {}; let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; - let body: IDataObject = {}; - for (let i = 0; i < length; i++) { + + for (let i = 0; i < items.length; i++) { qs = {}; //https://marketplace.zoom.us/docs/api-reference/zoom-api/ if (resource === 'meeting') { From 69a1f8af00f6bd64795c709fd25e407cfc28c0e5 Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 23 Jun 2020 20:29:47 -0700 Subject: [PATCH 09/44] fix pagination --- .../credentials/ZoomOAuth2Api.credentials.ts | 16 ++++---------- .../nodes-base/nodes/Zoom/GenericFunctions.ts | 18 ++++++++++----- .../nodes/Zoom/MeetingDescription.ts | 22 +++++++++---------- .../Zoom/MeetingRegistrantDescription.ts | 16 +++++++------- .../nodes/Zoom/WebinarDescription.ts | 14 ++++++------ packages/nodes-base/nodes/Zoom/Zoom.node.ts | 16 ++++++++++---- 6 files changed, 55 insertions(+), 47 deletions(-) diff --git a/packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts index c00c694661..f85cc75254 100644 --- a/packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts @@ -1,14 +1,7 @@ -import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; - -const userScopes = [ - 'meeting:read', - 'meeting:write', - 'user:read', - 'user:write', - 'user_profile', - 'webinar:read', - 'webinar:write' -]; +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; export class ZoomOAuth2Api implements ICredentialType { name = 'zoomOAuth2Api'; @@ -27,7 +20,6 @@ export class ZoomOAuth2Api implements ICredentialType { type: 'hidden' as NodePropertyTypes, default: 'https://zoom.us/oauth/token' }, - { displayName: 'Scope', name: 'scope', diff --git a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts index 500b0a64d9..6a8593e3ab 100644 --- a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -75,6 +75,7 @@ export async function zoomApiRequestAllItems( // tslint:disable-line:no-any const returnData: IDataObject[] = []; let responseData; + query.page_number = 0; do { responseData = await zoomApiRequest.call( this, @@ -83,14 +84,21 @@ export async function zoomApiRequestAllItems( body, query ); - query.page_number = responseData['page_number']; + query.page_number++; returnData.push.apply(returnData, responseData[propertyName]); + // zoom free plan rate limit is 1 request/second + // TODO just wait when the plan is free + await wait(); } while ( - responseData['page_number'] !== undefined && - responseData['page_number'] !== '' + responseData.page_count !== responseData.page_number ); return returnData; } - - +function wait() { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve(true); + }, 1000); + }); +} diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index 829418d12b..5a8a5966a9 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -39,7 +39,7 @@ export const meetingOperations = [ name: 'Update', value: 'update', description: 'Update a meeting', - } + }, ], default: 'create', description: 'The operation to perform.', @@ -69,7 +69,7 @@ export const meetingFields = [ description: 'User ID or email address of user.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -83,7 +83,7 @@ export const meetingFields = [ resource: [ 'meeting', ], - } + }, }, options: [ { @@ -209,7 +209,7 @@ export const meetingFields = [ ], default: 2, - description: 'Meeting type.' + description: 'Meeting type.', }, { displayName: 'Muting before entry', @@ -219,7 +219,7 @@ export const meetingFields = [ description: 'Mute participants upon entry.', }, { - displayName: 'Participant Video', + displayName: 'Participant video', name: 'participant_video', type: 'boolean', default: false, @@ -308,7 +308,7 @@ export const meetingFields = [ description: 'Meeting ID.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -398,13 +398,13 @@ export const meetingFields = [ }, typeOptions: { minValue: 1, - maxValue: 300 + maxValue: 300, }, default: 30, description: 'How many results to return.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -456,7 +456,7 @@ export const meetingFields = [ displayOptions: { show: { operation: [ - 'delete' + 'delete', ], resource: [ 'meeting', @@ -521,7 +521,7 @@ export const meetingFields = [ description: 'Meeting ID.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -667,7 +667,7 @@ export const meetingFields = [ ], default: 2, - description: 'Meeting type.' + description: 'Meeting type.', }, { displayName: 'Muting before entry', diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index b9a5147364..b7e5271f55 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -95,7 +95,7 @@ export const meetingRegistrantFields = [ description: 'First Name.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -109,7 +109,7 @@ export const meetingRegistrantFields = [ resource: [ 'meetingRegistrants', ], - } + }, }, options: [ { @@ -202,7 +202,7 @@ export const meetingRegistrantFields = [ }, ], default: '', - description: 'Meeting type.' + description: 'Meeting type.', }, { displayName: 'Role in purchase process', @@ -228,7 +228,7 @@ export const meetingRegistrantFields = [ ], default: '', - description: 'Role in purchase process.' + description: 'Role in purchase process.', }, { displayName: 'State', @@ -304,13 +304,13 @@ export const meetingRegistrantFields = [ }, typeOptions: { minValue: 1, - maxValue: 300 + maxValue: 300, }, default: 30, description: 'How many results to return.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -324,7 +324,7 @@ export const meetingRegistrantFields = [ resource: [ 'meetingRegistrants', ], - } + }, }, options: [ { @@ -412,7 +412,7 @@ export const meetingRegistrantFields = [ description: `Registrant Status.`, }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts index 19796ea51a..228cf9f1ae 100644 --- a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -69,7 +69,7 @@ export const webinarFields = [ description: 'User ID or email address of user.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -263,7 +263,7 @@ export const webinarFields = [ }, ], default: 5, - description: 'Webinar type.' + description: 'Webinar type.', }, ], @@ -290,7 +290,7 @@ export const webinarFields = [ description: 'Webinar ID.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -380,7 +380,7 @@ export const webinarFields = [ }, typeOptions: { minValue: 1, - maxValue: 300 + maxValue: 300, }, default: 30, description: 'How many results to return.', @@ -397,7 +397,7 @@ export const webinarFields = [ displayOptions: { show: { operation: [ - 'delete' + 'delete', ], resource: [ 'webinarId', @@ -456,7 +456,7 @@ export const webinarFields = [ description: 'User ID or email address of user.', }, { - displayName: 'Additional settings', + displayName: 'Additional fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -470,7 +470,7 @@ export const webinarFields = [ resource: [ 'webinar', ], - } + }, }, options: [ { diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 56901ac2ce..18e3614cc4 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -203,13 +203,21 @@ export class Zoom implements INodeType { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetings const userId = this.getNodeParameter('userId', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const additionalFields = this.getNodeParameter( + 'additionalFields', + i + ) as IDataObject; + if (additionalFields.type) { + qs.type = additionalFields.type as string; + + } if (returnAll) { - responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/users/${userId}/meetings`, {}, qs); + responseData = await zoomApiRequestAllItems.call(this, 'meetings', 'GET', `/users/${userId}/meetings`, {}, qs); } else { - const limit = this.getNodeParameter('limit', i) as number; - qs.page_size = limit; + qs.page_size = this.getNodeParameter('limit', i) as number;; responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/meetings`, {}, qs); - responseData = responseData.results; + } } From 5d98f5673f7d7637b3944dd63135853b6bd91ddc Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Tue, 23 Jun 2020 20:53:49 -0700 Subject: [PATCH 10/44] fix specific results for registrants --- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 18e3614cc4..f33a2a338e 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -215,7 +215,7 @@ export class Zoom implements INodeType { if (returnAll) { responseData = await zoomApiRequestAllItems.call(this, 'meetings', 'GET', `/users/${userId}/meetings`, {}, qs); } else { - qs.page_size = this.getNodeParameter('limit', i) as number;; + qs.page_size = this.getNodeParameter('limit', i) as number; responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/meetings`, {}, qs); } @@ -559,10 +559,9 @@ export class Zoom implements INodeType { if (returnAll) { responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/meetings/${meetingId}/registrants`, {}, qs); } else { - const limit = this.getNodeParameter('limit', i) as number; - qs.page_size = limit; + qs.page_size = this.getNodeParameter('limit', i) as number; responseData = await zoomApiRequest.call(this, 'GET', `/meetings/${meetingId}/registrants`, {}, qs); - responseData = responseData.results; + } } @@ -708,10 +707,9 @@ export class Zoom implements INodeType { if (returnAll) { responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/users/${userId}/webinars`, {}, qs); } else { - const limit = this.getNodeParameter('limit', i) as number; - qs.page_size = limit; + qs.page_size = this.getNodeParameter('limit', i) as number; responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/webinars`, {}, qs); - responseData = responseData.results; + } } if (operation === 'delete') { From e3cf858ebc64c78fdf7c3ce5ef4f0772a45f61ba Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Wed, 24 Jun 2020 10:47:35 -0700 Subject: [PATCH 11/44] fix naming conventions --- .../nodes/Zoom/MeetingDescription.ts | 52 +++++++++--------- .../Zoom/MeetingRegistrantDescription.ts | 20 +++---- .../nodes/Zoom/WebinarDescription.ts | 54 +++++++++---------- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 2 +- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index 5a8a5966a9..d335901bdf 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -69,7 +69,7 @@ export const meetingFields = [ description: 'User ID or email address of user.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -101,7 +101,7 @@ export const meetingFields = [ description: 'Alternative hosts email IDs.', }, { - displayName: 'Auto recording', + displayName: 'Auto Recording', name: 'auto_recording', type: 'options', options: [ @@ -179,14 +179,14 @@ export const meetingFields = [ description: 'Allow participants to join the meeting before host starts it.', }, { - displayName: 'Meeting topic', + displayName: 'Meeting Topic', name: 'topic', type: 'string', default: '', description: `Meeting topic.`, }, { - displayName: 'Meeting type', + displayName: 'Meeting Type', name: 'type', type: 'options', options: [ @@ -199,11 +199,11 @@ export const meetingFields = [ value: 2, }, { - name: 'Recurring meeting with no fixed time', + name: 'Recurring Meeting with no fixed time', value: 3, }, { - name: 'Recurring meeting with no fixed time', + name: 'Recurring Meeting with fixed time', value: 8, }, @@ -219,7 +219,7 @@ export const meetingFields = [ description: 'Mute participants upon entry.', }, { - displayName: 'Participant video', + displayName: 'Participant Video', name: 'participant_video', type: 'boolean', default: false, @@ -233,7 +233,7 @@ export const meetingFields = [ description: 'Password to join the meeting with maximum 10 characters.', }, { - displayName: 'Registration type', + displayName: 'Registration Type', name: 'registration_type', type: 'options', options: [ @@ -254,14 +254,14 @@ export const meetingFields = [ description: 'Registration type. Used for recurring meetings with fixed time only', }, { - displayName: 'Schedule for', + displayName: 'Schedule For', name: 'scheduleFor', type: 'string', default: '', description: 'Schedule meeting for someone else from your account, provide their email ID.', }, { - displayName: 'Start time', + displayName: 'Start Time', name: 'startTime', type: 'dateTime', default: '', @@ -308,7 +308,7 @@ export const meetingFields = [ description: 'Meeting ID.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -404,7 +404,7 @@ export const meetingFields = [ description: 'How many results to return.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -490,7 +490,7 @@ export const meetingFields = [ description: 'Meeting occurrence ID.', }, { - displayName: 'Schedule a reminder', + displayName: 'Schedule Reminder', name: 'scheduleForReminder', type: 'boolean', default: false, @@ -521,7 +521,7 @@ export const meetingFields = [ description: 'Meeting ID.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -573,16 +573,16 @@ export const meetingFields = [ description: 'Determine how participants can join audio portion of the meeting.', }, { - displayName: 'Auto recording', + displayName: 'Auto Recording', name: 'auto_recording', type: 'options', options: [ { - name: 'Record on local', + name: 'Record on Local', value: 'local', }, { - name: 'Record on cloud', + name: 'Record on Cloud', value: 'cloud', }, { @@ -602,7 +602,7 @@ export const meetingFields = [ description: 'Duration.', }, { - displayName: 'Join before Host', + displayName: 'Join Before Host', name: 'join_before_host', type: 'boolean', default: false, @@ -637,14 +637,14 @@ export const meetingFields = [ description: 'Occurrence ID.', }, { - displayName: 'Meeting topic', + displayName: 'Meeting Topic', name: 'topic', type: 'string', default: '', description: `Meeting topic.`, }, { - displayName: 'Meeting type', + displayName: 'Meeting Type', name: 'type', type: 'options', options: [ @@ -657,11 +657,11 @@ export const meetingFields = [ value: 2, }, { - name: 'Recurring meeting with no fixed time', + name: 'Recurring Meeting with no fixed time', value: 3, }, { - name: 'Recurring meeting with no fixed time', + name: 'Recurring Meeting with fixed time', value: 8, }, @@ -670,7 +670,7 @@ export const meetingFields = [ description: 'Meeting type.', }, { - displayName: 'Muting before entry', + displayName: 'Muting Before Entry', name: 'mute_upon_entry', type: 'boolean', default: false, @@ -691,7 +691,7 @@ export const meetingFields = [ description: 'Start video when participant joins the meeting.', }, { - displayName: 'Registration type', + displayName: 'Registration Type', name: 'registration_type', type: 'options', options: [ @@ -712,14 +712,14 @@ export const meetingFields = [ description: 'Registration type. Used for recurring meetings with fixed time only', }, { - displayName: 'Schedule for', + displayName: 'Schedule For', name: 'scheduleFor', type: 'string', default: '', description: 'Schedule meeting for someone else from your account, provide their email ID.', }, { - displayName: 'Start time', + displayName: 'Start Time', name: 'startTime', type: 'dateTime', default: '', diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index b7e5271f55..d1527f165a 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -22,12 +22,12 @@ export const meetingRegistrantOperations = [ { name: 'Update', value: 'update', - description: 'Update Meeting Registrant status', + description: 'Update Meeting Registrant Status', }, { name: 'Get All', value: 'getAll', - description: 'Retrieve all meeting registrants', + description: 'Retrieve all Meeting Registrants', }, ], @@ -77,7 +77,7 @@ export const meetingRegistrantFields = [ description: 'Valid email-id of registrant.', }, { - displayName: 'First name', + displayName: 'First Name', name: 'firstName', required: true, type: 'string', @@ -95,7 +95,7 @@ export const meetingRegistrantFields = [ description: 'First Name.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -141,7 +141,7 @@ export const meetingRegistrantFields = [ description: 'Valid country of registrant.', }, { - displayName: 'Job title', + displayName: 'Job Title', name: 'job_title', type: 'string', default: '', @@ -176,7 +176,7 @@ export const meetingRegistrantFields = [ description: 'Valid phone number of registrant.', }, { - displayName: 'Purchasing time frame', + displayName: 'Purchasing Time Frame', name: 'purchasing_time_frame', type: 'options', options: [ @@ -205,7 +205,7 @@ export const meetingRegistrantFields = [ description: 'Meeting type.', }, { - displayName: 'Role in purchase process', + displayName: 'Role in Purchase Process', name: 'role_in_purchase_process', type: 'options', options: [ @@ -238,7 +238,7 @@ export const meetingRegistrantFields = [ description: 'Valid state of registrant.', }, { - displayName: 'Zip code', + displayName: 'Zip Code', name: 'zip', type: 'string', default: '', @@ -310,7 +310,7 @@ export const meetingRegistrantFields = [ description: 'How many results to return.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -412,7 +412,7 @@ export const meetingRegistrantFields = [ description: `Registrant Status.`, }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts index 228cf9f1ae..1c0e03e096 100644 --- a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -69,7 +69,7 @@ export const webinarFields = [ description: 'User ID or email address of user.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -101,20 +101,20 @@ export const webinarFields = [ description: 'Alternative hosts email IDs.', }, { - displayName: 'Approval type', + displayName: 'Approval Type', name: 'approval_type', type: 'options', options: [ { - name: 'Automatically approve', + name: 'Automatically Approve', value: 0, }, { - name: 'Manually approve', + name: 'Manually Approve', value: 1, }, { - name: 'No registration required', + name: 'No Registration Required', value: 2, }, ], @@ -144,16 +144,16 @@ export const webinarFields = [ description: 'Determine how participants can join audio portion of the webinar.', }, { - displayName: 'Auto recording', + displayName: 'Auto Recording', name: 'auto_recording', type: 'options', options: [ { - name: 'Record on local', + name: 'Record on Local', value: 'local', }, { - name: 'Record on cloud', + name: 'Record on Cloud', value: 'cloud', }, { @@ -200,7 +200,7 @@ export const webinarFields = [ description: 'Enable Practice session.', }, { - displayName: 'Registration type', + displayName: 'Registration Type', name: 'registration_type', type: 'options', options: [ @@ -221,7 +221,7 @@ export const webinarFields = [ description: 'Registration type. Used for recurring webinar with fixed time only', }, { - displayName: 'Start time', + displayName: 'Start Time', name: 'startTime', type: 'dateTime', default: '', @@ -238,14 +238,14 @@ export const webinarFields = [ description: `Time zone used in the response. The default is the time zone of the calendar.`, }, { - displayName: 'Webinar topic', + displayName: 'Webinar Topic', name: 'topic', type: 'string', default: '', description: `Webinar topic.`, }, { - displayName: 'Webinar type', + displayName: 'Webinar Type', name: 'type', type: 'options', options: [ @@ -258,7 +258,7 @@ export const webinarFields = [ value: 6, }, { - name: 'Recurring webinar with fixed time', + name: 'Recurring webinar with fixed time', value: 9, }, ], @@ -290,7 +290,7 @@ export const webinarFields = [ description: 'Webinar ID.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -456,7 +456,7 @@ export const webinarFields = [ description: 'User ID or email address of user.', }, { - displayName: 'Additional fields', + displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', @@ -488,20 +488,20 @@ export const webinarFields = [ description: 'Alternative hosts email IDs.', }, { - displayName: 'Approval type', + displayName: 'Approval Type', name: 'approval_type', type: 'options', options: [ { - name: 'Automatically approve', + name: 'Automatically Approve', value: 0, }, { - name: 'Manually approve', + name: 'Manually Approve', value: 1, }, { - name: 'No registration required', + name: 'No Registration Required', value: 2, }, ], @@ -509,16 +509,16 @@ export const webinarFields = [ description: 'Approval type.', }, { - displayName: 'Auto recording', + displayName: 'Auto Recording', name: 'auto_recording', type: 'options', options: [ { - name: 'Record on local', + name: 'Record on Local', value: 'local', }, { - name: 'Record on cloud', + name: 'Record on Cloud', value: 'cloud', }, { @@ -594,7 +594,7 @@ export const webinarFields = [ description: 'Enable Practice session.', }, { - displayName: 'Registration type', + displayName: 'Registration Type', name: 'registration_type', type: 'options', options: [ @@ -615,7 +615,7 @@ export const webinarFields = [ description: 'Registration type. Used for recurring webinars with fixed time only.', }, { - displayName: 'Start time', + displayName: 'Start Time', name: 'startTime', type: 'dateTime', default: '', @@ -632,14 +632,14 @@ export const webinarFields = [ description: `Time zone used in the response. The default is the time zone of the calendar.`, }, { - displayName: 'Webinar topic', + displayName: 'Webinar Topic', name: 'topic', type: 'string', default: '', description: `Webinar topic.`, }, { - displayName: 'Webinar type', + displayName: 'Webinar Type', name: 'type', type: 'options', options: [ @@ -652,7 +652,7 @@ export const webinarFields = [ value: 6, }, { - name: 'Recurring webinar with fixed time', + name: 'Recurring webinar with fixed time', value: 9, }, ], diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index f33a2a338e..c0d1a05a9e 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -483,7 +483,7 @@ export class Zoom implements INodeType { } } - if (resource === 'meetingRegistrant') { + if (resource === 'meetingRegistrants') { if (operation === 'create') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantcreate const meetingId = this.getNodeParameter('meetingId', i) as string; From 582954008352add0c2b88b5a2094872ae846c34a Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Wed, 24 Jun 2020 11:09:11 -0700 Subject: [PATCH 12/44] fix registrants bug --- .../nodes/Zoom/MeetingDescription.ts | 46 +++++----- .../Zoom/MeetingRegistrantDescription.ts | 10 +-- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 87 ++++++++++--------- 3 files changed, 73 insertions(+), 70 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index d335901bdf..3efcc2a2bc 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -66,7 +66,7 @@ export const meetingFields = [ ], }, }, - description: 'User ID or email address of user.', + description: 'User ID or Email Address.', }, { displayName: 'Additional Fields', @@ -95,22 +95,22 @@ export const meetingFields = [ }, { displayName: 'Alternative Hosts', - name: 'alternative_hosts', + name: 'alternativeHosts', type: 'string', default: '', description: 'Alternative hosts email IDs.', }, { displayName: 'Auto Recording', - name: 'auto_recording', + name: 'autoRecording', type: 'options', options: [ { - name: 'Record on local', + name: 'Record on Local', value: 'local', }, { - name: 'Record on cloud', + name: 'Record on Cloud', value: 'cloud', }, { @@ -152,28 +152,28 @@ export const meetingFields = [ }, { displayName: 'Host Meeting in China', - name: 'cn_meeting', + name: 'cnMeeting', type: 'boolean', default: false, description: 'Host Meeting in China.', }, { displayName: 'Host Meeting in India', - name: 'in_meeting', + name: 'inMeeting', type: 'boolean', default: false, description: 'Host Meeting in India.', }, { displayName: 'Host Video', - name: 'host_video', + name: 'hostVideo', type: 'boolean', default: false, description: 'Start video when host joins the meeting.', }, { - displayName: 'Join before Host', - name: 'join_before_host', + displayName: 'Join Before Host', + name: 'joinBeforeHost', type: 'boolean', default: false, description: 'Allow participants to join the meeting before host starts it.', @@ -213,14 +213,14 @@ export const meetingFields = [ }, { displayName: 'Muting before entry', - name: 'mute_upon_entry', + name: 'muteUponEntry', type: 'boolean', default: false, description: 'Mute participants upon entry.', }, { displayName: 'Participant Video', - name: 'participant_video', + name: 'participantVideo', type: 'boolean', default: false, description: 'Start video when participant joins the meeting.', @@ -234,7 +234,7 @@ export const meetingFields = [ }, { displayName: 'Registration Type', - name: 'registration_type', + name: 'registrationType', type: 'options', options: [ { @@ -546,14 +546,14 @@ export const meetingFields = [ }, { displayName: 'Alternative Hosts', - name: 'alternative_hosts', + name: 'alternativeHosts', type: 'string', default: '', description: 'Alternative hosts email IDs.', }, { displayName: 'Audio', - name: 'auto_recording', + name: 'audio', type: 'options', options: [ { @@ -574,7 +574,7 @@ export const meetingFields = [ }, { displayName: 'Auto Recording', - name: 'auto_recording', + name: 'autoRecording', type: 'options', options: [ { @@ -603,28 +603,28 @@ export const meetingFields = [ }, { displayName: 'Join Before Host', - name: 'join_before_host', + name: 'joinBeforeHost', type: 'boolean', default: false, description: 'Allow participants to join the meeting before host starts it.', }, { displayName: 'Host Meeting in China', - name: 'cn_meeting', + name: 'cnMeeting', type: 'boolean', default: false, description: 'Host Meeting in China.', }, { displayName: 'Host Meeting in India', - name: 'in_meeting', + name: 'inMeeting', type: 'boolean', default: false, description: 'Host Meeting in India.', }, { displayName: 'Host Video', - name: 'host_video', + name: 'hostVideo', type: 'boolean', default: false, description: 'Start video when host joins the meeting.', @@ -671,7 +671,7 @@ export const meetingFields = [ }, { displayName: 'Muting Before Entry', - name: 'mute_upon_entry', + name: 'muteUponEntry', type: 'boolean', default: false, description: 'Mute participants upon entry.', @@ -685,14 +685,14 @@ export const meetingFields = [ }, { displayName: 'Participant Video', - name: 'participant_video', + name: 'participantVideo', type: 'boolean', default: false, description: 'Start video when participant joins the meeting.', }, { displayName: 'Registration Type', - name: 'registration_type', + name: 'registrationType', type: 'options', options: [ { diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index d1527f165a..5d40aae014 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -74,7 +74,7 @@ export const meetingRegistrantFields = [ ], }, }, - description: 'Valid email-id of registrant.', + description: 'Valid Email-ID.', }, { displayName: 'First Name', @@ -142,7 +142,7 @@ export const meetingRegistrantFields = [ }, { displayName: 'Job Title', - name: 'job_title', + name: 'jobTitle', type: 'string', default: '', description: 'Job title of registrant.', @@ -177,7 +177,7 @@ export const meetingRegistrantFields = [ }, { displayName: 'Purchasing Time Frame', - name: 'purchasing_time_frame', + name: 'purchasingTimeFrame', type: 'options', options: [ { @@ -206,7 +206,7 @@ export const meetingRegistrantFields = [ }, { displayName: 'Role in Purchase Process', - name: 'role_in_purchase_process', + name: 'roleInPurchaseProcess', type: 'options', options: [ { @@ -329,7 +329,7 @@ export const meetingRegistrantFields = [ options: [ { displayName: 'Occurrence ID', - name: 'occurrence_id', + name: 'occurrenceId', type: 'string', default: '', description: `Occurrence ID.`, diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index c0d1a05a9e..43221e8288 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -255,23 +255,23 @@ export class Zoom implements INodeType { i ) as IDataObject; const settings: Settings = {}; - if (additionalFields.cn_meeting) { - settings.cn_meeting = additionalFields.cn_meeting as boolean; + if (additionalFields.cnMeeting) { + settings.cn_meeting = additionalFields.cnMeeting as boolean; } - if (additionalFields.in_meeting) { - settings.in_meeting = additionalFields.in_meeting as boolean; + if (additionalFields.inMeeting) { + settings.in_meeting = additionalFields.inMeeting as boolean; } - if (additionalFields.join_before_host) { - settings.join_before_host = additionalFields.join_before_host as boolean; + if (additionalFields.joinBeforeHost) { + settings.join_before_host = additionalFields.joinBeforeHost as boolean; } - if (additionalFields.mute_upon_entry) { - settings.mute_upon_entry = additionalFields.mute_upon_entry as boolean; + if (additionalFields.muteUponEntry) { + settings.mute_upon_entry = additionalFields.muteUponEntry as boolean; } @@ -285,28 +285,28 @@ export class Zoom implements INodeType { } - if (additionalFields.alternative_hosts) { - settings.alternative_hosts = additionalFields.alternative_hosts as string; + if (additionalFields.alternativeHosts) { + settings.alternative_hosts = additionalFields.alternativeHosts as string; } - if (additionalFields.participant_video) { - settings.participant_video = additionalFields.participant_video as boolean; + if (additionalFields.participantVideo) { + settings.participant_video = additionalFields.participantVideo as boolean; } - if (additionalFields.host_video) { - settings.host_video = additionalFields.host_video as boolean; + if (additionalFields.hostVideo) { + settings.host_video = additionalFields.hostVideo as boolean; } - if (additionalFields.auto_recording) { - settings.auto_recording = additionalFields.auto_recording as string; + if (additionalFields.autoRecording) { + settings.auto_recording = additionalFields.autoRecording as string; } - if (additionalFields.registration_type) { - settings.registration_type = additionalFields.registration_type as number; + if (additionalFields.registrationType) { + settings.registration_type = additionalFields.registrationType as number; } @@ -374,23 +374,23 @@ export class Zoom implements INodeType { qs.occurrence_id = additionalFields.occurrenceId as string; } - if (additionalFields.cn_meeting) { - settings.cn_meeting = additionalFields.cn_meeting as boolean; + if (additionalFields.cnMeeting) { + settings.cn_meeting = additionalFields.cnMeeting as boolean; } - if (additionalFields.in_meeting) { - settings.in_meeting = additionalFields.in_meeting as boolean; + if (additionalFields.inMeeting) { + settings.in_meeting = additionalFields.inMeeting as boolean; } - if (additionalFields.join_before_host) { - settings.join_before_host = additionalFields.join_before_host as boolean; + if (additionalFields.joinBeforeHost) { + settings.join_before_host = additionalFields.joinBeforeHost as boolean; } - if (additionalFields.mute_upon_entry) { - settings.mute_upon_entry = additionalFields.mute_upon_entry as boolean; + if (additionalFields.muteUponEntry) { + settings.mute_upon_entry = additionalFields.muteUponEntry as boolean; } @@ -404,28 +404,28 @@ export class Zoom implements INodeType { } - if (additionalFields.alternative_hosts) { - settings.alternative_hosts = additionalFields.alternative_hosts as string; + if (additionalFields.alternativeHosts) { + settings.alternative_hosts = additionalFields.alternativeHosts as string; } - if (additionalFields.participant_video) { - settings.participant_video = additionalFields.participant_video as boolean; + if (additionalFields.participantVideo) { + settings.participant_video = additionalFields.participantVideo as boolean; } - if (additionalFields.host_video) { - settings.host_video = additionalFields.host_video as boolean; + if (additionalFields.hostVideo) { + settings.host_video = additionalFields.hostVideo as boolean; } - if (additionalFields.auto_recording) { - settings.auto_recording = additionalFields.auto_recording as string; + if (additionalFields.autoRecording) { + settings.auto_recording = additionalFields.autoRecording as string; } - if (additionalFields.registration_type) { - settings.registration_type = additionalFields.registration_type as number; + if (additionalFields.registrationType) { + settings.registration_type = additionalFields.registrationType as number; } @@ -525,14 +525,14 @@ export class Zoom implements INodeType { if (additionalFields.org) { body.org = additionalFields.org as string; } - if (additionalFields.job_title) { - body.job_title = additionalFields.job_title as string; + if (additionalFields.jobTitle) { + body.job_title = additionalFields.jobTitle as string; } - if (additionalFields.purchasing_time_frame) { - body.purchasing_time_frame = additionalFields.purchasing_time_frame as string; + if (additionalFields.purchasingTimeFrame) { + body.purchasing_time_frame = additionalFields.purchasingTimeFrame as string; } - if (additionalFields.role_in_purchase_process) { - body.role_in_purchase_process = additionalFields.role_in_purchase_process as string; + if (additionalFields.roleInPurchaseProcess) { + body.role_in_purchase_process = additionalFields.roleInPurchaseProcess as string; } responseData = await zoomApiRequest.call( this, @@ -575,6 +575,9 @@ export class Zoom implements INodeType { if (additionalFields.occurenceId) { qs.occurrence_id = additionalFields.occurrenceId as string; } + if (additionalFields.action) { + body.action = additionalFields.action as string; + } responseData = await zoomApiRequest.call( this, 'PUT', From 7fea380af557151b0419032d04f3336e06d2429b Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Wed, 24 Jun 2020 11:18:56 -0700 Subject: [PATCH 13/44] fix webinar resource --- .../nodes/Zoom/WebinarDescription.ts | 20 ++++++------- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 30 +++++++++++-------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts index 1c0e03e096..eff6f4159c 100644 --- a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -66,7 +66,7 @@ export const webinarFields = [ ], }, }, - description: 'User ID or email address of user.', + description: 'User ID or email ID.', }, { displayName: 'Additional Fields', @@ -95,14 +95,14 @@ export const webinarFields = [ }, { displayName: 'Alternative Hosts', - name: 'alternative_hosts', + name: 'alternativeHosts', type: 'string', default: '', description: 'Alternative hosts email IDs.', }, { displayName: 'Approval Type', - name: 'approval_type', + name: 'approvalType', type: 'options', options: [ { @@ -145,7 +145,7 @@ export const webinarFields = [ }, { displayName: 'Auto Recording', - name: 'auto_recording', + name: 'autoRecording', type: 'options', options: [ { @@ -173,14 +173,14 @@ export const webinarFields = [ }, { displayName: 'Host Video', - name: 'host_video', + name: 'hostVideo', type: 'boolean', default: false, description: 'Start video when host joins the webinar.', }, { displayName: 'Panelists Video', - name: 'panelists_video', + name: 'panelistsVideo', type: 'boolean', default: false, description: 'Start video when panelists joins the webinar.', @@ -194,14 +194,14 @@ export const webinarFields = [ }, { displayName: 'Practice Session', - name: 'practice_session', + name: 'practiceSession', type: 'boolean', default: false, description: 'Enable Practice session.', }, { displayName: 'Registration Type', - name: 'registration_type', + name: 'registrationType', type: 'options', options: [ { @@ -438,8 +438,8 @@ export const webinarFields = [ /* webinar:update */ /* -------------------------------------------------------------------------- */ { - displayName: 'User ID', - name: 'userId', + displayName: 'Webinar ID', + name: 'webinarId', type: 'string', default: '', required: true, diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 43221e8288..60f186b31a 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -603,30 +603,34 @@ export class Zoom implements INodeType { } - if (additionalFields.alternative_hosts) { - settings.alternative_hosts = additionalFields.alternative_hosts as string; + if (additionalFields.alternativeHosts) { + settings.alternative_hosts = additionalFields.alternativeHosts as string; } - if (additionalFields.panelists_video) { - settings.panelists_video = additionalFields.panelists_video as boolean; + if (additionalFields.panelistsVideo) { + settings.panelists_video = additionalFields.panelistsVideo as boolean; } - if (additionalFields.practice_session) { - settings.practice_session = additionalFields.practice_session as boolean; + if (additionalFields.hostVideo) { + settings.host_video = additionalFields.hostVideo as boolean; } - if (additionalFields.auto_recording) { - settings.auto_recording = additionalFields.auto_recording as string; + if (additionalFields.practiceSession) { + settings.practice_session = additionalFields.practiceSession as boolean; + + } + if (additionalFields.autoRecording) { + settings.auto_recording = additionalFields.autoRecording as string; } - if (additionalFields.registration_type) { - settings.registration_type = additionalFields.registration_type as number; + if (additionalFields.registrationType) { + settings.registration_type = additionalFields.registrationType as number; } - if (additionalFields.approval_type) { - settings.approval_type = additionalFields.approval_type as number; + if (additionalFields.approvalType) { + settings.approval_type = additionalFields.approvalType as number; } @@ -823,7 +827,7 @@ export class Zoom implements INodeType { responseData = await zoomApiRequest.call( this, 'PATCH', - `/users/${webinarId}/webinars`, + `webinars/${webinarId}`, body, qs ); From f9fe9de235184f13d2871356a830173f8787083b Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Wed, 24 Jun 2020 11:25:07 -0700 Subject: [PATCH 14/44] fix webinar naming conventions --- .../nodes/Zoom/WebinarDescription.ts | 16 +++++----- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 32 +++++++++++-------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts index eff6f4159c..421eb2cf50 100644 --- a/packages/nodes-base/nodes/Zoom/WebinarDescription.ts +++ b/packages/nodes-base/nodes/Zoom/WebinarDescription.ts @@ -482,14 +482,14 @@ export const webinarFields = [ }, { displayName: 'Alternative Hosts', - name: 'alternative_hosts', + name: 'alternativeHosts', type: 'string', default: '', description: 'Alternative hosts email IDs.', }, { displayName: 'Approval Type', - name: 'approval_type', + name: 'approvalType', type: 'options', options: [ { @@ -510,7 +510,7 @@ export const webinarFields = [ }, { displayName: 'Auto Recording', - name: 'auto_recording', + name: 'autoRecording', type: 'options', options: [ { @@ -560,14 +560,14 @@ export const webinarFields = [ }, { displayName: 'Host Video', - name: 'host_video', + name: 'hostVideo', type: 'boolean', default: false, description: 'Start video when host joins the webinar.', }, { displayName: 'Occurrence ID', - name: 'occurrence_id', + name: 'occurrenceId', type: 'string', default: '', description: `Webinar occurrence ID.`, @@ -581,21 +581,21 @@ export const webinarFields = [ }, { displayName: 'Panelists Video', - name: 'panelists_video', + name: 'panelistsVideo', type: 'boolean', default: false, description: 'Start video when panelists joins the webinar.', }, { displayName: 'Practice Session', - name: 'practice_session', + name: 'practiceSession', type: 'boolean', default: false, description: 'Enable Practice session.', }, { displayName: 'Registration Type', - name: 'registration_type', + name: 'registrationType', type: 'options', options: [ { diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 60f186b31a..f7e00b0de7 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -749,8 +749,8 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; - if (additionalFields.occurrence_id) { - qs.occurrence_id = additionalFields.occurrence_id as string; + if (additionalFields.occurrenceId) { + qs.occurrence_id = additionalFields.occurrenceId as string; } const settings: Settings = {}; @@ -758,30 +758,34 @@ export class Zoom implements INodeType { settings.audio = additionalFields.audio as string; } - if (additionalFields.alternative_hosts) { - settings.alternative_hosts = additionalFields.alternative_hosts as string; + if (additionalFields.alternativeHosts) { + settings.alternative_hosts = additionalFields.alternativeHosts as string; } - if (additionalFields.panelists_video) { - settings.panelists_video = additionalFields.panelists_video as boolean; + if (additionalFields.panelistsVideo) { + settings.panelists_video = additionalFields.panelistsVideo as boolean; } - if (additionalFields.practice_session) { - settings.practice_session = additionalFields.practice_session as boolean; + if (additionalFields.hostVideo) { + settings.host_video = additionalFields.hostVideo as boolean; } - if (additionalFields.auto_recording) { - settings.auto_recording = additionalFields.auto_recording as string; + if (additionalFields.practiceSession) { + settings.practice_session = additionalFields.practiceSession as boolean; + + } + if (additionalFields.autoRecording) { + settings.auto_recording = additionalFields.autoRecording as string; } - if (additionalFields.registration_type) { - settings.registration_type = additionalFields.registration_type as number; + if (additionalFields.registrationType) { + settings.registration_type = additionalFields.registrationType as number; } - if (additionalFields.approval_type) { - settings.approval_type = additionalFields.approval_type as number; + if (additionalFields.approvalType) { + settings.approval_type = additionalFields.approvalType as number; } From 34e05f0f72d1f9726ffd9b1e9c18840470bdee42 Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Wed, 24 Jun 2020 12:01:06 -0700 Subject: [PATCH 15/44] add credentials comments --- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index f7e00b0de7..e6e6a78600 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -65,6 +65,9 @@ export class Zoom implements INodeType { outputs: ['main'], credentials: [ { + // create a JWT app on Zoom Marketplace + //https://marketplace.zoom.us/develop/create + //get the JWT token as access token name: 'zoomApi', required: true, displayOptions: { @@ -76,6 +79,8 @@ export class Zoom implements INodeType { }, }, { + //create a account level OAuth app + //https://marketplace.zoom.us/develop/create name: 'zoomOAuth2Api', required: true, displayOptions: { From dee03a5e4c0de14911edcbeb899902774ce93ab1 Mon Sep 17 00:00:00 2001 From: shraddha shaligram Date: Wed, 24 Jun 2020 15:50:23 -0700 Subject: [PATCH 16/44] minor fix --- packages/nodes-base/nodes/Zoom/MeetingDescription.ts | 7 +++++++ packages/nodes-base/nodes/Zoom/Zoom.node.ts | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index 3efcc2a2bc..0df1f35b72 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -277,6 +277,13 @@ export const meetingFields = [ default: '', description: `Time zone used in the response. The default is the time zone of the calendar.`, }, + { + displayName: 'Waiting Room', + name: 'waitingRoom', + type: 'boolean', + default: false, + description: 'Enable waiting room.', + }, { displayName: 'Watermark', name: 'watermark', diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index e6e6a78600..52435e5ab9 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -41,6 +41,7 @@ interface Settings { join_before_host?: boolean; mute_upon_entry?: boolean; watermark?: boolean; + waiting_room?: boolean; audio?: string; alternative_hosts?: string; auto_recording?: string; @@ -278,6 +279,10 @@ export class Zoom implements INodeType { if (additionalFields.muteUponEntry) { settings.mute_upon_entry = additionalFields.muteUponEntry as boolean; + } + if (additionalFields.waitingRoom) { + settings.waiting_room = additionalFields.waitingRoom as boolean; + } if (additionalFields.watermark) { From 4555d6bfc9a18df485990968fb34c70de5b2242a Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 24 Jun 2020 19:28:08 -0400 Subject: [PATCH 17/44] :zap: Improvements to Zoom-Node --- .../nodes-base/nodes/Zoom/GenericFunctions.ts | 1 + .../nodes/Zoom/MeetingDescription.ts | 592 +++++---- .../Zoom/MeetingRegistrantDescription.ts | 31 +- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 1060 ++++++++--------- 4 files changed, 821 insertions(+), 863 deletions(-) diff --git a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts index 6a8593e3ab..95e07f09b9 100644 --- a/packages/nodes-base/nodes/Zoom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoom/GenericFunctions.ts @@ -33,6 +33,7 @@ export async function zoomApiRequest(this: IExecuteFunctions | IExecuteSingleFun if (Object.keys(query).length === 0) { delete options.qs; } + try { if (authenticationMethod === 'accessToken') { const credentials = this.getCredentials('zoomApi'); diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index 3efcc2a2bc..130830b9fe 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -43,31 +43,13 @@ export const meetingOperations = [ ], default: 'create', description: 'The operation to perform.', - } + }, ] as INodeProperties[]; export const meetingFields = [ /* -------------------------------------------------------------------------- */ - /* meeting:create */ + /* meeting:create */ /* -------------------------------------------------------------------------- */ - { - displayName: 'User ID', - name: 'userId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'create', - ], - resource: [ - 'meeting', - ], - }, - }, - description: 'User ID or Email Address.', - }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -90,98 +72,29 @@ export const meetingFields = [ displayName: 'Agenda', name: 'agenda', type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, default: '', description: 'Meeting agenda.', }, - { - displayName: 'Alternative Hosts', - name: 'alternativeHosts', - type: 'string', - default: '', - description: 'Alternative hosts email IDs.', - }, - { - displayName: 'Auto Recording', - name: 'autoRecording', - type: 'options', - options: [ - { - name: 'Record on Local', - value: 'local', - }, - { - name: 'Record on Cloud', - value: 'cloud', - }, - { - name: 'Disabled', - value: 'none', - }, - ], - default: 'none', - description: 'Auto recording.', - }, - { - displayName: 'Audio', - name: 'audio', - type: 'options', - options: [ - { - name: 'Both Telephony and VoiP', - value: 'both', - }, - { - name: 'Telephony', - value: 'telephony', - }, - { - name: 'VOIP', - value: 'voip', - }, - - ], - default: 'both', - description: 'Determine how participants can join audio portion of the meeting.', - }, { displayName: 'Duration', name: 'duration', type: 'number', - default: '', - description: 'Duration.', - }, - { - displayName: 'Host Meeting in China', - name: 'cnMeeting', - type: 'boolean', - default: false, - description: 'Host Meeting in China.', - }, - { - displayName: 'Host Meeting in India', - name: 'inMeeting', - type: 'boolean', - default: false, - description: 'Host Meeting in India.', - }, - { - displayName: 'Host Video', - name: 'hostVideo', - type: 'boolean', - default: false, - description: 'Start video when host joins the meeting.', - }, - { - displayName: 'Join Before Host', - name: 'joinBeforeHost', - type: 'boolean', - default: false, - description: 'Allow participants to join the meeting before host starts it.', + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'Meeting duration (minutes).', }, { displayName: 'Meeting Topic', name: 'topic', type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, default: '', description: `Meeting topic.`, }, @@ -211,20 +124,6 @@ export const meetingFields = [ default: 2, description: 'Meeting type.', }, - { - displayName: 'Muting before entry', - name: 'muteUponEntry', - type: 'boolean', - default: false, - description: 'Mute participants upon entry.', - }, - { - displayName: 'Participant Video', - name: 'participantVideo', - type: 'boolean', - default: false, - description: 'Start video when participant joins the meeting.', - }, { displayName: 'Password', name: 'password', @@ -232,27 +131,6 @@ export const meetingFields = [ default: '', description: 'Password to join the meeting with maximum 10 characters.', }, - { - displayName: 'Registration Type', - name: 'registrationType', - type: 'options', - options: [ - { - name: 'Attendees register once and can attend any of the occurences', - value: 1, - }, - { - name: 'Attendees need to register for every occurrence', - value: 2, - }, - { - name: 'Attendees register once and can choose one or more occurrences to attend', - value: 3, - }, - ], - default: 1, - description: 'Registration type. Used for recurring meetings with fixed time only', - }, { displayName: 'Schedule For', name: 'scheduleFor', @@ -260,6 +138,135 @@ export const meetingFields = [ default: '', description: 'Schedule meeting for someone else from your account, provide their email ID.', }, + { + displayName: 'Settings', + name: 'settings', + type: 'collection', + placeholder: 'Add Setting', + default: {}, + options: [ + { + displayName: 'Audio', + name: 'audio', + type: 'options', + options: [ + { + name: 'Both Telephony and VoiP', + value: 'both', + }, + { + name: 'Telephony', + value: 'telephony', + }, + { + name: 'VOIP', + value: 'voip', + }, + + ], + default: 'both', + description: 'Determine how participants can join audio portion of the meeting.', + }, + { + displayName: 'Alternative Hosts', + name: 'alternativeHosts', + type: 'string', + default: '', + description: 'Alternative hosts email IDs.', + }, + { + displayName: 'Auto Recording', + name: 'autoRecording', + type: 'options', + options: [ + { + name: 'Record on Local', + value: 'local', + }, + { + name: 'Record on Cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + ], + default: 'none', + description: 'Auto recording.', + }, + { + displayName: 'Host Meeting in China', + name: 'cnMeeting', + type: 'boolean', + default: false, + description: 'Host Meeting in China.', + }, + { + displayName: 'Host Meeting in India', + name: 'inMeeting', + type: 'boolean', + default: false, + description: 'Host Meeting in India.', + }, + { + displayName: 'Host Video', + name: 'hostVideo', + type: 'boolean', + default: false, + description: 'Start video when host joins the meeting.', + }, + { + displayName: 'Join Before Host', + name: 'joinBeforeHost', + type: 'boolean', + default: false, + description: 'Allow participants to join the meeting before host starts it.', + }, + { + displayName: 'Muting Upon Entry', + name: 'muteUponEntry', + type: 'boolean', + default: false, + description: 'Mute participants upon entry.', + }, + { + displayName: 'Participant Video', + name: 'participantVideo', + type: 'boolean', + default: false, + description: 'Start video when participant joins the meeting.', + }, + { + displayName: 'Registration Type', + name: 'registrationType', + type: 'options', + options: [ + { + name: 'Attendees register once and can attend any of the occurences', + value: 1, + }, + { + name: 'Attendees need to register for every occurrence', + value: 2, + }, + { + name: 'Attendees register once and can choose one or more occurrences to attend', + value: 3, + }, + ], + default: 1, + description: 'Registration type. Used for recurring meetings with fixed time only', + }, + { + displayName: 'Watermark', + name: 'watermark', + type: 'boolean', + default: false, + description: 'Adds watermark when viewing a shared screen.', + }, + ], + }, { displayName: 'Start Time', name: 'startTime', @@ -277,13 +284,6 @@ export const meetingFields = [ default: '', description: `Time zone used in the response. The default is the time zone of the calendar.`, }, - { - displayName: 'Watermark', - name: 'watermark', - type: 'boolean', - default: false, - description: 'Adds watermark when viewing a shared screen.', - }, ], }, /* -------------------------------------------------------------------------- */ @@ -342,26 +342,8 @@ export const meetingFields = [ ], }, /* -------------------------------------------------------------------------- */ - /* meeting:getAll */ + /* meeting:getAll */ /* -------------------------------------------------------------------------- */ - { - displayName: 'User ID', - name: 'userId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'getAll', - ], - resource: [ - 'meeting', - ], - }, - }, - description: 'User ID or email-ID.', - }, { displayName: 'Return All', name: 'returnAll', @@ -404,10 +386,10 @@ export const meetingFields = [ description: 'How many results to return.', }, { - displayName: 'Additional Fields', - name: 'additionalFields', + displayName: 'Filters', + name: 'filters', type: 'collection', - placeholder: 'Add Field', + placeholder: 'Add Filter', default: {}, displayOptions: { show: { @@ -429,23 +411,26 @@ export const meetingFields = [ { name: 'Scheduled', value: 'scheduled', + description: 'This includes all valid past meetings, live meetings and upcoming scheduled meetings' }, { name: 'Live', value: 'live', + description: 'All ongoing meetings', }, { name: 'Upcoming', value: 'upcoming', + description: 'All upcoming meetings including live meetings', }, ], default: 'live', description: `Meeting type.`, }, - ] + ], }, /* -------------------------------------------------------------------------- */ - /* meeting:delete */ + /* meeting:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Meeting ID', @@ -500,7 +485,7 @@ export const meetingFields = [ }, /* -------------------------------------------------------------------------- */ - /* meeting:update */ + /* meeting:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Meeting ID', @@ -521,8 +506,8 @@ export const meetingFields = [ description: 'Meeting ID.', }, { - displayName: 'Additional Fields', - name: 'additionalFields', + displayName: 'Update Fields', + name: 'updateFields', type: 'collection', placeholder: 'Add Field', default: {}, @@ -541,100 +526,21 @@ export const meetingFields = [ displayName: 'Agenda', name: 'agenda', type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, default: '', description: 'Meeting agenda.', }, - { - displayName: 'Alternative Hosts', - name: 'alternativeHosts', - type: 'string', - default: '', - description: 'Alternative hosts email IDs.', - }, - { - displayName: 'Audio', - name: 'audio', - type: 'options', - options: [ - { - name: 'Both Telephony and VoiP', - value: 'both', - }, - { - name: 'Telephony', - value: 'telephony', - }, - { - name: 'VOIP', - value: 'voip', - }, - ], - default: 'both', - description: 'Determine how participants can join audio portion of the meeting.', - }, - { - displayName: 'Auto Recording', - name: 'autoRecording', - type: 'options', - options: [ - { - name: 'Record on Local', - value: 'local', - }, - { - name: 'Record on Cloud', - value: 'cloud', - }, - { - name: 'Disabled', - value: 'none', - }, - ], - default: 'none', - description: 'Auto recording.', - }, - { displayName: 'Duration', name: 'duration', type: 'number', - default: '', - description: 'Duration.', - }, - { - displayName: 'Join Before Host', - name: 'joinBeforeHost', - type: 'boolean', - default: false, - description: 'Allow participants to join the meeting before host starts it.', - }, - { - displayName: 'Host Meeting in China', - name: 'cnMeeting', - type: 'boolean', - default: false, - description: 'Host Meeting in China.', - }, - { - displayName: 'Host Meeting in India', - name: 'inMeeting', - type: 'boolean', - default: false, - description: 'Host Meeting in India.', - }, - { - displayName: 'Host Video', - name: 'hostVideo', - type: 'boolean', - default: false, - description: 'Start video when host joins the meeting.', - }, - { - displayName: 'Occurrence ID', - name: 'occurrenceId', - type: 'string', - default: '', - description: 'Occurrence ID.', + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'Meeting duration (minutes).', }, { displayName: 'Meeting Topic', @@ -669,13 +575,6 @@ export const meetingFields = [ default: 2, description: 'Meeting type.', }, - { - displayName: 'Muting Before Entry', - name: 'muteUponEntry', - type: 'boolean', - default: false, - description: 'Mute participants upon entry.', - }, { displayName: 'Password', name: 'password', @@ -683,34 +582,6 @@ export const meetingFields = [ default: '', description: 'Password to join the meeting with maximum 10 characters.', }, - { - displayName: 'Participant Video', - name: 'participantVideo', - type: 'boolean', - default: false, - description: 'Start video when participant joins the meeting.', - }, - { - displayName: 'Registration Type', - name: 'registrationType', - type: 'options', - options: [ - { - name: 'Attendees register once and can attend any of the occurrences', - value: 1, - }, - { - name: 'Attendees need to register for every occurrence', - value: 2, - }, - { - name: 'Attendees register once and can choose one or more occurrences to attend', - value: 3, - }, - ], - default: 1, - description: 'Registration type. Used for recurring meetings with fixed time only', - }, { displayName: 'Schedule For', name: 'scheduleFor', @@ -718,6 +589,135 @@ export const meetingFields = [ default: '', description: 'Schedule meeting for someone else from your account, provide their email ID.', }, + { + displayName: 'Settings', + name: 'settings', + type: 'collection', + placeholder: 'Add Setting', + default: {}, + options: [ + { + displayName: 'Audio', + name: 'audio', + type: 'options', + options: [ + { + name: 'Both Telephony and VoiP', + value: 'both', + }, + { + name: 'Telephony', + value: 'telephony', + }, + { + name: 'VOIP', + value: 'voip', + }, + + ], + default: 'both', + description: 'Determine how participants can join audio portion of the meeting.', + }, + { + displayName: 'Alternative Hosts', + name: 'alternativeHosts', + type: 'string', + default: '', + description: 'Alternative hosts email IDs.', + }, + { + displayName: 'Auto Recording', + name: 'autoRecording', + type: 'options', + options: [ + { + name: 'Record on Local', + value: 'local', + }, + { + name: 'Record on Cloud', + value: 'cloud', + }, + { + name: 'Disabled', + value: 'none', + }, + ], + default: 'none', + description: 'Auto recording.', + }, + { + displayName: 'Host Meeting in China', + name: 'cnMeeting', + type: 'boolean', + default: false, + description: 'Host Meeting in China.', + }, + { + displayName: 'Host Meeting in India', + name: 'inMeeting', + type: 'boolean', + default: false, + description: 'Host Meeting in India.', + }, + { + displayName: 'Host Video', + name: 'hostVideo', + type: 'boolean', + default: false, + description: 'Start video when host joins the meeting.', + }, + { + displayName: 'Join Before Host', + name: 'joinBeforeHost', + type: 'boolean', + default: false, + description: 'Allow participants to join the meeting before host starts it.', + }, + { + displayName: 'Muting Upon Entry', + name: 'muteUponEntry', + type: 'boolean', + default: false, + description: 'Mute participants upon entry.', + }, + { + displayName: 'Participant Video', + name: 'participantVideo', + type: 'boolean', + default: false, + description: 'Start video when participant joins the meeting.', + }, + { + displayName: 'Registration Type', + name: 'registrationType', + type: 'options', + options: [ + { + name: 'Attendees register once and can attend any of the occurences', + value: 1, + }, + { + name: 'Attendees need to register for every occurrence', + value: 2, + }, + { + name: 'Attendees register once and can choose one or more occurrences to attend', + value: 3, + }, + ], + default: 1, + description: 'Registration type. Used for recurring meetings with fixed time only', + }, + { + displayName: 'Watermark', + name: 'watermark', + type: 'boolean', + default: false, + description: 'Adds watermark when viewing a shared screen.', + }, + ], + }, { displayName: 'Start Time', name: 'startTime', @@ -735,16 +735,6 @@ export const meetingFields = [ default: '', description: `Time zone used in the response. The default is the time zone of the calendar.`, }, - { - displayName: 'Watermark', - name: 'watermark', - type: 'boolean', - default: false, - description: 'Adds watermark when viewing a shared screen.', - }, - - ], }, - ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts index 5d40aae014..5438f18fa3 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingRegistrantDescription.ts @@ -1,6 +1,7 @@ import { INodeProperties, } from 'n8n-workflow'; + export const meetingRegistrantOperations = [ { displayName: 'Operation', @@ -9,7 +10,7 @@ export const meetingRegistrantOperations = [ displayOptions: { show: { resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -38,7 +39,7 @@ export const meetingRegistrantOperations = [ export const meetingRegistrantFields = [ /* -------------------------------------------------------------------------- */ - /* meetingRegistrants:create */ + /* meetingRegistrant:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Meeting Id', @@ -52,7 +53,7 @@ export const meetingRegistrantFields = [ 'create', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -70,7 +71,7 @@ export const meetingRegistrantFields = [ 'create', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -88,7 +89,7 @@ export const meetingRegistrantFields = [ 'create', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -107,7 +108,7 @@ export const meetingRegistrantFields = [ ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -248,7 +249,7 @@ export const meetingRegistrantFields = [ ], }, /* -------------------------------------------------------------------------- */ - /* meetingRegistrants:getAll */ + /* meetingRegistrant:getAll */ /* -------------------------------------------------------------------------- */ { displayName: 'Meeting ID', @@ -262,7 +263,7 @@ export const meetingRegistrantFields = [ 'getAll', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -278,7 +279,7 @@ export const meetingRegistrantFields = [ 'getAll', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -295,7 +296,7 @@ export const meetingRegistrantFields = [ 'getAll', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], returnAll: [ false, @@ -322,7 +323,7 @@ export const meetingRegistrantFields = [ ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -359,7 +360,7 @@ export const meetingRegistrantFields = [ ] }, /* -------------------------------------------------------------------------- */ - /* meetingRegistrants:update */ + /* meetingRegistrant:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Meeting ID', @@ -373,7 +374,7 @@ export const meetingRegistrantFields = [ 'update', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -390,7 +391,7 @@ export const meetingRegistrantFields = [ 'update', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, @@ -423,7 +424,7 @@ export const meetingRegistrantFields = [ 'update', ], resource: [ - 'meetingRegistrants', + 'meetingRegistrant', ], }, }, diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index e6e6a78600..e3c9a083a5 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -1,14 +1,16 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, - INodeExecutionData, - INodeType, ILoadOptionsFunctions, - INodeTypeDescription, + INodeExecutionData, INodePropertyOptions, + INodeType, + INodeTypeDescription, } from 'n8n-workflow'; + import { zoomApiRequest, zoomApiRequestAllItems, @@ -19,16 +21,15 @@ import { meetingFields, } from './MeetingDescription'; -import { - meetingRegistrantOperations, - meetingRegistrantFields, +// import { +// meetingRegistrantOperations, +// meetingRegistrantFields, +// } from './MeetingRegistrantDescription'; -} from './MeetingRegistrantDescription'; - -import { - webinarOperations, - webinarFields, -} from './WebinarDescription'; +// import { +// webinarOperations, +// webinarFields, +// } from './WebinarDescription'; import * as moment from 'moment-timezone'; @@ -48,6 +49,7 @@ interface Settings { approval_type?: number; practice_session?: boolean; } + export class Zoom implements INodeType { description: INodeTypeDescription = { displayName: 'Zoom', @@ -119,14 +121,14 @@ export class Zoom implements INodeType { name: 'Meeting', value: 'meeting' }, - { - name: 'Meeting Registrant', - value: 'meetingRegistrants' - }, - { - name: 'Webinar', - value: 'webinar' - } + // { + // name: 'Meeting Registrant', + // value: 'meetingRegistrant' + // }, + // { + // name: 'Webinar', + // value: 'webinar' + // } ], default: 'meeting', description: 'The resource to operate on.' @@ -135,13 +137,13 @@ export class Zoom implements INodeType { ...meetingOperations, ...meetingFields, - //MEETING REGISTRANTS - ...meetingRegistrantOperations, - ...meetingRegistrantFields, + // //MEETING REGISTRANTS + // ...meetingRegistrantOperations, + // ...meetingRegistrantFields, - //WEBINARS - ...webinarOperations, - ...webinarFields, + // //WEBINARS + // ...webinarOperations, + // ...webinarFields, ] }; @@ -169,7 +171,6 @@ export class Zoom implements INodeType { const items = this.getInputData(); const returnData: IDataObject[] = []; let qs: IDataObject = {}; - let body: IDataObject = {}; let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; @@ -186,14 +187,13 @@ export class Zoom implements INodeType { 'additionalFields', i ) as IDataObject; + if (additionalFields.showPreviousOccurrences) { qs.show_previous_occurrences = additionalFields.showPreviousOccurrences as boolean; - } if (additionalFields.occurrenceId) { qs.occurrence_id = additionalFields.occurrenceId as string; - } responseData = await zoomApiRequest.call( @@ -206,23 +206,22 @@ export class Zoom implements INodeType { } if (operation === 'getAll') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetings - const userId = this.getNodeParameter('userId', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const additionalFields = this.getNodeParameter( - 'additionalFields', + const filters = this.getNodeParameter( + 'filters', i ) as IDataObject; - if (additionalFields.type) { - qs.type = additionalFields.type as string; - + if (filters.type) { + qs.type = filters.type as string; } + if (returnAll) { - responseData = await zoomApiRequestAllItems.call(this, 'meetings', 'GET', `/users/${userId}/meetings`, {}, qs); + responseData = await zoomApiRequestAllItems.call(this, 'meetings', 'GET', '/users/me/meetings', {}, qs); } else { qs.page_size = this.getNodeParameter('limit', i) as number; - responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/meetings`, {}, qs); - + responseData = await zoomApiRequest.call(this, 'GET', '/users/me/meetings', {}, qs); + responseData = responseData.meetings; } } @@ -235,12 +234,10 @@ export class Zoom implements INodeType { ) as IDataObject; if (additionalFields.scheduleForReminder) { qs.schedule_for_reminder = additionalFields.scheduleForReminder as boolean; - } if (additionalFields.occurrenceId) { qs.occurrence_id = additionalFields.occurrenceId; - } responseData = await zoomApiRequest.call( @@ -254,114 +251,100 @@ export class Zoom implements INodeType { } if (operation === 'create') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate - const userId = this.getNodeParameter('userId', i) as string; const additionalFields = this.getNodeParameter( 'additionalFields', i ) as IDataObject; - const settings: Settings = {}; - if (additionalFields.cnMeeting) { - settings.cn_meeting = additionalFields.cnMeeting as boolean; + const body: IDataObject = {}; + + if (additionalFields.settings) { + const settingValues: Settings = {}; + const settings = additionalFields.settings as IDataObject; + + if (settings.cnMeeting) { + settingValues.cn_meeting = settings.cnMeeting as boolean; + } + + if (settings.inMeeting) { + settingValues.in_meeting = settings.inMeeting as boolean; + } + + if (settings.joinBeforeHost) { + settingValues.join_before_host = settings.joinBeforeHost as boolean; + } + + if (settings.muteUponEntry) { + settingValues.mute_upon_entry = settings.muteUponEntry as boolean; + } + + if (settings.watermark) { + settingValues.watermark = settings.watermark as boolean; + } + + if (settings.audio) { + settingValues.audio = settings.audio as string; + } + + if (settings.alternativeHosts) { + settingValues.alternative_hosts = settings.alternativeHosts as string; + } + + if (settings.participantVideo) { + settingValues.participant_video = settings.participantVideo as boolean; + } + + if (settings.hostVideo) { + settingValues.host_video = settings.hostVideo as boolean; + } + + if (settings.autoRecording) { + settingValues.auto_recording = settings.autoRecording as string; + } + + if (settings.registrationType) { + settingValues.registration_type = settings.registrationType as number; + } + + body.settings = settingValues; } - if (additionalFields.inMeeting) { - settings.in_meeting = additionalFields.inMeeting as boolean; - - } - - if (additionalFields.joinBeforeHost) { - settings.join_before_host = additionalFields.joinBeforeHost as boolean; - - } - - if (additionalFields.muteUponEntry) { - settings.mute_upon_entry = additionalFields.muteUponEntry as boolean; - - } - - if (additionalFields.watermark) { - settings.watermark = additionalFields.watermark as boolean; - - } - - if (additionalFields.audio) { - settings.audio = additionalFields.audio as string; - - } - - if (additionalFields.alternativeHosts) { - settings.alternative_hosts = additionalFields.alternativeHosts as string; - - } - - if (additionalFields.participantVideo) { - settings.participant_video = additionalFields.participantVideo as boolean; - - } - - if (additionalFields.hostVideo) { - settings.host_video = additionalFields.hostVideo as boolean; - - } - - if (additionalFields.autoRecording) { - settings.auto_recording = additionalFields.autoRecording as string; - - } - - if (additionalFields.registrationType) { - settings.registration_type = additionalFields.registrationType as number; - - } - - body = { - settings, - }; - if (additionalFields.topic) { body.topic = additionalFields.topic as string; - } if (additionalFields.type) { body.type = additionalFields.type as string; - } if (additionalFields.startTime) { body.start_time = additionalFields.startTime as string; - } if (additionalFields.duration) { body.duration = additionalFields.duration as number; - } if (additionalFields.scheduleFor) { body.schedule_for = additionalFields.scheduleFor as string; - } if (additionalFields.timeZone) { body.timezone = additionalFields.timeZone as string; - } if (additionalFields.password) { body.password = additionalFields.password as string; - } if (additionalFields.agenda) { body.agenda = additionalFields.agenda as string; - } + responseData = await zoomApiRequest.call( this, 'POST', - `/users/${userId}/meetings`, + `/users/me/meetings`, body, qs ); @@ -369,112 +352,94 @@ export class Zoom implements INodeType { if (operation === 'update') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingupdate const meetingId = this.getNodeParameter('meetingId', i) as string; - const settings: Settings = {}; - const additionalFields = this.getNodeParameter( - 'additionalFields', + const updateFields = this.getNodeParameter( + 'updateFields', i ) as IDataObject; - if (additionalFields.occurrenceId) { - qs.occurrence_id = additionalFields.occurrenceId as string; + const body: IDataObject = {}; + + if (updateFields.settings) { + const settingValues: Settings = {}; + const settings = updateFields.settings as IDataObject; + + if (settings.cnMeeting) { + settingValues.cn_meeting = settings.cnMeeting as boolean; + } + + if (settings.inMeeting) { + settingValues.in_meeting = settings.inMeeting as boolean; + } + + if (settings.joinBeforeHost) { + settingValues.join_before_host = settings.joinBeforeHost as boolean; + } + + if (settings.muteUponEntry) { + settingValues.mute_upon_entry = settings.muteUponEntry as boolean; + } + + if (settings.watermark) { + settingValues.watermark = settings.watermark as boolean; + } + + if (settings.audio) { + settingValues.audio = settings.audio as string; + } + + if (settings.alternativeHosts) { + settingValues.alternative_hosts = settings.alternativeHosts as string; + } + + if (settings.participantVideo) { + settingValues.participant_video = settings.participantVideo as boolean; + } + + if (settings.hostVideo) { + settingValues.host_video = settings.hostVideo as boolean; + } + + if (settings.autoRecording) { + settingValues.auto_recording = settings.autoRecording as string; + } + + if (settings.registrationType) { + settingValues.registration_type = settings.registrationType as number; + } + + body.settings = settingValues; } - if (additionalFields.cnMeeting) { - settings.cn_meeting = additionalFields.cnMeeting as boolean; - + if (updateFields.topic) { + body.topic = updateFields.topic as string; } - if (additionalFields.inMeeting) { - settings.in_meeting = additionalFields.inMeeting as boolean; - + if (updateFields.type) { + body.type = updateFields.type as string; } - if (additionalFields.joinBeforeHost) { - settings.join_before_host = additionalFields.joinBeforeHost as boolean; - + if (updateFields.startTime) { + body.start_time = updateFields.startTime as string; } - if (additionalFields.muteUponEntry) { - settings.mute_upon_entry = additionalFields.muteUponEntry as boolean; - + if (updateFields.duration) { + body.duration = updateFields.duration as number; } - if (additionalFields.watermark) { - settings.watermark = additionalFields.watermark as boolean; - + if (updateFields.scheduleFor) { + body.schedule_for = updateFields.scheduleFor as string; } - if (additionalFields.audio) { - settings.audio = additionalFields.audio as string; - + if (updateFields.timeZone) { + body.timezone = updateFields.timeZone as string; } - if (additionalFields.alternativeHosts) { - settings.alternative_hosts = additionalFields.alternativeHosts as string; - + if (updateFields.password) { + body.password = updateFields.password as string; } - if (additionalFields.participantVideo) { - settings.participant_video = additionalFields.participantVideo as boolean; - - } - - if (additionalFields.hostVideo) { - settings.host_video = additionalFields.hostVideo as boolean; - - } - - if (additionalFields.autoRecording) { - settings.auto_recording = additionalFields.autoRecording as string; - - } - - if (additionalFields.registrationType) { - settings.registration_type = additionalFields.registrationType as number; - - } - - body = { - settings, - }; - if (additionalFields.topic) { - body.topic = additionalFields.topic as string; - - } - - if (additionalFields.type) { - body.type = additionalFields.type as string; - - } - - if (additionalFields.startTime) { - body.start_time = additionalFields.startTime as string; - - } - - if (additionalFields.duration) { - body.duration = additionalFields.duration as number; - - } - - if (additionalFields.scheduleFor) { - body.schedule_for = additionalFields.scheduleFor as string; - - } - - if (additionalFields.timeZone) { - body.timezone = additionalFields.timeZone as string; - - } - - if (additionalFields.password) { - body.password = additionalFields.password as string; - - } - - if (additionalFields.agenda) { - body.agenda = additionalFields.agenda as string; - + if (updateFields.agenda) { + body.agenda = updateFields.agenda as string; } responseData = await zoomApiRequest.call( @@ -484,364 +449,365 @@ export class Zoom implements INodeType { body, qs ); - responseData = { updated: true }; - } - } - if (resource === 'meetingRegistrants') { - if (operation === 'create') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantcreate - const meetingId = this.getNodeParameter('meetingId', i) as string; - const emailId = this.getNodeParameter('email', i) as string; - body.email = emailId; - const firstName = this.getNodeParameter('firstName', i) as string; - body.first_name = firstName; - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; - if (additionalFields.occurrenceId) { - qs.occurrence_ids = additionalFields.occurrenceId as string; - } - if (additionalFields.lastName) { - body.last_name = additionalFields.lastName as string; - } - if (additionalFields.address) { - body.address = additionalFields.address as string; - } - if (additionalFields.city) { - body.city = additionalFields.city as string; - } - if (additionalFields.state) { - body.state = additionalFields.state as string; - } - if (additionalFields.country) { - body.country = additionalFields.country as string; - } - if (additionalFields.zip) { - body.zip = additionalFields.zip as string; - } - if (additionalFields.phone) { - body.phone = additionalFields.phone as string; - } - if (additionalFields.comments) { - body.comments = additionalFields.comments as string; - } - if (additionalFields.org) { - body.org = additionalFields.org as string; - } - if (additionalFields.jobTitle) { - body.job_title = additionalFields.jobTitle as string; - } - if (additionalFields.purchasingTimeFrame) { - body.purchasing_time_frame = additionalFields.purchasingTimeFrame as string; - } - if (additionalFields.roleInPurchaseProcess) { - body.role_in_purchase_process = additionalFields.roleInPurchaseProcess as string; - } - responseData = await zoomApiRequest.call( - this, - 'POST', - `/meetings/${meetingId}/registrants`, - body, - qs - ); - } - if (operation === 'getAll') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrants - const meetingId = this.getNodeParameter('meetingId', i) as string; - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; - if (additionalFields.occurrenceId) { - qs.occurrence_id = additionalFields.occurrenceId as string; - } - if (additionalFields.status) { - qs.status = additionalFields.status as string; - } - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - if (returnAll) { - responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/meetings/${meetingId}/registrants`, {}, qs); - } else { - qs.page_size = this.getNodeParameter('limit', i) as number; - responseData = await zoomApiRequest.call(this, 'GET', `/meetings/${meetingId}/registrants`, {}, qs); - - } - - } - if (operation === 'update') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantstatus - const meetingId = this.getNodeParameter('meetingId', i) as string; - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; - if (additionalFields.occurenceId) { - qs.occurrence_id = additionalFields.occurrenceId as string; - } - if (additionalFields.action) { - body.action = additionalFields.action as string; - } - responseData = await zoomApiRequest.call( - this, - 'PUT', - `/meetings/${meetingId}/registrants/status`, - body, - qs - ); - } - } - if (resource === 'webinar') { - if (operation === 'create') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarcreate - const userId = this.getNodeParameter('userId', i) as string; - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; - const settings: Settings = {}; - - - if (additionalFields.audio) { - settings.audio = additionalFields.audio as string; - - } - - if (additionalFields.alternativeHosts) { - settings.alternative_hosts = additionalFields.alternativeHosts as string; - - } - - if (additionalFields.panelistsVideo) { - settings.panelists_video = additionalFields.panelistsVideo as boolean; - - } - if (additionalFields.hostVideo) { - settings.host_video = additionalFields.hostVideo as boolean; - - } - if (additionalFields.practiceSession) { - settings.practice_session = additionalFields.practiceSession as boolean; - - } - if (additionalFields.autoRecording) { - settings.auto_recording = additionalFields.autoRecording as string; - - } - - if (additionalFields.registrationType) { - settings.registration_type = additionalFields.registrationType as number; - - } - if (additionalFields.approvalType) { - settings.approval_type = additionalFields.approvalType as number; - - } - - body = { - settings, - }; - - if (additionalFields.topic) { - body.topic = additionalFields.topic as string; - - } - - if (additionalFields.type) { - body.type = additionalFields.type as string; - - } - - if (additionalFields.startTime) { - body.start_time = additionalFields.startTime as string; - - } - - if (additionalFields.duration) { - body.duration = additionalFields.duration as number; - - } - - - if (additionalFields.timeZone) { - body.timezone = additionalFields.timeZone as string; - - } - - if (additionalFields.password) { - body.password = additionalFields.password as string; - - } - - if (additionalFields.agenda) { - body.agenda = additionalFields.agenda as string; - - } - responseData = await zoomApiRequest.call( - this, - 'POST', - `/users/${userId}/webinars`, - body, - qs - ); - } - if (operation === 'get') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinar - const webinarId = this.getNodeParameter('webinarId', i) as string; - - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; - if (additionalFields.showPreviousOccurrences) { - qs.show_previous_occurrences = additionalFields.showPreviousOccurrences as boolean; - - } - - if (additionalFields.occurrenceId) { - qs.occurrence_id = additionalFields.occurrenceId as string; - - } - - responseData = await zoomApiRequest.call( - this, - 'GET', - `/webinars/${webinarId}`, - {}, - qs - ); - } - if (operation === 'getAll') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinars - const userId = this.getNodeParameter('userId', i) as string; - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - if (returnAll) { - responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/users/${userId}/webinars`, {}, qs); - } else { - qs.page_size = this.getNodeParameter('limit', i) as number; - responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/webinars`, {}, qs); - - } - } - if (operation === 'delete') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinardelete - const webinarId = this.getNodeParameter('webinarId', i) as string; - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; - - - if (additionalFields.occurrenceId) { - qs.occurrence_id = additionalFields.occurrenceId; - - } - - responseData = await zoomApiRequest.call( - this, - 'DELETE', - `/webinars/${webinarId}`, - {}, - qs - ); responseData = { success: true }; - } - if (operation === 'update') { - //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarupdate - const webinarId = this.getNodeParameter('webinarId', i) as string; - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; - if (additionalFields.occurrenceId) { - qs.occurrence_id = additionalFields.occurrenceId as string; - } - const settings: Settings = {}; - if (additionalFields.audio) { - settings.audio = additionalFields.audio as string; - - } - if (additionalFields.alternativeHosts) { - settings.alternative_hosts = additionalFields.alternativeHosts as string; - - } - - if (additionalFields.panelistsVideo) { - settings.panelists_video = additionalFields.panelistsVideo as boolean; - - } - if (additionalFields.hostVideo) { - settings.host_video = additionalFields.hostVideo as boolean; - - } - if (additionalFields.practiceSession) { - settings.practice_session = additionalFields.practiceSession as boolean; - - } - if (additionalFields.autoRecording) { - settings.auto_recording = additionalFields.autoRecording as string; - - } - - if (additionalFields.registrationType) { - settings.registration_type = additionalFields.registrationType as number; - - } - if (additionalFields.approvalType) { - settings.approval_type = additionalFields.approvalType as number; - - } - - body = { - settings, - }; - - if (additionalFields.topic) { - body.topic = additionalFields.topic as string; - - } - - if (additionalFields.type) { - body.type = additionalFields.type as string; - - } - - if (additionalFields.startTime) { - body.start_time = additionalFields.startTime as string; - - } - - if (additionalFields.duration) { - body.duration = additionalFields.duration as number; - - } - - - if (additionalFields.timeZone) { - body.timezone = additionalFields.timeZone as string; - - } - - if (additionalFields.password) { - body.password = additionalFields.password as string; - - } - - if (additionalFields.agenda) { - body.agenda = additionalFields.agenda as string; - - } - responseData = await zoomApiRequest.call( - this, - 'PATCH', - `webinars/${webinarId}`, - body, - qs - ); } } + // if (resource === 'meetingRegistrant') { + // if (operation === 'create') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantcreate + // const meetingId = this.getNodeParameter('meetingId', i) as string; + // const emailId = this.getNodeParameter('email', i) as string; + // body.email = emailId; + // const firstName = this.getNodeParameter('firstName', i) as string; + // body.first_name = firstName; + // const additionalFields = this.getNodeParameter( + // 'additionalFields', + // i + // ) as IDataObject; + // if (additionalFields.occurrenceId) { + // qs.occurrence_ids = additionalFields.occurrenceId as string; + // } + // if (additionalFields.lastName) { + // body.last_name = additionalFields.lastName as string; + // } + // if (additionalFields.address) { + // body.address = additionalFields.address as string; + // } + // if (additionalFields.city) { + // body.city = additionalFields.city as string; + // } + // if (additionalFields.state) { + // body.state = additionalFields.state as string; + // } + // if (additionalFields.country) { + // body.country = additionalFields.country as string; + // } + // if (additionalFields.zip) { + // body.zip = additionalFields.zip as string; + // } + // if (additionalFields.phone) { + // body.phone = additionalFields.phone as string; + // } + // if (additionalFields.comments) { + // body.comments = additionalFields.comments as string; + // } + // if (additionalFields.org) { + // body.org = additionalFields.org as string; + // } + // if (additionalFields.jobTitle) { + // body.job_title = additionalFields.jobTitle as string; + // } + // if (additionalFields.purchasingTimeFrame) { + // body.purchasing_time_frame = additionalFields.purchasingTimeFrame as string; + // } + // if (additionalFields.roleInPurchaseProcess) { + // body.role_in_purchase_process = additionalFields.roleInPurchaseProcess as string; + // } + // responseData = await zoomApiRequest.call( + // this, + // 'POST', + // `/meetings/${meetingId}/registrants`, + // body, + // qs + // ); + // } + // if (operation === 'getAll') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrants + // const meetingId = this.getNodeParameter('meetingId', i) as string; + // const additionalFields = this.getNodeParameter( + // 'additionalFields', + // i + // ) as IDataObject; + // if (additionalFields.occurrenceId) { + // qs.occurrence_id = additionalFields.occurrenceId as string; + // } + // if (additionalFields.status) { + // qs.status = additionalFields.status as string; + // } + // const returnAll = this.getNodeParameter('returnAll', i) as boolean; + // if (returnAll) { + // responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/meetings/${meetingId}/registrants`, {}, qs); + // } else { + // qs.page_size = this.getNodeParameter('limit', i) as number; + // responseData = await zoomApiRequest.call(this, 'GET', `/meetings/${meetingId}/registrants`, {}, qs); + + // } + + // } + // if (operation === 'update') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantstatus + // const meetingId = this.getNodeParameter('meetingId', i) as string; + // const additionalFields = this.getNodeParameter( + // 'additionalFields', + // i + // ) as IDataObject; + // if (additionalFields.occurenceId) { + // qs.occurrence_id = additionalFields.occurrenceId as string; + // } + // if (additionalFields.action) { + // body.action = additionalFields.action as string; + // } + // responseData = await zoomApiRequest.call( + // this, + // 'PUT', + // `/meetings/${meetingId}/registrants/status`, + // body, + // qs + // ); + // } + // } + // if (resource === 'webinar') { + // if (operation === 'create') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarcreate + // const userId = this.getNodeParameter('userId', i) as string; + // const additionalFields = this.getNodeParameter( + // 'additionalFields', + // i + // ) as IDataObject; + // const settings: Settings = {}; + + + // if (additionalFields.audio) { + // settings.audio = additionalFields.audio as string; + + // } + + // if (additionalFields.alternativeHosts) { + // settings.alternative_hosts = additionalFields.alternativeHosts as string; + + // } + + // if (additionalFields.panelistsVideo) { + // settings.panelists_video = additionalFields.panelistsVideo as boolean; + + // } + // if (additionalFields.hostVideo) { + // settings.host_video = additionalFields.hostVideo as boolean; + + // } + // if (additionalFields.practiceSession) { + // settings.practice_session = additionalFields.practiceSession as boolean; + + // } + // if (additionalFields.autoRecording) { + // settings.auto_recording = additionalFields.autoRecording as string; + + // } + + // if (additionalFields.registrationType) { + // settings.registration_type = additionalFields.registrationType as number; + + // } + // if (additionalFields.approvalType) { + // settings.approval_type = additionalFields.approvalType as number; + + // } + + // body = { + // settings, + // }; + + // if (additionalFields.topic) { + // body.topic = additionalFields.topic as string; + + // } + + // if (additionalFields.type) { + // body.type = additionalFields.type as string; + + // } + + // if (additionalFields.startTime) { + // body.start_time = additionalFields.startTime as string; + + // } + + // if (additionalFields.duration) { + // body.duration = additionalFields.duration as number; + + // } + + + // if (additionalFields.timeZone) { + // body.timezone = additionalFields.timeZone as string; + + // } + + // if (additionalFields.password) { + // body.password = additionalFields.password as string; + + // } + + // if (additionalFields.agenda) { + // body.agenda = additionalFields.agenda as string; + + // } + // responseData = await zoomApiRequest.call( + // this, + // 'POST', + // `/users/${userId}/webinars`, + // body, + // qs + // ); + // } + // if (operation === 'get') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinar + // const webinarId = this.getNodeParameter('webinarId', i) as string; + + // const additionalFields = this.getNodeParameter( + // 'additionalFields', + // i + // ) as IDataObject; + // if (additionalFields.showPreviousOccurrences) { + // qs.show_previous_occurrences = additionalFields.showPreviousOccurrences as boolean; + + // } + + // if (additionalFields.occurrenceId) { + // qs.occurrence_id = additionalFields.occurrenceId as string; + + // } + + // responseData = await zoomApiRequest.call( + // this, + // 'GET', + // `/webinars/${webinarId}`, + // {}, + // qs + // ); + // } + // if (operation === 'getAll') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinars + // const userId = this.getNodeParameter('userId', i) as string; + // const returnAll = this.getNodeParameter('returnAll', i) as boolean; + // if (returnAll) { + // responseData = await zoomApiRequestAllItems.call(this, 'results', 'GET', `/users/${userId}/webinars`, {}, qs); + // } else { + // qs.page_size = this.getNodeParameter('limit', i) as number; + // responseData = await zoomApiRequest.call(this, 'GET', `/users/${userId}/webinars`, {}, qs); + + // } + // } + // if (operation === 'delete') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinardelete + // const webinarId = this.getNodeParameter('webinarId', i) as string; + // const additionalFields = this.getNodeParameter( + // 'additionalFields', + // i + // ) as IDataObject; + + + // if (additionalFields.occurrenceId) { + // qs.occurrence_id = additionalFields.occurrenceId; + + // } + + // responseData = await zoomApiRequest.call( + // this, + // 'DELETE', + // `/webinars/${webinarId}`, + // {}, + // qs + // ); + // responseData = { success: true }; + // } + // if (operation === 'update') { + // //https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarupdate + // const webinarId = this.getNodeParameter('webinarId', i) as string; + // const additionalFields = this.getNodeParameter( + // 'additionalFields', + // i + // ) as IDataObject; + // if (additionalFields.occurrenceId) { + // qs.occurrence_id = additionalFields.occurrenceId as string; + + // } + // const settings: Settings = {}; + // if (additionalFields.audio) { + // settings.audio = additionalFields.audio as string; + + // } + // if (additionalFields.alternativeHosts) { + // settings.alternative_hosts = additionalFields.alternativeHosts as string; + + // } + + // if (additionalFields.panelistsVideo) { + // settings.panelists_video = additionalFields.panelistsVideo as boolean; + + // } + // if (additionalFields.hostVideo) { + // settings.host_video = additionalFields.hostVideo as boolean; + + // } + // if (additionalFields.practiceSession) { + // settings.practice_session = additionalFields.practiceSession as boolean; + + // } + // if (additionalFields.autoRecording) { + // settings.auto_recording = additionalFields.autoRecording as string; + + // } + + // if (additionalFields.registrationType) { + // settings.registration_type = additionalFields.registrationType as number; + + // } + // if (additionalFields.approvalType) { + // settings.approval_type = additionalFields.approvalType as number; + + // } + + // body = { + // settings, + // }; + + // if (additionalFields.topic) { + // body.topic = additionalFields.topic as string; + + // } + + // if (additionalFields.type) { + // body.type = additionalFields.type as string; + + // } + + // if (additionalFields.startTime) { + // body.start_time = additionalFields.startTime as string; + + // } + + // if (additionalFields.duration) { + // body.duration = additionalFields.duration as number; + + // } + + + // if (additionalFields.timeZone) { + // body.timezone = additionalFields.timeZone as string; + + // } + + // if (additionalFields.password) { + // body.password = additionalFields.password as string; + + // } + + // if (additionalFields.agenda) { + // body.agenda = additionalFields.agenda as string; + + // } + // responseData = await zoomApiRequest.call( + // this, + // 'PATCH', + // `webinars/${webinarId}`, + // body, + // qs + // ); + // } + // } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); From 591630abd16c6a18e803071ca85650aaf800e9d2 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 26 Jun 2020 09:27:41 -0400 Subject: [PATCH 18/44] :bug: Fix timezone issue --- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 2395f8a318..7f735d33b8 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -319,7 +319,12 @@ export class Zoom implements INodeType { } if (additionalFields.startTime) { - body.start_time = additionalFields.startTime as string; + if (additionalFields.timeZone) { + body.start_time = moment(additionalFields.startTime as string).format('YYYY-MM-DDTHH:mm:ss'); + } else { + // if none timezone it's defined used n8n timezone + body.start_time = moment.tz(additionalFields.startTime as string, this.getTimezone()).format(); + } } if (additionalFields.duration) { @@ -342,6 +347,8 @@ export class Zoom implements INodeType { body.agenda = additionalFields.agenda as string; } + console.log(body); + responseData = await zoomApiRequest.call( this, 'POST', From 3660f535ac4ced653f7257c8c2f9f363f17ba064 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 26 Jun 2020 17:48:07 -0400 Subject: [PATCH 19/44] :zap: Add .svg to gulpfile --- packages/nodes-base/gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/gulpfile.js b/packages/nodes-base/gulpfile.js index 9771a4017c..58ba6ec51a 100644 --- a/packages/nodes-base/gulpfile.js +++ b/packages/nodes-base/gulpfile.js @@ -1,7 +1,7 @@ const { src, dest } = require('gulp'); function copyIcons() { - return src('nodes/**/*.png') + return src('nodes/**/*.{png,svg}') .pipe(dest('dist/nodes')); } From 95068aa132526a3469275b6541c191fd2ea51e1b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 30 Jun 2020 16:55:17 +0200 Subject: [PATCH 20/44] :whale: Expose port in dockerfile --- docker/images/n8n-custom/Dockerfile | 2 ++ docker/images/n8n-ubuntu/Dockerfile | 2 ++ docker/images/n8n/Dockerfile | 2 ++ 3 files changed, 6 insertions(+) diff --git a/docker/images/n8n-custom/Dockerfile b/docker/images/n8n-custom/Dockerfile index f4e4af4ed7..d12f8f6b08 100644 --- a/docker/images/n8n-custom/Dockerfile +++ b/docker/images/n8n-custom/Dockerfile @@ -42,3 +42,5 @@ COPY --from=builder /data ./ COPY docker/images/n8n-custom/docker-entrypoint.sh /docker-entrypoint.sh ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] + +EXPOSE 5678/tcp diff --git a/docker/images/n8n-ubuntu/Dockerfile b/docker/images/n8n-ubuntu/Dockerfile index 200506f058..94935f0602 100644 --- a/docker/images/n8n-ubuntu/Dockerfile +++ b/docker/images/n8n-ubuntu/Dockerfile @@ -19,3 +19,5 @@ WORKDIR /data COPY docker-entrypoint.sh /docker-entrypoint.sh ENTRYPOINT ["/docker-entrypoint.sh"] + +EXPOSE 5678/tcp diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index c0997dcabd..af8f29cc5c 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -22,3 +22,5 @@ WORKDIR /data COPY docker-entrypoint.sh /docker-entrypoint.sh ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] + +EXPOSE 5678/tcp From 574167bf3d40cd5a74597ffa73cee59c248eb09d Mon Sep 17 00:00:00 2001 From: Erin McNulty Date: Tue, 30 Jun 2020 11:59:58 -0400 Subject: [PATCH 21/44] :bug: Changed reponseMode to responseMode (#711) --- packages/node-dev/templates/webhook/simple.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-dev/templates/webhook/simple.ts b/packages/node-dev/templates/webhook/simple.ts index eaf1521e84..ab81ca51d2 100644 --- a/packages/node-dev/templates/webhook/simple.ts +++ b/packages/node-dev/templates/webhook/simple.ts @@ -27,7 +27,7 @@ export class ClassNameReplace implements INodeType { { name: 'default', httpMethod: 'POST', - reponseMode: 'onReceived', + responseMode: 'onReceived', // Each webhook property can either be hardcoded // like the above ones or referenced from a parameter // like the "path" property bellow From bdd63fd54d7a8ffcdf961450751fc6719ddfdd16 Mon Sep 17 00:00:00 2001 From: Sven Schmidt <0xsven@gmail.com> Date: Tue, 30 Jun 2020 18:02:26 +0200 Subject: [PATCH 22/44] =?UTF-8?q?=F0=9F=93=9A=20Fix=20typo=20in=20create-n?= =?UTF-8?q?ode=20docs=20(#693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/create-node.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/create-node.md b/docs/create-node.md index aa19393983..183c11c734 100644 --- a/docs/create-node.md +++ b/docs/create-node.md @@ -135,7 +135,7 @@ When a node can perform multiple operations like edit and delete some kind of en Some nodes may need a lot of options. Add only the very important ones to the top level and for all others, create an "Options" parameter where they can be added if needed. This ensures that the interface stays clean and does not unnecessarily confuse people. A good example of that would be the XML node. -### Follow exiting parameter naming guideline +### Follow existing parameter naming guideline There is not much of a guideline yet but if your node can do multiple things, call the parameter which sets the behavior either "mode" (like "Merge" and "XML" node) or "operation" like the most other ones. If these operations can be done on different resources (like "User" or "Order) create a "resource" parameter (like "Pipedrive" and "Trello" node) From 872bc9df75427ffe742cfc169729dfc99210e195 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 30 Jun 2020 20:08:52 +0200 Subject: [PATCH 23/44] :bug: Write env encryption key to config if file does not exist #713 --- packages/core/src/UserSettings.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/UserSettings.ts b/packages/core/src/UserSettings.ts index 211341ed25..fb38fc779c 100644 --- a/packages/core/src/UserSettings.ts +++ b/packages/core/src/UserSettings.ts @@ -41,8 +41,13 @@ export async function prepareUserSettings(): Promise { userSettings = {}; } - // Settings and/or key do not exist. So generate a new encryption key - userSettings.encryptionKey = randomBytes(24).toString('base64'); + if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) { + // Use the encryption key which got set via environment + userSettings.encryptionKey = process.env[ENCRYPTION_KEY_ENV_OVERWRITE]; + } else { + // Generate a new encryption key + userSettings.encryptionKey = randomBytes(24).toString('base64'); + } console.log(`UserSettings got generated and saved to: ${settingsPath}`); From fc4ebfedca4071bfd6f57d8db774bb9a3131e89c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 30 Jun 2020 20:30:14 +0200 Subject: [PATCH 24/44] :bug: Fix bug that fromFormat field did not get used in pre-check #712 --- packages/nodes-base/nodes/DateTime.node.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/DateTime.node.ts b/packages/nodes-base/nodes/DateTime.node.ts index 953375e8d9..15c33dcffc 100644 --- a/packages/nodes-base/nodes/DateTime.node.ts +++ b/packages/nodes-base/nodes/DateTime.node.ts @@ -243,9 +243,10 @@ export class DateTime implements INodeType { if (currentDate === undefined) { continue; } - if (!moment(currentDate as string | number).isValid()) { + if (options.fromFormat === undefined && !moment(currentDate as string | number).isValid()) { throw new Error('The date input format could not be recognized. Please set the "From Format" field'); } + if (Number.isInteger(currentDate as unknown as number)) { newDate = moment.unix(currentDate as unknown as number); } else { From d72a7119570025df299acec221bb50314ce71ace Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 1 Jul 2020 15:08:00 +0200 Subject: [PATCH 25/44] :bug: Fix credential issue with ExecuteWorkflow-Node --- packages/cli/src/WorkflowExecuteAdditionalData.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 7a61bfd657..ed20a985f8 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -316,14 +316,14 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi // Does not get used so set it simply to empty string const executionId = ''; - // Create new additionalData to have different workflow loaded and to call - // different webooks - const additionalDataIntegrated = await getBase(additionalData.credentials); - additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode }); - // Get the needed credentials for the current workflow as they will differ to the ones of the // calling workflow. - additionalDataIntegrated.credentials = await WorkflowCredentials(workflowData!.nodes); + const credentials = await WorkflowCredentials(workflowData!.nodes); + + // Create new additionalData to have different workflow loaded and to call + // different webooks + const additionalDataIntegrated = await getBase(credentials); + additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode }); // Find Start-Node const requiredNodeTypes = ['n8n-nodes-base.start']; From 7de478d502f75f67b458bf350d3a2e795eb98515 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 2 Jul 2020 07:18:08 +0200 Subject: [PATCH 26/44] :bookmark: Release n8n-core@0.37.0 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 598ae14b23..6dc56f7bea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.36.0", + "version": "0.37.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 8c19b2fbaed69ef262948834a92761b8b657e503 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 2 Jul 2020 07:19:21 +0200 Subject: [PATCH 27/44] :arrow_up: Set n8n-core@0.37.0 on n8n-nodes-base --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c19d8e2349..94689feaf3 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -348,7 +348,7 @@ "moment-timezone": "^0.5.28", "mongodb": "^3.5.5", "mysql2": "^2.0.1", - "n8n-core": "~0.36.0", + "n8n-core": "~0.37.0", "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg-promise": "^9.0.3", From 6720e7593b1046f2a9ef37d4073938b35a12c122 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 2 Jul 2020 07:19:57 +0200 Subject: [PATCH 28/44] :bookmark: Release n8n-nodes-base@0.67.0 --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 94689feaf3..86980d1efa 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.66.0", + "version": "0.67.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 69e95730049f31d93f41bf671d3777fd66cdfd6e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 2 Jul 2020 07:21:15 +0200 Subject: [PATCH 29/44] :arrow_up: Set n8n-core@0.37.0 and n8n-nodes-base@0.67.0 on n8n --- packages/cli/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 9e23c91021..6516faa21d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -100,9 +100,9 @@ "lodash.get": "^4.4.2", "mongodb": "^3.5.5", "mysql2": "^2.0.1", - "n8n-core": "~0.36.0", + "n8n-core": "~0.37.0", "n8n-editor-ui": "~0.48.0", - "n8n-nodes-base": "~0.66.0", + "n8n-nodes-base": "~0.67.0", "n8n-workflow": "~0.33.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", From a01a764874908dd979fa7041333b80d3814ac3e7 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 2 Jul 2020 07:21:56 +0200 Subject: [PATCH 30/44] :bookmark: Release n8n@0.72.0 --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 6516faa21d..4eaaade97f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.71.0", + "version": "0.72.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 549b26fa3d0766647a4b44061e238f429f7e6312 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 2 Jul 2020 15:07:55 +0200 Subject: [PATCH 31/44] :bug: Fix issue with nodes in stack which do actually not get executed --- packages/core/src/WorkflowExecute.ts | 8 +++++++- packages/workflow/src/Workflow.ts | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 1e8b8898a7..c36ffe997e 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -593,9 +593,15 @@ export class WorkflowExecute { } } - this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name; nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode); + if (nodeSuccessData === undefined) { + // Node did not get executed + nodeSuccessData = null; + } else { + this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name; + } + if (nodeSuccessData === null || nodeSuccessData[0][0] === undefined) { if (executionData.node.alwaysOutputData === true) { nodeSuccessData = nodeSuccessData || []; diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 9a7d3ef1eb..5fbd780524 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -1085,18 +1085,18 @@ export class Workflow { * @returns {(Promise)} * @memberof Workflow */ - async runNode(node: INode, inputData: ITaskDataConnections, runExecutionData: IRunExecutionData, runIndex: number, additionalData: IWorkflowExecuteAdditionalData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode): Promise { + async runNode(node: INode, inputData: ITaskDataConnections, runExecutionData: IRunExecutionData, runIndex: number, additionalData: IWorkflowExecuteAdditionalData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode): Promise { if (node.disabled === true) { // If node is disabled simply pass the data through // return NodeRunHelpers. if (inputData.hasOwnProperty('main') && inputData.main.length > 0) { // If the node is disabled simply return the data from the first main input if (inputData.main[0] === null) { - return null; + return undefined; } return [(inputData.main[0] as INodeExecutionData[])]; } - return null; + return undefined; } const nodeType = this.nodeTypes.getByName(node.type); @@ -1112,7 +1112,7 @@ export class Workflow { if (connectionInputData.length === 0) { // No data for node so return - return null; + return undefined; } if (runExecutionData.resultData.lastNodeExecuted === node.name && runExecutionData.resultData.error !== undefined) { From 63280b74078f891d6e7793486ee753db77011a48 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 2 Jul 2020 23:07:28 +0200 Subject: [PATCH 32/44] :bug: Fix build issue --- packages/core/package.json | 2 +- packages/core/src/WorkflowExecute.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 6dc56f7bea..7a4b0c7349 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -45,7 +45,7 @@ "crypto-js": "3.1.9-1", "lodash.get": "^4.4.2", "mmmagic": "^0.5.2", - "n8n-workflow": "~0.32.0", + "n8n-workflow": "~0.33.0", "p-cancelable": "^2.0.0", "request": "^2.88.2", "request-promise-native": "^1.0.7" diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index c36ffe997e..c30be39ca1 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -459,7 +459,7 @@ export class WorkflowExecute { let executionData: IExecuteData; let executionError: IExecutionError | undefined; let executionNode: INode; - let nodeSuccessData: INodeExecutionData[][] | null; + let nodeSuccessData: INodeExecutionData[][] | null | undefined; let runIndex: number; let startTime: number; let taskData: ITaskData; From ae902589b87f80c3eaf43e5e4b885144d270c8e8 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 3 Jul 2020 07:31:08 +0200 Subject: [PATCH 33/44] :zap: Temporary fix to repair build --- packages/nodes-base/nodes/Redis/Redis.node.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index 68b7401205..12263b1d64 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -402,6 +402,7 @@ export class Redis implements INodeType { } else if (type === 'hash') { const clientHset = util.promisify(client.hset).bind(client); for (const key of Object.keys(value)) { + // @ts-ignore await clientHset(keyName, key, (value as IDataObject)[key]!.toString()); } } else if (type === 'list') { From 1f04c9eaac5c8bcd364e2df60860d4487b3901d2 Mon Sep 17 00:00:00 2001 From: einSelbst Date: Fri, 3 Jul 2020 15:35:58 +0200 Subject: [PATCH 34/44] Fix a few typos in node-dev readme I hope I didn't took it to far :) --- packages/node-dev/README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/node-dev/README.md b/packages/node-dev/README.md index 1c3d8df52f..fa817c9124 100644 --- a/packages/node-dev/README.md +++ b/packages/node-dev/README.md @@ -127,7 +127,7 @@ export class MyNode implements INodeType { The "description" property has to be set on all nodes because it contains all the base information. Additionally do all nodes have to have exactly one of the -following methods defined which contains the the actual logic: +following methods defined which contains the actual logic: **Regular node** @@ -138,8 +138,8 @@ Method get called when the workflow gets executed By default always `execute` should be used especially when creating a third-party integration. The reason for that is that it is way more flexible and allows to, for example, return a different amount of items than it received -as input. This is very important when a node should query data like return -all users. In that case, does the node normally just receive one input-item +as input. This is very important when a node should query data like *return +all users*. In that case, does the node normally just receive one input-item but returns as many as users exist. So in doubt always `execute` should be used! @@ -188,10 +188,10 @@ The following properties can be set in the node description: - **outputs** [required]: Types of outputs the node has (currently only "main" exists) and the amount - **outputNames** [optional]: In case a node has multiple outputs names can be set that users know what data to expect - **maxNodes** [optional]: If not an unlimited amount of nodes of that type can exist in a workflow the max-amount can be specified - - **name** [required]: Nme of the node (for n8n to use internally in camelCase) + - **name** [required]: Name of the node (for n8n to use internally, in camelCase) - **properties** [required]: Properties which get displayed in the Editor UI and can be set by the user - **subtitle** [optional]: Text which should be displayed underneath the name of the node in the Editor UI (can be an expression) - - **version** [required]: Version of the node. Currently always "1" (integer). For future usage does not get used yet. + - **version** [required]: Version of the node. Currently always "1" (integer). For future usage, does not get used yet. - **webhooks** [optional]: Webhooks the node should listen to @@ -200,12 +200,12 @@ The following properties can be set in the node description: The following properties can be set in the node properties: - **default** [required]: Default value of the property - - **description** [required]: Description to display users in Editor UI - - **displayName** [required]: Name to display users in Editor UI + - **description** [required]: Description that is displayed to users in the Editor UI + - **displayName** [required]: Name that is displayed to users in the Editor UI - **displayOptions** [optional]: Defines logic to decide if a property should be displayed or not - - **name** [required]: Name of the property (for n8n to use internally in camelCase) + - **name** [required]: Name of the property (for n8n to use internally, in camelCase) - **options** [optional]: The options the user can select when type of property is "collection", "fixedCollection" or "options" - - **placeholder** [optional]: Placeholder text to display users in Editor UI + - **placeholder** [optional]: Placeholder text that is displayed to users in the Editor UI - **type** [required]: Type of the property. If it is for example a "string", "number", ... - **typeOptions** [optional]: Additional options for type. Like for example the min or max value of a number - **required** [optional]: Defines if the value has to be set or if it can stay empty @@ -215,11 +215,11 @@ The following properties can be set in the node properties: The following properties can be set in the node property options. -All properties are optional. The most, however, work only work when the node-property is of a specfic type. +All properties are optional. However, most only work when the node-property is of a specfic type. - - **alwaysOpenEditWindow** [type: string]: If set then the "Editor Window" will always open when the user tries to edit the field. Is helpful when long texts normally get used in the property + - **alwaysOpenEditWindow** [type: string]: If set then the "Editor Window" will always open when the user tries to edit the field. Helpful if long text is typically used in the property. - **loadOptionsMethod** [type: options]: Method to use to load options from an external service - - **maxValue** [type: number]: Maximal value of the number + - **maxValue** [type: number]: Maximum value of the number - **minValue** [type: number]: Minimum value of the number - **multipleValues** [type: all]: If set the property gets turned into an Array and the user can add multiple values - **multipleValueButtonText** [type: all]: Custom text for add button in case "multipleValues" got set From fe56c8778d69cc13442000b6ebbeefa117a2133b Mon Sep 17 00:00:00 2001 From: Innokenty Lebedev Date: Sun, 5 Jul 2020 12:19:13 +0300 Subject: [PATCH 35/44] :bug: Fix slack as_user (#708) * Ignore as_user only when it is false * Update as_user description --- .../nodes/Slack/MessageDescription.ts | 22 ++++++++++++++++++- packages/nodes-base/nodes/Slack/Slack.node.ts | 5 +---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index 8d91663155..6619c51812 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -91,7 +91,7 @@ export const messageFields = [ ], }, }, - description: 'Post the message as authenticated user instead of bot.', + description: 'Post the message as authenticated user instead of bot. Works only with user token.', }, { displayName: 'User Name', @@ -486,6 +486,26 @@ export const messageFields = [ }, description: `Timestamp of the message to be updated.`, }, + { + displayName: 'As User', + name: 'as_user', + type: 'boolean', + default: false, + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + operation: [ + 'update' + ], + resource: [ + 'message', + ], + }, + }, + description: 'Pass true to update the message as the authed user. Works only with user token.', + }, { displayName: 'Update Fields', name: 'updateFields', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index befe931fb5..d6df14ab41 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -452,12 +452,9 @@ export class Slack implements INodeType { } if (body.as_user === false) { body.username = this.getNodeParameter('username', i) as string; + delete body.as_user; } - // ignore body.as_user as it's deprecated - - delete body.as_user; - if (!jsonParameters) { const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[]; From 9c58ca8f77eb5217b60517c179dffb94d56bc275 Mon Sep 17 00:00:00 2001 From: Shraddha Shaligram Date: Sun, 5 Jul 2020 02:33:05 -0700 Subject: [PATCH 36/44] :zap: Move Google Task title to top level (#718) --- .../nodes/Google/Task/GoogleTasks.node.ts | 8 ++------ .../nodes/Google/Task/TaskDescription.ts | 15 ++++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts index 8e2f06dcf5..603bc1af5e 100644 --- a/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts +++ b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts @@ -1,6 +1,6 @@ import { IExecuteFunctions, - } from 'n8n-core'; +} from 'n8n-core'; import { IDataObject, @@ -102,6 +102,7 @@ export class GoogleTasks implements INodeType { body = {}; //https://developers.google.com/tasks/v1/reference/tasks/insert const taskId = this.getNodeParameter('task', i) as string; + body.title = this.getNodeParameter('title', i) as string; const additionalFields = this.getNodeParameter( 'additionalFields', i @@ -121,11 +122,6 @@ export class GoogleTasks implements INodeType { if (additionalFields.notes) { body.notes = additionalFields.notes as string; } - - if (additionalFields.title) { - body.title = additionalFields.title as string; - } - if (additionalFields.dueDate) { body.dueDate = additionalFields.dueDate as string; } diff --git a/packages/nodes-base/nodes/Google/Task/TaskDescription.ts b/packages/nodes-base/nodes/Google/Task/TaskDescription.ts index 8030f0a7f3..3300572f2c 100644 --- a/packages/nodes-base/nodes/Google/Task/TaskDescription.ts +++ b/packages/nodes-base/nodes/Google/Task/TaskDescription.ts @@ -70,6 +70,13 @@ export const taskFields = [ }, default: '', }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the task.', + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -146,13 +153,7 @@ export const taskFields = [ default: '', description: 'Current status of the task.', }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'Title of the task.', - }, + ], }, /* -------------------------------------------------------------------------- */ From 62612d0ad4a62a0d143bea9d9cd467d68a92dacd Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 5 Jul 2020 13:03:50 +0200 Subject: [PATCH 37/44] :books: Removed docs-folders as they moved to n8n-io/n8n-docs --- docs/.nojekyll | 0 docs/CNAME | 1 - docs/README.md | 10 -- docs/_sidebar.md | 43 ------ docs/configuration.md | 244 ------------------------------- docs/create-node.md | 145 ------------------- docs/data-structure.md | 39 ----- docs/database.md | 109 -------------- docs/development.md | 3 - docs/docker.md | 7 - docs/faq.md | 47 ------ docs/images/n8n-logo.png | Bin 2675 -> 0 bytes docs/images/n8n-screenshot.png | Bin 129698 -> 0 bytes docs/index.html | 53 ------- docs/key-components.md | 25 ---- docs/keyboard-shortcuts.md | 28 ---- docs/license.md | 5 - docs/node-basics.md | 76 ---------- docs/nodes.md | 247 -------------------------------- docs/quick-start.md | 43 ------ docs/security.md | 13 -- docs/sensitive-data.md | 18 --- docs/server-setup.md | 183 ----------------------- docs/setup.md | 35 ----- docs/start-workflows-via-cli.md | 15 -- docs/test.md | 3 - docs/troubleshooting.md | 58 -------- docs/tutorials.md | 26 ---- docs/workflow.md | 111 -------------- 29 files changed, 1587 deletions(-) delete mode 100644 docs/.nojekyll delete mode 100644 docs/CNAME delete mode 100644 docs/README.md delete mode 100644 docs/_sidebar.md delete mode 100644 docs/configuration.md delete mode 100644 docs/create-node.md delete mode 100644 docs/data-structure.md delete mode 100644 docs/database.md delete mode 100644 docs/development.md delete mode 100644 docs/docker.md delete mode 100644 docs/faq.md delete mode 100644 docs/images/n8n-logo.png delete mode 100644 docs/images/n8n-screenshot.png delete mode 100644 docs/index.html delete mode 100644 docs/key-components.md delete mode 100644 docs/keyboard-shortcuts.md delete mode 100644 docs/license.md delete mode 100644 docs/node-basics.md delete mode 100644 docs/nodes.md delete mode 100644 docs/quick-start.md delete mode 100644 docs/security.md delete mode 100644 docs/sensitive-data.md delete mode 100644 docs/server-setup.md delete mode 100644 docs/setup.md delete mode 100644 docs/start-workflows-via-cli.md delete mode 100644 docs/test.md delete mode 100644 docs/troubleshooting.md delete mode 100644 docs/tutorials.md delete mode 100644 docs/workflow.md diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 22a8459481..0000000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -docs-old.n8n.io diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 3454cc15e2..0000000000 --- a/docs/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# n8n Documentation - -This is the documentation of n8n, a free and open [fair-code](http://faircode.io) licensed node-based Workflow Automation Tool. - -It covers everything from setup to usage and development. It is still a work in progress and all contributions are welcome. - - -## What is n8n? - -n8n (pronounced nodemation) helps you to interconnect every app with an API in the world with each other to share and manipulate its data without a single line of code. It is an easy to use, user-friendly and highly customizable service, which uses an intuitive user interface for you to design your unique workflows very fast. Hosted on your server and not based in the cloud, it keeps your sensible data very secure in your own trusted database. diff --git a/docs/_sidebar.md b/docs/_sidebar.md deleted file mode 100644 index 6cbe725286..0000000000 --- a/docs/_sidebar.md +++ /dev/null @@ -1,43 +0,0 @@ -- Home - - - [Welcome](/) - -- Getting started - - - [Key Components](key-components.md) - - [Quick Start](quick-start.md) - - [Setup](setup.md) - - [Tutorials](tutorials.md) - - [Docker](docker.md) - -- Advanced - - - [Configuration](configuration.md) - - [Data Structure](data-structure.md) - - [Database](database.md) - - [Keyboard Shortcuts](keyboard-shortcuts.md) - - [Node Basics](node-basics.md) - - [Nodes](nodes.md) - - [Security](security.md) - - [Sensitive Data](sensitive-data.md) - - [Server Setup](server-setup.md) - - [Start Workflows via CLI](start-workflows-via-cli.md) - - [Workflow](workflow.md) - -- Development - - - [Create Node](create-node.md) - - [Development](development.md) - - -- Other - - - [FAQ](faq.md) - - [License](license.md) - - [Troubleshooting](troubleshooting.md) - - -- Links - - - [![Jobs](https://n8n.io/favicon.ico ':size=16')Jobs](https://jobs.n8n.io) - - [![Website](https://n8n.io/favicon.ico ':size=16')n8n.io](https://n8n.io) diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index 63b8c95f12..0000000000 --- a/docs/configuration.md +++ /dev/null @@ -1,244 +0,0 @@ - - -# Configuration - -It is possible to change some of the n8n defaults via special environment variables. -The ones that currently exist are: - - -## Publish - -Sets how n8n should be made available. - -```bash -# The port n8n should be made available on -N8N_PORT=5678 - -# The IP address n8n should listen on -N8N_LISTEN_ADDRESS=0.0.0.0 - -# This ones are currently only important for the webhook URL creation. -# So if "WEBHOOK_TUNNEL_URL" got set they do get ignored. It is however -# encouraged to set them correctly anyway in case they will become -# important in the future. -N8N_PROTOCOL=https -N8N_HOST=n8n.example.com -``` - - -## Base URL - -Tells the frontend how to reach the REST API of the backend. - -```bash -export VUE_APP_URL_BASE_API="https://n8n.example.com/" -``` - - -## Execution Data Manual Runs - -n8n creates a random encryption key automatically on the first launch and saves -it in the `~/.n8n` folder. That key is used to encrypt the credentials before -they get saved to the database. It is also possible to overwrite that key and -set it via an environment variable. - -```bash -export N8N_ENCRYPTION_KEY="" -``` - - -## Execution Data Manual Runs - -Normally executions which got started via the Editor UI will not be saved as -they are normally only for testing and debugging. That default can be changed -with this environment variable. - -```bash -export EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true -``` - -This setting can also be overwritten on a per workflow basis in the workflow -settings in the Editor UI. - - -## Execution Data Error/Success - -When a workflow gets executed, it will save the result in the database. That's -the case for executions that succeeded and for the ones that failed. The -default behavior can be changed like this: - -```bash -export EXECUTIONS_DATA_SAVE_ON_ERROR=none -export EXECUTIONS_DATA_SAVE_ON_SUCCESS=none -``` - -Possible values are: - - **all**: Saves all data - - **none**: Does not save anything (recommended if a workflow runs very often and/or processes a lot of data, set up "Error Workflow" instead) - -These settings can also be overwritten on a per workflow basis in the workflow -settings in the Editor UI. - - -## Execute In Same Process - -All workflows get executed in their own separate process. This ensures that all CPU cores -get used and that they do not block each other on CPU intensive tasks. Additionally, this makes sure that -the crash of one execution does not take down the whole application. The disadvantage is, however, -that it slows down the start-time considerably and uses much more memory. So in case the -workflows are not CPU intensive and they have to start very fast, it is possible to run them -all directly in the main-process with this setting. - -```bash -export EXECUTIONS_PROCESS=main -``` - - -## Exclude Nodes - -It is possible to not allow users to use nodes of a specific node type. For example, if you -do not want that people can write data to the disk with the "n8n-nodes-base.writeBinaryFile" -node and that they cannot execute commands with the "n8n-nodes-base.executeCommand" node, you can -set the following: - -```bash -export NODES_EXCLUDE="[\"n8n-nodes-base.executeCommand\",\"n8n-nodes-base.writeBinaryFile\"]" -``` - - -## Custom Nodes Location - -Every user can add custom nodes that get loaded by n8n on startup. The default -location is in the subfolder `.n8n/custom` of the user who started n8n. -Additional folders can be defined with an environment variable. - -```bash -export N8N_CUSTOM_EXTENSIONS="/home/jim/n8n/custom-nodes;/data/n8n/nodes" -``` - - -## Use built-in and external modules in Function-Nodes - -For security reasons, importing modules is restricted by default in the Function-Nodes. -It is, however, possible to lift that restriction for built-in and external modules by -setting the following environment variables: -- `NODE_FUNCTION_ALLOW_BUILTIN`: For builtin modules -- `NODE_FUNCTION_ALLOW_EXTERNAL`: For external modules sourced from n8n/node_modules directory. External module support is disabled when env variable is not set. - -```bash -# Allows usage of all builtin modules -export NODE_FUNCTION_ALLOW_BUILTIN=* - -# Allows usage of only crypto -export NODE_FUNCTION_ALLOW_BUILTIN=crypto - -# Allows usage of only crypto and fs -export NODE_FUNCTION_ALLOW_BUILTIN=crypto,fs - -# Allow usage of external npm modules. Wildcard matching is not supported. -export NODE_FUNCTION_ALLOW_EXTERNAL=moment,lodash -``` - - -## SSL - -It is possible to start n8n with SSL enabled by supplying a certificate to use: - - -```bash -export N8N_PROTOCOL=https -export N8N_SSL_KEY=/data/certs/server.key -export N8N_SSL_CERT=/data/certs/server.pem -``` - - - -## Timezone - -The timezone is set by default to "America/New_York". For instance, it is used by the -Cron node to know at what time the workflow should be started. To set a different -default timezone simply set `GENERIC_TIMEZONE` to the appropriate value. For example, -if you want to set the timezone to Berlin (Germany): - -```bash -export GENERIC_TIMEZONE="Europe/Berlin" -``` - -You can find the name of your timezone here: -[https://momentjs.com/timezone/](https://momentjs.com/timezone/) - - -## User Folder - -User-specific data like the encryption key, SQLite database file, and -the ID of the tunnel (if used) gets saved by default in the subfolder -`.n8n` of the user who started n8n. It is possible to overwrite the -user-folder via an environment variable. - -```bash -export N8N_USER_FOLDER="/home/jim/n8n" -``` - - -## Webhook URL - -The webhook URL will normally be created automatically by combining -`N8N_PROTOCOL`, `N8N_HOST` and `N8N_PORT`. However, if n8n runs behind a -reverse proxy that would not work. That's because n8n runs internally -on port 5678 but is exposed to the web via the reverse proxy on port 443. In -that case, it is important to set the webhook URL manually so that it can be -displayed correctly in the Editor UI and even more important is that the correct -webhook URLs get registred with the external services. - -```bash -export WEBHOOK_TUNNEL_URL="https://n8n.example.com/" -``` - - -## Configuration via file - -It is also possible to configure n8n using a configuration file. - -It is not necessary to define all values but only the ones that should be -different from the defaults. - -If needed multiple files can also be supplied to. For example, have generic -base settings and some specific ones depending on the environment. - -The path to the JSON configuration file to use can be set using the environment -variable `N8N_CONFIG_FILES`. - -```bash -# Single file -export N8N_CONFIG_FILES=/folder/my-config.json - -# Multiple files can be comma-separated -export N8N_CONFIG_FILES=/folder/my-config.json,/folder/production.json -``` - -A possible configuration file could look like this: -```json -{ - "executions": { - "process": "main", - "saveDataOnSuccess": "none" - }, - "generic": { - "timezone": "Europe/Berlin" - }, - "security": { - "basicAuth": { - "active": true, - "user": "frank", - "password": "some-secure-password" - } - }, - "nodes": { - "exclude": "[\"n8n-nodes-base.executeCommand\",\"n8n-nodes-base.writeBinaryFile\"]" - } -} -``` - -All possible values which can be set and their defaults can be found here: - -[https://github.com/n8n-io/n8n/blob/master/packages/cli/config/index.ts](https://github.com/n8n-io/n8n/blob/master/packages/cli/config/index.ts) diff --git a/docs/create-node.md b/docs/create-node.md deleted file mode 100644 index 183c11c734..0000000000 --- a/docs/create-node.md +++ /dev/null @@ -1,145 +0,0 @@ -# Create Node - -It is quite easy to create your own nodes in n8n. Mainly three things have to be defined: - - 1. Generic information like name, description, image/icon - 1. The parameters to display via which the user can interact with it - 1. The code to run once the node gets executed - -To simplify the development process, we created a very basic CLI which creates boilerplate code to get started, builds the node (as they are written in TypeScript), and copies it to the correct location. - - -## Create the first basic node - - 1. Install the n8n-node-dev CLI: `npm install -g n8n-node-dev` - 1. Create and go into the newly created folder in which you want to keep the code of the node - 1. Use CLI to create boilerplate node code: `n8n-node-dev new` - 1. Answer the questions (the ā€œExecuteā€ node type is the regular node type that you probably want to create). - It will then create the node in the current folder. - 1. Programā€¦ Add the functionality to the node - 1. Build the node and copy to correct location: `n8n-node-dev build` - That command will build the JavaScript version of the node from the TypeScript code and copy it to the user folder where custom nodes get read from `~/.n8n/custom/` - 1. Restart n8n and refresh the window so that the new node gets displayed - - -## Create own custom n8n-nodes-module - -If you want to create multiple custom nodes which are either: - - - Only for yourself/your company - - Are only useful for a small number of people - - Require many or large dependencies - -It is best to create your own `n8n-nodes-module` which can be installed separately. -That is a simple npm package that contains the nodes and is set up in a way -that n8n can automatically find and load them on startup. - -When creating such a module the following rules have to be followed that n8n -can automatically find the nodes in the module: - - - The name of the module has to start with `n8n-nodes-` - - The `package.json` file has to contain a key `n8n` with the paths to nodes and credentials - - The module has to be installed alongside n8n - -An example starter module which contains one node and credentials and implements -the above can be found here: - -[https://github.com/n8n-io/n8n-nodes-starter](https://github.com/n8n-io/n8n-nodes-starter) - - -### Setup to use n8n-nodes-module - -To use a custom `n8n-nodes-module`, it simply has to be installed alongside n8n. -For example like this: - -```bash -# Create folder for n8n installation -mkdir my-n8n -cd my-n8n - -# Install n8n -npm install n8n - -# Install custom nodes module -npm install n8n-nodes-my-custom-nodes - -# Start n8n -n8n -``` - - -### Development/Testing of custom n8n-nodes-module - -This works in the same way as for any other npm module. - -Execute in the folder which contains the code of the custom `n8n-nodes-module` -which should be loaded with n8n: - -```bash -# Build the code -npm run build - -# "Publish" the package locally -npm link -``` - -Then in the folder in which n8n is installed: - -```bash -# "Install" the above locally published module -npm link n8n-nodes-my-custom-nodes - -# Start n8n -n8n -``` - - - -## Node Development Guidelines - - -Please make sure that everything works correctly and that no unnecessary code gets added. It is important to follow the following guidelines: - - -### Do not change incoming data - -Never change the incoming data a node receives (which can be queried with `this.getInputData()`) as it gets shared by all nodes. If data has to get added, changed or deleted it has to be cloned and the new data returned. If that is not done, sibling nodes which execute after the current one will operate on the altered data and would process different data than they were supposed to. -It is however not needed to always clone all the data. If a node for, example only, changes only the binary data but not the JSON data, a new item can be created which reuses the reference to the JSON item. - -An example can be seen in the code of the [ReadBinaryFile-Node](https://github.com/n8n-io/n8n/blob/master/packages/nodes-base/nodes/ReadBinaryFile.node.ts#L69-L83). - - -### Write nodes in TypeScript - -All code of n8n is written in TypeScript and hence, the nodes should also be written in TypeScript. That makes development easier, faster, and avoids at least some bugs. - - -### Use the built in request library - -Some third-party services have their own libraries on npm which make it easier to create an integration. It can be quite tempting to use them. The problem with those is that you add another dependency and not just one you add but also all the dependencies of the dependencies. This means more and more code gets added, has to get loaded, can introduce security vulnerabilities, bugs and so on. So please use the built-in module which can be used like this: - -```typescript -const response = await this.helpers.request(options); -``` - -That is simply using the npm package [`request-promise-native`](https://github.com/request/request-promise-native) which is the basic npm `request` module but with promises. For a full set of `options` consider looking at [the underlying `request` options documentation](https://github.com/request/request#requestoptions-callback). - - -### Reuse parameter names - -When a node can perform multiple operations like edit and delete some kind of entity, for both operations, it would need an entity-id. Do not call them "editId" and "deleteId" simply call them "id". n8n can handle multiple parameters with the same name without a problem as long as only one is visible. To make sure that is the case, the "displayOptions" can be used. By keeping the same name, the value can be kept if a user switches the operation from "edit" to "delete". - - -### Create an "Options" parameter - -Some nodes may need a lot of options. Add only the very important ones to the top level and for all others, create an "Options" parameter where they can be added if needed. This ensures that the interface stays clean and does not unnecessarily confuse people. A good example of that would be the XML node. - - -### Follow existing parameter naming guideline - -There is not much of a guideline yet but if your node can do multiple things, call the parameter which sets the behavior either "mode" (like "Merge" and "XML" node) or "operation" like the most other ones. If these operations can be done on different resources (like "User" or "Order) create a "resource" parameter (like "Pipedrive" and "Trello" node) - - -### Node Icons - -Check existing node icons as a reference when you create own ones. The resolution of an icon should be 60x60px and saved as PNG. diff --git a/docs/data-structure.md b/docs/data-structure.md deleted file mode 100644 index daeea8474d..0000000000 --- a/docs/data-structure.md +++ /dev/null @@ -1,39 +0,0 @@ -# Data Structure - -For "basic usage" it is not necessarily needed to understand how the data that -gets passed from one node to another is structured. However, it becomes important if you want to: - - - create your own node - - write custom expressions - - use the Function or Function Item node - - you want to get the most out of n8n - - -In n8n, all the data that is passed between nodes is an array of objects. It has the following structure: - -```json -[ - { - // Each item has to contain a "json" property. But it can be an empty object like {}. - // Any kind of JSON data is allowed. So arrays and the data being deeply nested is fine. - json: { // The actual data n8n operates on (required) - // This data is only an example it could be any kind of JSON data - jsonKeyName: 'keyValue', - anotherJsonKey: { - lowerLevelJsonKey: 1 - } - }, - // Binary data of item. The most items in n8n do not contain any (optional) - binary: { - // The key-name "binaryKeyName" is only an example. Any kind of key-name is possible. - binaryKeyName: { - data: '....', // Base64 encoded binary data (required) - mimeType: 'image/png', // Optional but should be set if possible (optional) - fileExtension: 'png', // Optional but should be set if possible (optional) - fileName: 'example.png', // Optional but should be set if possible (optional) - } - } - }, - ... -] -``` diff --git a/docs/database.md b/docs/database.md deleted file mode 100644 index 041520cf15..0000000000 --- a/docs/database.md +++ /dev/null @@ -1,109 +0,0 @@ -# Database - -By default, n8n uses SQLite to save credentials, past executions, and workflows. However, -n8n also supports MongoDB and PostgresDB. - - -## Shared Settings - -The following environment variables get used by all databases: - - - `DB_TABLE_PREFIX` (default: '') - Prefix for table names - - -## MongoDB - -!> **WARNING**: Use PostgresDB, if possible! MongoDB has problems saving large - amounts of data in a document, among other issues. So, support - may be dropped in the future. - -To use MongoDB as the database, you can provide the following environment variables like -in the example below: - - `DB_TYPE=mongodb` - - `DB_MONGODB_CONNECTION_URL=` - -Replace the following placeholders with the actual data: - - MONGO_DATABASE - - MONGO_HOST - - MONGO_PORT - - MONGO_USER - - MONGO_PASSWORD - -```bash -export DB_TYPE=mongodb -export DB_MONGODB_CONNECTION_URL=mongodb://MONGO_USER:MONGO_PASSWORD@MONGO_HOST:MONGO_PORT/MONGO_DATABASE -n8n start -``` - - -## PostgresDB - -To use PostgresDB as the database, you can provide the following environment variables - - `DB_TYPE=postgresdb` - - `DB_POSTGRESDB_DATABASE` (default: 'n8n') - - `DB_POSTGRESDB_HOST` (default: 'localhost') - - `DB_POSTGRESDB_PORT` (default: 5432) - - `DB_POSTGRESDB_USER` (default: 'root') - - `DB_POSTGRESDB_PASSWORD` (default: empty) - - `DB_POSTGRESDB_SCHEMA` (default: 'public') - - -```bash -export DB_TYPE=postgresdb -export DB_POSTGRESDB_DATABASE=n8n -export DB_POSTGRESDB_HOST=postgresdb -export DB_POSTGRESDB_PORT=5432 -export DB_POSTGRESDB_USER=n8n -export DB_POSTGRESDB_PASSWORD=n8n -export DB_POSTGRESDB_SCHEMA=n8n - -n8n start -``` - -## MySQL / MariaDB - -The compatibility with MySQL/MariaDB has been tested. Even then, it is advisable to observe the operation of the application with this database as this option has been recently added. If you spot any problems, feel free to submit a burg report or a pull request. - -To use MySQL as database you can provide the following environment variables: - - `DB_TYPE=mysqldb` or `DB_TYPE=mariadb` - - `DB_MYSQLDB_DATABASE` (default: 'n8n') - - `DB_MYSQLDB_HOST` (default: 'localhost') - - `DB_MYSQLDB_PORT` (default: 3306) - - `DB_MYSQLDB_USER` (default: 'root') - - `DB_MYSQLDB_PASSWORD` (default: empty) - - -```bash -export DB_TYPE=mysqldb -export DB_MYSQLDB_DATABASE=n8n -export DB_MYSQLDB_HOST=mysqldb -export DB_MYSQLDB_PORT=3306 -export DB_MYSQLDB_USER=n8n -export DB_MYSQLDB_PASSWORD=n8n - -n8n start -``` - -## SQLite - -This is the default database that gets used if nothing is defined. - -The database file is located at: -`~/.n8n/database.sqlite` - - -## Other Databases - -Currently, only the databases mentioned above are supported. n8n internally uses -[TypeORM](https://typeorm.io), so adding support for the following databases -should not be too much work: - - - CockroachDB - - Microsoft SQL - - Oracle - -If you cannot use any of the currently supported databases for some reason and -you can code, we'd appreciate your support in the form of a pull request. If not, you can request -for support here: - -[https://community.n8n.io/c/feature-requests/cli](https://community.n8n.io/c/feature-requests/cli) diff --git a/docs/development.md b/docs/development.md deleted file mode 100644 index d7d8de4744..0000000000 --- a/docs/development.md +++ /dev/null @@ -1,3 +0,0 @@ -# Development - -Have you found a bug :bug:? Or maybe you have a nice feature :sparkles: to contribute? The [CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you get your development environment ready in minutes. diff --git a/docs/docker.md b/docs/docker.md deleted file mode 100644 index 317b5b9552..0000000000 --- a/docs/docker.md +++ /dev/null @@ -1,7 +0,0 @@ -# Docker - -Detailed information about how to run n8n in Docker can be found in the README -of the [Docker Image](https://github.com/n8n-io/n8n/blob/master/docker/images/n8n/README.md). - -A basic step by step example setup of n8n with docker-compose and Let's Encrypt is available on the -[Server Setup](server-setup.md) page. diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 2b03a0b76d..0000000000 --- a/docs/faq.md +++ /dev/null @@ -1,47 +0,0 @@ -# FAQ - -## Integrations - - -### Can you create an integration for service X? - -You can request new integrations to be added to our forum. There is a special section for that where -other users can also upvote it so that we know which integrations are important and should be -created next. Request a new feature [here](https://community.n8n.io/c/feature-requests/nodes). - - -### An integration exists already but a feature is missing. Can you add it? - -Adding new functionality to an existing integration is normally not that complicated. So the chance is -high that we can do that quite fast. Post your feature request in the forum and we'll see -what we can do. Request a new feature [here](https://community.n8n.io/c/feature-requests/nodes). - - -### How can I create an integration myself? - -Information about that can be found in the [CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md). - - -## License - - -### Which license does n8n use? - -n8n is [fair-code](http://faircode.io) licensed under [Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) - - -### Is n8n open-source? - -No. The [Commons Clause](https://commonsclause.com) that is attached to the Apache 2.0 license takes away some rights. Hence, according to the definition of the [Open Source Initiative (OSI)](https://opensource.org/osd), n8n is not open-source. Nonetheless, the source code is open and everyone (individuals and companies) can use it for free. However, it is not allowed to make money directly with n8n. - -For instance, one cannot charge others to host or support n8n. However, to make things simpler, we grant everyone (individuals and companies) the right to offer consulting or support without prior permission as long as it is less than 30,000 USD ($30k) per annum. -If your revenue from services based on n8n is greater than $30k per annum, we'd invite you to become a partner and apply for a license. If you have any questions about this, feel free to reach out to us at [license@n8n.io](mailto:license@n8n.io). - - -### Why is n8n not open-source but [fair-code](http://faircode.io) licensed instead? - -We love open-source and the idea that everybody can freely use and extend what we wrote. Our community is at the heart of everything that we do and we understand that people who contribute to a project are the main drivers that push a project forward. So to make sure that the project continues to evolve and stay alive in the longer run, we decided to attach the Commons Clause. This ensures that no other person or company can make money directly with n8n. Especially if it competes with how we plan to finance our further development. For the greater majority of the people, it will not make any difference at all. At the same time, it protects the project. - -As n8n itself depends on and uses a lot of other open-source projects, it is only fair that we support them back. That is why we have planned to contribute a certain percentage of revenue/profit every month to these projects. - -We have already started with the first monthly contributions via [Open Collective](https://opencollective.com/n8n). It is not much yet, but we hope to be able to ramp that up substantially over time. diff --git a/docs/images/n8n-logo.png b/docs/images/n8n-logo.png deleted file mode 100644 index a77f36aeeead7e17aa4390937e90f48652d811c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2675 zcmV-(3XJuMP)Px#Do{*RMF0Q*I89ssZCd|rS~yKx|7}|TZCW@@TdQ7L z|7}`0O000bhQchC013}YL_t(| z+U=d$nyVlTfXlwvzW?h^_X3gxP^@;y)%i}xYWc}R67Y07peQATQ0g?~2gC_}ehC)? z>cK^QXug=*F)F`<^Nw_A=|RQnb6)gN(nC5=eujj*F+9J-a}fPCo`dM|{3p(H5Ivgc zGbMTq(t$*rX%Sm_q39=yeTRx3MHC#(M#gyL z01p`AI%Iu10^NP23~t3uR5<1*Lq9qO^ZV9R^K*q{++=8Uht2&q&MKOKQ57T3Ju1$; z3aSBgkBiGP$t7&v0J;aUx`Agt>gfm68p+t@DvZg=Z!s!LUqu|S&|vv_??CNzXZaB( zdoz7Iq2rmFHlADhPy#Gt$bc~TWd-)F!=cp?UFLmkIDL& zr>peSSiu|j>ZQuUkx)lBL@@;mowYix-xilym~|JM>(*|k5t&irC1akggMnMZbW0SY znzDk6)+`h9S)Y|lKvb+eKWb(^R5yY4ZStmdqi9G9E_0zdv)VV#eFu4KYo-ulL_>{a-Kwass{6g}ZrVZ))y6Z&Y(e?b<;`v;q7ITh zpYb$FNi09o6PDnc1I=Y2Y*I^QB|&jmNJnYTQp=_u3m6&k`U>Wv@?}Ozv+5cB>IXN= z%F^r}3w-7mW@@FG(m1mfK#?HYWXrU(2~$wb(oLafZYnG+U&f8RTq`mi`xqWmTsT1% zn`W-P(bqu}r-bNbJ_l!W4XV0dn8&c_q+1;v{8Y2~?aDNTqv1Kwm<&-uKLid3WjfAN zc`0myQ?_g#Fr{S}4-L7k0V+{vKy*^XSs4CG__>%VCsz`0SC-gC1qze@<3*(~js(h{GcB5&2qn9NQ3**G< zGW;Tj`Ona7L}9O-RkK{SMi<6Pp`g}Nhi+M9!mR%1&={nt)}yt})N-=P1QF&$On@ln z48E$Ma|KFVc({?&qyYh5%_`e~!W(iG{kz%>64_I=O+)lb}GL5y)x3$3e8I&5+9mqC|- z6Phl9d0GOfsY*SS0cH4VHkf^u%#{~yTByzsZ*q{EL1S58lv^lUe6S*e+zga#eet2W zC%l?ybD=oUZ4Tx$cqj{hR+^H@xj5|8R-lYD+A_HRzW{0xpx6>Ynt3{0BE3dg+;SmM zi~fw{PyD|yxw0mrG)Y7`-zRE8pzPEEXh|oN{LHBT5ES_epf&@}%S&0rEPDWz1q<0A z^7;qM7FH8c$}G0HW(_VAeGi~TKKS!!8nwA)J99I=Lwiar$O@pKT1QByny_arpseJ| zOEmHd!*V;)G!l$XSXRlW{?2ZfK#}rxxHz>H3zMZli`KE9n>a+%qH34Lv{w~;wqbHU z25QO&56uDuN;-W3s4N2-*T^i$6K(fOgB;*HH!Ght1D(pW8c^$f098<(T2TdV4){cP zm)}+e?q;Bj=<3PaT!s$$#wIt7M_+AM$^Iiwu1xrICt=dN68p-HT2Ph)rD6}WW1wob zRqrY#O=7b>VLWr8fF}WEbKR>QOenVzP-^hd<3H!Ry3t;oXOrs zUVmO%51{4?KxIK!l8qjm-hByeKc%OFLhQ6C##ws2#!mvB3U;L?mV?R%fr&bDDhN!g zVW5Jqkc?OY^s*z9TT)O>eY%NA6q)@5MdU+;(RWd?3-fg- z(JV0%DE5^#L|cGLTpN5oG1wWXBvmw19e_715o3rPe4C#L6i|HYR0YvRiG}<{L}H;6 zP*3%lWuF=XL%%*3aCr2NHQwTu%ZauCwP;a%0!Fqo(38YZNP>D&RR*apI_XQ-oVGz6 znxR`G?p1&WV~6yWr7MWuKt0+4cIxSuzm(~rbN9ZGI9(GxcxvuIxo$ndrU_`gvE_4R+=$|vfO_)r zz$`z5I>y7A9{bMA-_WDsk_7T2Jv7t*Re@fZyh_fC-anh}n@$+oJYNX(ej{H3GN8PKcP3jUrC87kBJySe@w7fHd8*-j^R5P zQnx(H9r6vNniuajJn;Kc|CKoPKATLVohE#|J^gTOv9L&-M@)1$(J)bU9v1h2E*0!I z6r1i99*)Xh diff --git a/docs/images/n8n-screenshot.png b/docs/images/n8n-screenshot.png deleted file mode 100644 index 55b476cfb61bcf418baf09ebfd3c827ce563b4e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129698 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Ⓢ&|eMmSFYY{aFcd}WsmeQbVsh<(}{1561ehB5^ z75ti)PFdNhyxZEM5aGmRm5}DqYibYW&u&3OuZSWLXzE>&oXO$C5Mv@>5&>_ORZE#H za41m$A0xmY$gdL{A4~iAy|Rb(Ji9DMESL*8noB}&Mc-cR6*V*!`hB#E-xqE{JL|4( z$3D#nP4#swK|d;um^-nlMWw7>iwW$zZ+m!Ukxpw%UESR3DnA+7PK{vCGkx=w!fK_8 z&WH*WE$$>$g}~%Hyl0C3racsb-ITZ+_)T^jwfE`>WfeBm)JN%h!)kg`|Iy+x#sR5$ zC(`vhNiSb*v;5t7C{p8aWBzTTu0sY^MQ!S!`@x4f-AGTt06I2$N$pHb$YIHVw6R}a z_G5U7|_G2UnE@oe3)Ym}9!vrmdSeOz>^etxwlg_(ogwiHRk z9cQ_yaZqOg3#yfLoXm$mdWy&PW$vmxrJ3$*;x;xmMv&Z0dn!PcprykiP8pVHoqRIO z?-?Qe-IfqJ+Ut@VS(4^dC0Q5CtLY0fNUloH~q;Y}i?=a(HJ4-BGykGKj?e2Ip zcM#1TAe9%k@c-w|?E;t-$y8C9)JZTtWJZfEE4I>c#=Fa-p-joc4=J(1>a0o>jYkc?X5Oa3ra#a9mhf<~0Vkk73M;Q6XFGXEwucVpE94k)){e^w}x?@O1OU3d>ZF&PWAv=!(@+oP3{; zSM?L3Bire+V+2oyQMHw#^Kx098g{<zlc(N%dM1_k0{KC4OUEY zG|C!O_3?4QHu=Ee!0AfPe|`K)NJmbxuV|k6BuWwXnJksYD*DSRI&j=^zxzD7#jIRu zuIS^LUAI5f_gG@S>&nG=y-C@j$s>=8!rWi~C)64oaV$%3lAu$Ck{ISyHt9#_dioPgt~$PyGf@c<1Vr$Q9x*Q@i833ljqaYbI1 zx@cdsUU{XUcaJlweWRvbHz#LK$^iTY&t~jANok}78b=VXR!INApuN6$K-a|u(II6A zPhFeQI?_FN))E8>A#%W2evr)R!_{j1Z?uh`$(~h&*!tWFrqY>T-i1Z^uAhlzKkUD(qzcZ% zk(|Z!6uH-UEBgplYQJ#v+xLP4R-gbsQ&Ahj;StY;BB18zmh|jvtKJn`=)wT6mx|Mc ziKUrq_Nb@}afk+MyoJnKSu1U$E;*;~M28JLIV=fBvQ@1j&R4XyHDgi_@lnUQTsN>b zDo>|`jqDV+d`~gY&N4k=E3ydoC$LD8{hj3v-e5p|`LePz#&8bo9ys!xuQ|?t;+$!h z7^tlW2U{RsKK+p{uUT{4R8DlVUB@+9>R=!5V1Qs7f#wkP&GFRSqT_{Es!1%2^}%1GJ_Tge7Fw;fJZdI-ZlFQ5>`F!;hfX?XLc&A>w zG3Fa+EL_Bbskn;Za$8;~!1}KkoQ`2e9+7^&Uapu}>MLw+sN~8v20ByUlKXKQvA|j!MSeEXKopa`RJ|aN^POm`kT< z_?4_VXfT^1sFOi6Euhy3$-k%YEi}gHL+m;2B~-{Rypz`O`wuL37IpiO&nu3A*vuh3 z`^9V&`=F%U#Dm8hT(??H@-1_fnq%tX;<737_*+|B+pT{}8aMvq@mT1^Zt#faMWUm`7ekz&Xh4CYk6y#UM8^(_dtUqNJYMpgH-^_E zho7}-Qj!m}=ek#Q7oa4`>2}%iJQ4ws;ScgA*6uwF|2Q&%7LvWS{aK{>Pa^J5hJn}L z{#!L z$67Z3aM%4HI}kQg4wQLObGQic?&Up!&urSkx3_H+7>E+Prxd8_VvPCGrBkC*Ryz06 zNQ8)S5cff3ZveuRmHEyFlb*g5gi3B6kB^T{aVM=I7&th}eJ)(!<&c&YGN?VWpFC>o z+`TgYVEj@B6p}4DRF~Y~>*`vGL9=K+%DINg-)9I@9)=@jkW z^g6IIVo>g)!g86*6+sX=WQqVnh2_K4;o@N6!6`I|Ijoim!L}hm7 zph{gt(c$817>Gr9@9@YjG0ty`hv3X(0j}#(CJ(9mu}y55Soa$5QbVjCTWP0czG#ji z`jO&4k-`D9CZ>62^CYh(pJDH%Hac3|1F`)m$V#l#oIrq$U1W=V@ciisMZH*y+h0%a z*D97H1HUVpF5%d7?y|*|&HI0rg6`OW`GK~vvxhYdyu$I_r3U*>07_ASJ)46UX6Rmv z`d2R=Mxz+-qojOwYug=5ASiN!V00;|-%cO?+fCAAvy-SeZTp|69UC z!Ld)x)czsY!=*{2^h>eju(Z~6()3}f{f`g-2{f<4AI`CNOKT?v<8RpdIt(izQs#(n z)dp7wnfavp$F3c7K+y{kp(2vM@qp!N)i;lemJONkI3k%9Ee04Bwy)&Azc~|vS~IO;u)=ePZ&6n=lA(N4UiWhr_DPz!;5lM}a}-J2g=zS2+N1;D30*CCrf zdQXkyE0r0fl@{^Tm>%~aD=P3wh~G1Co_F%j*WAvs7=B%E6E|n7`Of7q*aT3JD=Obt zXZr#*9sC``x4SuX)}$h8_Vhaj5#>UqspyJUDYPS848T3MYK`k-ruINr``J@b~j?mEss{T)c8TN25D_ ztbs$I!@Dd0N4g5-0mB56@=|MEx}oT3sCj+*6)=yHWenX_{!4DkycbFj z;hz4>pDo&6D`Lh*ramDw8lR9VVw#Mn3hupHOyzuVi>&BDDF)pa07y(T46j9dRXOe~ z^vJT=_G74y!Q)~6BlN7+KNc!o3Fer2I4+j~`nlkZM6w={4MJZ70!-k-jyM#AOpt!B zLDIQWEe)avP>#->Upx8ba8mg$uu~F7rk%m#>6WS3OI9CTZu4%t?h>WppgSAR$6Mn8 zjOi@TQ=FGBJI2z^3)38LuZgVOBe@?6*`x^m4Am05{;G75zm~b`CyiKydmUU|e8+m8 zWBPKp^O%ff55%E9U15*e- z%V7~cUbJB#3h(2PdK`6-PI3q6#SU)SZ85c8P0<*Rc^h?70S+i%?|Qp>5-XoWKXZr0F#F`ihR9A$)PpDs07aU|i!VrmiXmQiT{J1QC%qB4>N0P+ zq~#3b?8&HW`_hoj<=QU2`?BIWf2Bvhmg{wQcAWp%0$jNqmx>%0q&uBWjD&rO)_M|y zXvG$TaI+c7(H{x8xfd}pr%z~7jlvm{n}W2oGgn;%#+{p;kONmr3D$ZY1|&edySbiL zY8TF6dPVCJ%?3R~gjIZkxs*C`ay`Ju{*~uqqZ`R{z8!NzveIS-hjPwp)vbX$u}hWd zb?L{7PE|)X4yN%OXsvgI^4`r7vX$aubl? z2KGLUA}`(fQz$|=Z19S&VoIrz?3i#hPUaY&cs(~tcBBR0vd7D=gv~PHcMKE7eiu!& zUWYFyxpdf&0gM6HhvmJ)uDY5jDWr7^3Is+#$pQkZb1GA0ni=^b!!*P=DJt*hg<~1!>^}zdlF`|*ir*nS=y2kaU z{l_JlvG2J5v8VGCxb`dMqcv8mkrn&b*Dk*R)VnST3;V_~QTy7V$D6RIU-fP*Ce2`6 zyOm-mraq1mPn6}Lc$?=<)y}4X`Ls`@_h9nL;-vly zY`0x2dm|s^G*8Mj#WUn@Eu_G2cyk?Af=yv$E3N2GQ31CPkRK7gu-!~DkHWefdwvlihQbd0*5pkuTJCGOXq_kQh+ zSr?AlKQ!!TM=NT!;P%3r5vBYWGqFL2pYlKF8=@k~KSk7~gfP5^PzqW=y^p8#jVov1 z%2}X`V5~j43?mw$U}RS}hacqi3Z;zYQlFZD!GeKo#%x7f)9Tr z(WnZ_t0ZeG@s4=~fsftnKuWAE^D+E@(gb>|+f`0OCAUQb6sasn<~A*@by>25qP(k( zsD#Yqb&MUyB(T{MzMvs|d(|6~K*tNl_q($FBKknl6+V<+O}3g3ybNYnSD=PJ06&?l z*n+^UN_C*M_Qp4}B)UIQmx(#3n;BZ-`F8DNDhjVpA>b3oS z2IUl!-0E(Bd;;ecH}V@?#lO`RsKN{{1`?yK##JN9c&KyJ3_1LI*KLc*b<9)A$a;E> z`l%%@x`k-hm8p~61OeKdZY;dTM~MmX#dsz{3n$gm2^M?pyvAv>Rsmtio|>wtiKK^* zEJoRi1yehr)H>gX6RMo^4fmuS2K={cb$l?Lr6WpQrwWrawgbHJ!;RyUsk6wX*%KvA z5}JO+a6hukl6CAqJcSMB4tgCReC>5yu+>b@FJ^Y<2(`kCv5$-RDYr#bXAAhU*j4MC zHS6t#dR6|^C_UBzqpp)~QqSlsARD~sC&4r#AFN|WIIPt2n(~}8Z|$NG%RRoAms7n| zF-A}0&~4Y==338S34;KGozGoWjNn``vp$@rWGpCoV$Q&VFwXV8Z(Nh*Tr4sD z(YJkrFu5-Dc$OZOmPgwVZc2Ype<2_@=~r1x zFHzaYrT@UiblUiogV%VWRU>JVbV1Ka=7?m)Y8XP{>czxcco|;v9=8DB2`Ei1#y(r6 zQj1SD#Hq<42k-bBG%hBc{?1mkmDcq&uEno#cl*G-->mPK@Tk2(_&3DBR-$7S_wx0f z28x%bRxXRR@*}spmb=&IDzSC#cdqIQ4=_wx!sL|j^7;hu- z34UoQ2lO!>b#!d5nI$AF87=%LLoyp?hGYzMylWt&V#kMX3!}kcs|tc26ufMbY@R&pL~`U{PKfX zexo=rknV&&g}Iaeu=1AZ*zz*UrtH;Dj3{ zAIG%Ti(sDpf}Ao4an7U4s>6K4r_Nr27aGQHEt&+pPy2bUiHZ(taw)=ATuqZR$FFnu zP~0y`u1ggwa=KYtIZmyISKSlOE;XOgTISn6@i;gP9&magT{qBD$gry{$G>_LkeYn` zjHr_B8bM==oO&&k_xc-tuyrZUb!o4Up{X1+j)xi6a^)Q2d}?ln9G)m5su(9|WGClX z+Z)cct5r+dyx#D&cZhFQu}6appUZkCO77PE(Wo2&=Ee(rZAW^lP*2D^<0a2w^}~+& z`&6x{y-83(Ij6)7a#6U=Azdvdc90pmhXPR4ter*6QMAyiR<;xl`Jxujn7DM>X4~Zt z8Wbc_Ap5dM_@SeEH^t?FK5>95r$Rz!^^Uv)Q12hg9}X#mIIvhb_^O%W)LzGUKk3mR zMy|e6aeN=i@Ai8(I&JoQmXKu_8iX%0e*p3a1Qa?e8jB_8(@H%plzW}eyE5Tcm5{RJ zF+?&oG?V6tOkh_(i6$GQ|J1NRbTrQzI?~O{xbp-Qm3J21`jKdeqWr4vnmLV(Iw+4t ziXDrr9xfW^?3-`Z+)|8|a`|(=>!xgg*Q^iTOD`kwwu%C=;>T@XmR@)8zY!Mei-d9X z;OpI5?WxRqmCE5YX@R1toCSY~r;0z(BQSP`ow@{!bwhu_{(2pX%X9JTM~d zamad;ZFb=#OLlJx(eV95$;++eX?D0X-v>~kx9|l5v;Yw6p%RQC{p#b0*BizF)wTbr z(p3l-YGBcAH&<`B%xYVwGK^ehaeVfo5#ey<9_`IM2~Eka2Xtol4Fl_uHbp9(ctA$% zXJaS@)eB<`1oj4q%3%n4K%=|6JI=z&>Qq(74I?cg)ayUX(-uBNG4;xyltiC;=+3$n z5if|buEeJln$r9%X?R!)YF&EWd7Ws{2BkG; zr^eha+(|V!Iogu-(7vLgkoETL%;dn6-&q>rq(UKI5cdy(2>(>1%PI&8E37=~${*Ka zWMBIAfq>3*a?ia;e@#M!k!Mkbwg-NktG_?^EiW8T|FUOTyV+HLa`9m4cN%%Khr#`+ z@%_>{3B^$NJJJ35ad-TwOeBW3|CLSg$_senv>$nt-~)al+U>IO@BTrTo! z{5v)MZf{95P#pS7ZZwAYFe9HG9s=|0{>XNECq*x*p2R9h7)1KJ)`dGGjp zWy5C`#8zdvrX?ODe>|Pen~dz1bdFoA)7923>m0PF+{OA`{^&EiTjWtv z^Tks^rmVS1&E*|ua8@RvA*(B>1pE(Ar)X&)eR8|yW#pGnwm|=L=e_CNVIb%CAQyw# zudk}wCYC9090oA3haqcCtALvuy%rFy>%FiUlOy3bLO}JxB1=#JC1>K5Q(`at2g_o` zLb?-BQl`Sy-qWe!d0?x=x-x z{q+@JX5q>XfM%Swe+S6gTj!SfZ%R+1Pp9jL;F0d*IDGi95~^4&m#UY6@8n>gRK;pM zsT`g8E^&s%E(vw#p|C0MxwRcM>K|+j+P*ylQ8E|QL%=yn!q30IlwEpC`vvr%ZQJtC zZnBs*I_pkJ3OIPppKjgbiWsYda_FG%om{P=n@3pN+ZPuU9317v$E)(|$Y$#wEm10# zMrT%ZWXkeOSlM^Ey9y>%WeXe5VsO`Lo2;}iWxD=y0Ni{oT))2Zi)<)6fbKqT<*E2l zdRIT|@I>obT0+woOF*x<$WA!4ieBya+P{Le^k_8Qzkdh$M=b}uw3pj}RhN&?OgwP4 zP9%u%k;B@gG0;GK{5Q%8NUnOIH)vR-Z%?<)O0;u%K1tbN3M`S)tFsmsw(}yCBPZ6H zMOrn!J8r15OPVf&_++m)jkqwWb42HMR((~g$_g#GL!&nBTDI0kT<@TlR?NjskoNJ9 z_5k``9fMqyeg)f+q^hY4a>7Bp-uUA+BK_cR#s) z3A1qAFDvsd!&8YfMiU`OV*LGgo|G|c4oM<5*V`Xo%n;A*Pj-lQ2*0rw>4M0jyq>G= zRv)`FSoK;ZTe8LoFUuuAYMuk%U%V1z(9J#W9QMEW7;3pnilR?_nhSoF(qtTO0yBQe z$yeO=7`Qj@G>0wCO)WukdDL3X!m{H3XXn*wtV%0IBTx4&s%f2@5-uVmKgRARSU@1P zAlG`vqJq4OP=^rg3$@?u6OGh%ck}DAGERAsjl243ncU9ZN}9QPcAcA8#NxZjLBG=4EJu9o#-QUaUvzjk-l>4HW zHDP)tX=6^X@UBVRDRsg{0g{W&%;%y401}x`SvKLaW|NNHdJ`Af3(YR5%rJ%tTX8K` z?HB&MA-fXB93Bty6NbhMyBWgFz1JT>Li!7WTFpMxdVOcvca;C~;>u+e(c@r0c9j42 zmypT5Zx>eLL%sF=C;YpooT>KF*IO-Kd3KZq?WcjXw1+#!`lcvI4L^L-?fMQa9Ln;J zoUUWpUUHZ}x#(e)%>4|Q3cG1OcI=8*$IiO0T1bOaVNkXjL=6Ei45<9!;_~`*IjV(! z3>+Z@>bL_f<0(zj0t^d3S*}V-JCkK>hvwRiS5DmIa><5B{+hE;>Ab;#QYkw?TKD#N z;322mytL3z}U^-9M3{%by-@TsEe+f~{CX`@=6Nq${$_dEHt& zBt~)hFlPFgOzM6E+noTUo53>vYEf$sqGB=p3?%`v^GHrIWLqxQZ~2Xn!#CY;j)?g7 zzUdivClq_QbCG@ivC+)ue)}L~H85Sn)pu=86zByalo8svJl z?DTa=I01C+rQyHrB*c8H{Q4UQv%6mL&dxOIYftZ3vicJ}tiXAw57=M;S8 zNgx)c-n2$RVL@P$=q5U?CSSfPyEL5)Lirevcoe9cB71@E(hSJO_w|pV zH}f&jC2OfglBMKb-!XMM&er+cYqe3mP#o>-)aZ3WyJ8E%hD`Y?vu|0s-LLw#WT5KE zhJR3U)9V=-PNIW)J%1LvJpH_y{&;ulmTLFW`C6!@+1tM~&Hv$SS3LJL$t}F$X=&H$ zSz}~3sIjl{cwgU<`0vj%>G3P{-4g!HjUlX_pp@A@Cn7k-d!wd-I8U^{(x4EACSi+o zr&&kz^|s5iW>Wz??B5TbgiAdX3F_DV-8>@Ikl?V2JWeTJ*arp4{JNTesYLf4uWp7L z*`e?d-knBIdhCE^UYjrtRi|3sf6N4{?eTA=;)+yH!nW34X{`%95N-oCYSOoQ{QHQ#9n$s_G}qG&X&w; z^BEd8$==NJ_raqJ?GfNUes_%_8jS(eKLwZG81JCRP>0k9_J|wA@nMi+rr))Xz5o9^ zkm&@hvw#U8na6`H$i5-Uvs_#uKxOnuClK%fr0hr&$S-ljw{plVqqmh|dwhK*pF}7w z;bRDn{AJv#FCR6;s9MUDJiIv8imxg9HErF8NSgm*A-t zTLR!X`$P|(N<-A6FT{qM#`C!7{MgXxe^o57{-M|DPS%N$GyNgk`+fX>;` zfpm@hHZ$<693u0Rj{%qWA7+ki1=-_opKDPJr+7FqH91g_e1^qF=5r<%xVylgi>b7= zO&KsZUGCmFUBFw$#iK{>Tsxn;-KRp}rNCwkP-WV9z{?uw&LC_Y(*Z4HrPV|fh za?p-AZX@&!&5wxp_d>a!R2$7~|5K~xeDchXbV`SJZ79$-m<%7W|LI%sij7?-N#Wq2)yBLR&v5%B zYp<1UT*(3LJ(6UkO<+#M)lp}`(>HNok7s*xOw~8FhqI{>)Cpjrx{6J|M^7@qZnA{^ zLx)_RDQ*z1JxzDmwag^n*(Xr@T&k8-I~@T`z-x^vd!%;Aibxq@>*g)VA)}G8SV@Sa z)A@ipfCfpsSjM@x_wp0I z9=VJfWX8gh@!c&m#}o3d)G`7Xjj=1+v6Q|Ef`^P%Iz_r@Eq#~_pOe(kI(Q0!7(j&- zDKT!SA9>V}R=wrr+>KbpMKD!fAm^=TXDui#eH8*a z9p%K8+j#HTB4fTb4bXQ)_7WA*(Y`S*%^Hg}>_bo1C_6G?gAE8(4eq(HtpsHe)R)&M zlQ}108;M>-wB1GGdQmnAs~s+x$_<~w$hbN6o*8{L@JBm^kv zi6Ywu|1V`G-xwTiT}(C+eah(6iN1WSn&;6KJW&ipvworZW8-we@S0SWeHKd`!Tq}% zGN*9asSeCz)LH$zEnQ+13ze1GmT-km+i{Jc{0_0Yb?a2b=UDUrT$LOw%~H+k2<&(W z)6W<76mi)}8w9DyKEX-k;_LVRzA&(QAl8b~$>x*qG!&pW?D-@8dH`uSS=T6fvAxXcC99>(0-?-OiUmwzG)Eo|9x#3Z{jgHzMEd{EWz_4B2r4HF(-`L?Gw(|eNem3YrZ;`5i%sHW1I`xi<6IQ*ycxoE8 z2Ni%GqsJep60%2D<&f+EB;}o1=}nCe1GSP*eK6!Kj9_I6^ejPO2$0zM@P9Wo#8xv> zT(02`3TzBLr^)k#rsml}hiV%yF|;D6+ZBjd5E-dxbtbU>6sA`50u4(u;#~U1`HOvs zek25}<7+1(SQ7Z^ls|(thDp`lpW&s<3@~ z+TMJzNeAVrbC90B)#dTH_OaD)(dm$_V|e6_1loIrselc1uakvVw!TlTQvm(6imrfqwcxO~LvLm|aeh_6#GFMNX8N19WBkkzev#(x& z77ZilW~7aoXdTD=fA(Y5^(J%^FpD#Be!IvLkA{MH%RZe%-QS6w+=*qD5JU5>bE9GU z!Gov@2=2+-H6Me0A08gEva&!#XAtCG72nBzzv9lnIKMbwHOz3WKEO)B!$r~;irdFs zz!_IJ=yY8W@lp>HUCo+LN2iBtp@9Maujt_NVJ}v-QdRd0A1rA=YsJ%Q8uY_ZRt+jjigtE!n zL0hQI05*ejpel_=0~1+|$9~i19Sfdv=e<^|r6m~W73wyAsQDa0ie$#q=4c7zHuQZc z0{T@zci8K_4>t-Y5}fO#PlWe8)@|k;Q=*y%F}hD#raE>)lbW5KF1H@0>qYrxonLz{ zR!1)~&es1t(6_`$gUMN{`*IuAP|)6HP&rp*sa}VHd!drLQy}q*#qS~`>r8;%r!l|k zp6D?VHk_??6y)6fkOqHgE~%4-;<9_lMIAfP3hIjR_4U=_i=nzbnvadWo_uYt7?I$r z@yzg)$$kD&N_ULaQDXpWX6Kdk5Uc|v3o#gdhF`BPFoJG=5vt&Ou4_>dJJ3Xsz*@cq zBtM?4f2Psn%w&Vm%~rGLXNi@lHXMw58L|;-8#0a$k2~d2_X{U{vwP|M4A?d3tV*od z_;oZv(i}7$#}|gZWDs7#pmtGSQsA*R+5``lejq4~^ZSNFzOtEE<# z1b3!MFH4r*BVQ6Huy6Nm-KSq%@Wv12L%`I?XXes`vlW`JCM4g*a&I$V5_2&YvN)Ic z-|GvTHX>^mkyqJ7XY`H&&>0iSw%@PL@4h@5lK~!@ln9+4pBsd&cutJV2cN79&1?jO zmfN!?Eu-tOMwLHEjOO76jD~E(pIEUsewJmo0{V;BfDKw z6}`MhAI`z%4rd?|m|x42`WIE3{gTU^(aL9@0|J_VjnANe)k`ZG_iLELNp!64w54cf ziXT6`$5$!bFx*G<8wz^L&ukgpHs0@_D69*V$X+^|dH8xX?X|%7t;O3fNaA?iz~?NK z7FqE5DRv6`KyCk!X60 ztV#?Tl7+HEsZ(vwnyR`vM2?L53&KRFEv{i5ONkLEI_><}hc{OVXA?D;s^@;KWOw|o z`6cOGEM@D|$a(ielkW7p(0oZ`(v#S59XiIAVxEV6`HQ^+NUO_?L?Aa>I1^i47~X%Z zkyPH~A?t%S{qI4NBCg_ly(*{q3*?Iv8-TZiF_0XF#ezMryicFiK zICeRT2+o4%h@ccam`Zv`y~R1g;{_hemX@ z=Np9MriNfM<&*#}zSKLvkiftk#)S@CZTKWZ*I!W?jgrOy^2F$VN-UK2GQ=Y4k&T0c zg(u)zUcGb5#gS1nv6&Q#){lA-RjSqlG91Yd)Ig+DV#0?>2QcqWuCici3`b+a88N4(^cKw<+foiNUEumY#UVugzhw4j5L6 z=(}dKx8e%6wzel*ag`M#ncD3K|39+cfw9hRTN{qm*hzynwr$%^8r!y;G`5Y#Sy5x# zwr$(~R-b+L+55cT4@l-*^Bx}8xJLAfKH6ZddF#Nph~Gc4=S0lDS&BPdVqmEWm>aby zg3?sRI28Zh2s7stsbV8t$k?T!q4n~f%Hlvj)5xJ&giqO2214F0&*<6gsNf>F5o$KL z8ji=`_ua%Lb+rBKHG_fZX1BqUw|p~+quL@lqQqk8W|h9!>v2Y`qLR=xGy~63By92X zHg8!~79pnDyAArGd-I3-(ZV$chqr89K*U=F-Cj#@)wPrPS{0yNC2hAnaVW|-8FgnZ zQ_@70)ZtIJINg8QOZQy%R5GP7LKAkgf{!;^pFp|Ie#g;)fa|?lp)5{3wjWi@Q3OmK zGR3d*Ip>Vq#Tk$^J?VQy4=&EENB2dBb6&&PwkKYGUXnLV|3DBjNkly2%TqPE0=cFqO;Udmkpxxw}+N z9f^Q;BPfs5s2{p%9c8wb>V0VMe4({WFMim%#vN7~{qxQ94ZG&{Rq605Etw)Lr;qde z)oCu3qvde%l*iA^tmQ-uAdk+TH*L#r+@xu9F*YMW!RxlcGm*E%{elsfG%TZq=|QThhCwjTk}Tk{Z-4R_ zR3u4)v%~GR@0=6;uy-cOTlx*GhvImPJoy*yB<{S1+Z#MFiZH!SK8cCQ_ombP%oAhtLv!63?eMAt8K*HZ`Fen zD#LNVO$a?)ZW7|*$;?K5a+Ik6sybc(gg5h6bbcv`clm`}Oz`JuT=WJ%?QV(ve>}|$ z&U8Gp9Alf5r&ICr`hEaHi;fjC19>c?7eyj%&LV}Pk~uDyfKY~u@qO;`&F9~u%6yy8 z6VQnX_@a1kL%Hj%N61O?YdU$ z0nIMb4rN7^a++l<9|{W#tRFPA^i>rVW$KG%C#P0PQc+^P`}Qyq7??*MZnnw&re8H^ zOcqQI)~ZDES)-O2uwg>Z{~Q@KL?m?U>pO6?;F`S0kMtK_9@Y>SDOsfCklUH!xaToWLZ?!8{7sf$ zkG5ip8SI7jcV%Lk|AXYi3`(+e3q~rU#JM0=}06=0(k} z)W7Mzs-f*ZQqH|b*0GZm);Aes{O*_P`hmR1uTQ$~ZTH6;7D_EZRJMFd0gips4Iu5 z^%I@ZL3?AvM2zDz>VA8MR_Cak93d8uZcCt}<>Sl8!v~UY%oA=BczLNIEtr}vUu&R& z4gK=9#6%yy5epW-j--vXzXsLJwhiMCq1fkC(I|_BJGPI>lnJwo#DF11eUIkRY5LUI z&L*ze8tlmk*$G`aKgRy8QNS#lM8q)O_$}^*rC-RKM^dfcLC+O2j?OMzrYTs;r)rJB zlruk(XC9jYMJ5(T&Wu{p-ObD;Sjkt6RMYT;i6|-FCM|qQ;`{dN3lnf18p<9wEWF4TRLRldad{jg0Y0TE091%8^>gMssULZ4Ar^tv}z z-727e>tS<|o!0)WaJ(n8S(bI3o%Vd*4}X%q@cNi=a3G;k(|O=VXeO+t5bt3XrEkgV zQu#ru{&8nESeo<4gZSlu$8xTOJFGU+`D@q;j5VgLYkFIoT3pR}6J<;85OpW=DgCzU zBJyv&3}^ha=2D7|CgMU1OpC&JM6*zV8tZzif$J(|LJrzbta!5#KufNW8UYS5k*;D{%6_3wN_=JBd(Ywc6}xE>Wdb2*wvNq(oVxDNFSipr znq0%>>0QVJcMhlaUqIg7`&BGy@&2L2;^K?a5;S@9w%*DJpdp??wR}F+`wW*if%z}Q z%845uK7^mUY4i2{e24I=>&1dOPJN5PlwESE3XAtE@ectsxmF9R@qPQ#1Y#LU&eh+A z&ApgQ%pQv;F*$D9EULvXQIu=FJ8&}jA)Xp-iTP@AvkH!h)V_Ik6F-0c6xd0JS5XxGtXK#Ax7f{1t8}n|H3~*2?t{5=&TmY#J13IYgrO0 zZaXBT3W3A7*IY}pLrL@v2QpgiEI=|!9UyX1r~KR~y-cLkDtmdj4&7@t6_yq~oH(Tl z-UjW*l(U=KAbQTot#-5P@X|mdBsV)gb)@Ew)tLAZzfssHyzKL^4do^*8j+Oi8BN;# z8Wa8O3`Ke*?P^PCCf@6vRm~pWSC3~b!i!-q*D(Ih z-5%(LuC1!?XZ!o}hxuKW-D9`^u4NUORdSJv&my{^~RNy|u6Uk#$9r|{3@Uv;hrz(<9pj3x=`#hJWo2JtX=-Ud~>%2ZLYzGROT)ozo|7b)c?41hyPe~Fgc#Zm* z(C1-3t^76$1pycjFE%37EDiP#@U(M001JwjJ>!mIFdVLl!#~#@H$4M?j^;1P)pfrW z^%6~6)eV4KM|YtcGah-WT0;e9_n*JseFgjZjf=~3u~Nr}!_8I2IVObYuh6YfB6tTEm66EAnx`YUz0`y}7LbuDyP{8{ZE8{%zqYUCQ!q z`o(btDNBl zqN^Z7V$}J=g*Vx6bp@%`VN@(>Rq$H@@ynldi8L3zuBSSdULRxbwUrbRydDtG_d*fd^YP2184t0h~k>uR>hX-mK}Tf zMi(l6{18c!An|2&5mxOn63S99lo)BYRMvKW^d_}3$9%Ru=wBmO*z_cbb9~1AK&xjP zO9%&yd}dKontm^zj)75PjF+0viUFJ~v;AO88C`F3*p0xFJw-P%oH1H=+iZ-O1bfVN z1oRB)9|OrPcIC#z6G{YVK`THb=O9&c;Z%N70dK7p`}B&07?SPx#8L0*a`dfrS($8U zEIhes&zRRe zFllZPoZ$h0{dyw*Nw7LN1|_V2-3s`YmMA-@s7SzzWq!ys$PmSeArf+J4zBi@prxkZ<@>>RnJz zi4mbv2VKI6zsLV2Igv!Va3`1`+r$sO2ypCNGwog~LQ@teuP9eM>vp~}Kcq{0zLWrY z(Jhw`2jWi$_Vllg5y;3;@1Oa3u!O7*-2Bs=rqHHRp0u!t! z7A{MGNJ6xRh4zX>XT6M6sXK(LVamLYkszCZz-V>R_;2pV1-DiEw}cAmH@!X1t z*Y0!KVXdhwyuLc?^U_y+Vrl6zyNA&xA9MDC(Yw;DNbaTz6DpuADr>I+ zh)MibEntu~M2ia+C#Ik%sI+W)95N#&$!7fg@4_XohC(`|S zY_q_xpo2bV+ibbN?Rj7o>Z$;QtWBwOnSp8*|AzeUgNwR$uVb=W?YVfA6jVitP{|{4 zo}bGtRqk*xTP(N-c<;k!=98MFw9;rvk;}}w zlB7sgO=3inCpzxQWrV)X%36c)__Xi7r{IAXksO}<%_4|qIa--u8 zP*SH3q(QYmi{CCNvsJ5o)SOLVkSLV}Nyi^s);8}%K7z`6#_RYN z@#kU##&ALRQ>Ke0)|*ZH+3Po~;rHf2bTBcC73#bn=%ks@Sit{( zgbaN8e@>dhB>6*d@pfvjCYNpPq3?H!5pj@~k?tID8_%}nBtV5)`oJ)?fu?bDuRDE< z7ho6~Yx%jcvYHa_jDw0N4nb6Cm#Z>X<>>%5td0VzObkr+SRWHYMb4LK1z~{{MTyAO z8Z_u|WdHyP>X8>Dhz{7fSOMt&`;@AqWj%SD0Z0P_04))6aGD&eJECQBg;265>VFE9O2c|b-9O-<%i zoRy+NVYjY9)_Pu{-sWktN22;og&HQJ3UclT0%H09y8tQppy=Ra5FzhZT~*S862L?n zpGCu?J4Vujg@64FPg4{xz74{BDe=ZifPr1bF`__=3p%P~joFfGTc{MUt-ltCuLG14 z9f&JUf3=?=|0~Zf9;#tT@r~yn)byW8*2^ZTyEmdvX)RpmB>?<`ZpbYH2vraeXWqXm z4oUgHk~JTsis8hO5r`-{xRFHr)ooF)-MU(*v*Wx{3YOcPg8BbRHKu0 zK%b$cx%*ysg4r_|Xi^^6CqAVSGKY7 z@hGu;R_?~P(N3My4#x`+Qf^6I%cp9qXA#`swAL|4#zC#x@)@hntXa*6=dQ*t!!d8~ z=?-0Mz9hf0v);Dt8=^b%;z@}L446G<%&BEX0cK`?UiyuW;hbyae`R%w0V8RxJtSJM z4VN}a3L({Vl<)Jc_V>ubhk1pEJvTMO7|^JIm=lcb*II9O@bdDudp0*UJ?->INJ>fq zGn5>|`Z~0yeCb7VBcAcoQ!;JmSFO179#TXpN2S!F^Ev-TQ@Be__o<%};~3dcQ{6a! z55*k>T6Z@>o(W3oK@}BK*)#^T72|4bWuYvFfAMG4flBSwYPY#pFBq4Sl9QVOx3=`d z0ry350g3;jx0^$6a}KYp`To?FqaQyg&D7@J-{4hL6K@wDitNgdK#QFO{QJYf0cxD| zY&V)Or_nsX2NsGZSN5gh5n3ckB)%yQv{1==K-p{#Fdr>d{g0i)p@^Gw+#QJWY+OvI zpWh8rtI(RyTiO@)!!#qV!X>Dy;#*^?`J`_TwhZv9$wXqizZ7w zVc}0DM06w;`j(V&VUCy>8LfwE6B~ve7zr7r%jD`jZ-Alxc3a((%-!FUoHWs*joVlf3%W z_bD@q!)CkLY;H@eSMeYik6M_;key6wGF7kKQ?&SOb>V1j!Bfk`?arT)IbqQ-mR^w2 zEYJ)YEstltTr_=;@9u(SW-SYkho;Xo2P`Rtr;uyQJv@}D1@I|JX=#Vgo=l35uA>W{ z#OXESAF-0_UyJmgl_s8D(t_(f~x!zr2Z2Qq7stW!NN`W zsY;Nbl11UyJAcr;tiIbM)mB`DUR;*i>|y>f304GkS2w>_Bpt||@vic1fEYHN|JW~$z5 zX>!%K9i?^EnKn?q1v4D0HAlii^O?|(pvCvJ!FqYF&)jD#CHcSvh7E>UT3 zEDhyZLCA`WUAbw@8`JlZn-nPQ+Hmca<`~PC`0AoTzud=<*TYjt^*9s&-!Hju+XS5E z@IEyUa`;|g{!6Qk0R>9OLDcLI_*|OZ`@s0<-Bk8IjU!rw$ThcRWX-Q=^^Y(7D&G{? z2SrbwQcWf1T3{}~NePdz&OqGyyATPNu>2Q?rL?eWPdPqa1!j;bp}Nk0)P5j?hABQi zer9%dvEoN@*=nC07D>OJk@>`$3)Q$!6-bfL%P%E9PnzJ;P?1ZUeuw6Il?0(#(;lJz z*Wi(hi`A4stvyKC|57(IrKF^U#^)FxN(*_D(P(h!a@~fKjBMMGGHMK+_MIk?D^1+M z^5h|?7ki`tzywT+3zdj%4R#A;<+WR)Jk~ekV&mT%cPr?|Jj~WV6#9C3zNc9nA7E`+ z6(1e-$abROx@ILk0}_cv;DxytkH45LjXn;Ef9=PaSl$tDMMuR=8P04rn&oi3*@TYP zksqer-q~4vet1OEJQTy)8_4FtbY3yloUf^AO&4Zhdd}lf8I#Ya*^$4!23&#tt)Vpl2;(Ut=-Isnwy)ML!$m~T@>-Hb zdU!z;!JLxZkx$YHle9E<2kworYuk_dZ>x!Q#at^|{JyXA4^dWM*)$U|$A1Ub#OU~2tuz%oQx-uQ zQTR`UGH|lR1tPB-q*ob@6GNq0*mRS0O2$9SOK7kyPsRRhI@^m_eO%w6Vw+`jb@YT> zFUiTTWks8m;`m&Bpgvn13}vpe-bBSqnCM2y5<3*RTy2rf5vW7oQ)JlZQ?u&pyN6Mc&9Mz&c&<>8P&!v z!yv_cIJ+A5MUM~0L)P#Tjv|1pggrcJIyyOJILyr!%m0v9y6#OCPpbN%&be5bvdS?m zPHEsuH(-Hf7@^BuBih)TaIbTAf^Q@|Gna_If19D`t}JB%PvVr)-FC(NIBFSX%)|dM z#;c<)KK_EMk2tFxLHDZ|ZfoK1JA&MO+aF#t(Ie!-AVZBHkHU9xyhLO_{Eq+VtTH#B zS%n`w`l1L3=aek_Kso_E%1qqbA5MQE5tgn+a(g|xsJ0kPmi^)Ov^=WkpPrVk*}iOj z5FM-YW1$KjMg#xIglaP(^Lx3^fWA6IyBc&*CP%|SQg&K;G_mpC&Ms?@%|O#n_+{I^ zx@yDRwvtd0#Jr_vnv7jugXqt-ZTMg0n=IzY=V9VEFGb54?2PSKfLtVxIv1EiH-l!Dp9etf+bS430~dHj@obpaK?0*U6eeYAjQ=*n^7$uCJ)S9IXXoXmiqrGg=&PE7T0wdX zmpTtRoYfM_yZ>Y?0K^JF{OA`h`@NZZ?B$x$>TO_vQC{Kc7SKYZL}4h15tauRwFO1d z;Sysz3wKi`NgYnwFU%Jg zmq^V@qfR#Ihl9OCD&?&S7)Qy+T`X=b3<}mR5cQKD=;x6$AowvhF`D33GTd56xiym! z&xxl^@uqJpgX$d*syfEqQ z+kLbP8}fXmxM#;VPBlt?F0_V|x9Iw%! z@RpBJm}cCx*7lFN&mcM~a!&lH2Aapw7nkoVS?=K6DsP)w6g8f6Qr<6b@)&MPW!?fnd{?NrGH)Y3hHs^ zA<4E^tpAVCjc_H4F$?Ecu<6m9EwMa9K_b2h(~N-;4pfws=>W;EEJ?E~#^lkoH7qzPH9=~ z+if$QTy$1Vr3~iFUQR0npY*#b2_{zm`l3BH2}bwd9}gEMg8Je&({xKHPl+zk^YLx3 z2+ij+By{|uw%a~~m-yv)`mQ`wb~GvvFB1pK06Y`DJWuQfW>!DfHZ z>4rkKtCxBDJkU|Jx=9j{#SE~uTB8YVdKsKEc=kr3o5rRH!Dns8kHF+~)o|Ht2a6o4 zUXNikx;LS8=eG|`hvB-c#W)yH(l{qZX}+`E+D@e-RAb8YnV1z~EkT@JH@=z!A49H$ zA`Fp`kgy3m3l@k?{-5j>0A4yWGQ5h5{&p1BhV%j~Y+;UN3Ms0dm%9`K%GPfqIH%M~ zQ&Rl&4m+vl?Xe;L$~7~|q1%E{O^FgLe36PP!M>=HugN;^%&$=;1pJ-?lX3c)KC5@K z_CK>I4wPwUU~FQuOttm3%uhptVn%O=#t=ck-SrpSY`yPnvZgjGI6&&*Oh;B>UYaWZ ztZXG_N?Pjgd^$}=vk*nQDH&k*F=A^{k{w?o(2ix4xsh(!@fF4fpgB@Rv}J~bor}iJ zJ3-unj0`W@eZhI}?vkGFek~Pa59n(lR;$4y8nR@51*uyVk44v|L#hMAi%yhNrjta4 z-T8w2zj^pQBGA3_IaA3rOjX77pU`8a-7{R=xsWDF5cst=Um}eTYvE3rmx^j=`Ficd ze@FQaQA71Ne2dfHlXrA3c(jucT4=v_H?@?-uJzaF#5RCyR3}N~laANrCX#)EDOQiz z8{^T5t;`fiOXu{@wX}qRXd>sWltdO#u(+jhE0vz|B)OE2cv@kSIh6>w;fF~*33SzE zWiF69NL6piB|;Z=^>r(o30dCd$@i%yF5Q(>cj?%XQ z4_J(k$xMu@J*y&KQ!*wl=CsPxC!okZ1>39?D9g>9{rGooa6kydy}SYhq4w-fQbbs%YX zQpCll!q|m492Qx`xdKwB;(z&nibj+P0IJ^%dwt7OSJFgrOHHjG!Bybpo*BYA!wlK{ zyqxyRlHT#-2UV@6`J$uU3l~!$s7YXd&yp?(;*?;Fl8n+H?Y48}?I7m-a2mg9-rTpt zsv$&}!~&W6>7O>~zwmtUALEuNHebOF%bZfq<9iVxL6XgD(lflZ`D5PrSi_|-Pwukw z(M*TExN<=a#o@Wy2rIeJl!!?*NG!+;S*4z}t)>ye>ZncARJ6VRbeq<_3B*4d5;!oU z1^FK{B88)ZfstapfrIDw`4gDbE!p=jg0_353+c3jD+U$ZE+fZDq}O=XUR(*TlSiN2 z(SN6smix5(z#Y8@twZVq|Eo~*-+@)P@RK{6=_!v@5}&jVHRy{BP$f*?lw9&)QNs; zeTcG*bFW?PA6lw}(Atdwy@C>ujWRPOY7)eH8n&p=xy?F?%hgveUWjBz6qd-K50Pgf z-~uWD-P&D<-*pDbML1D^hMV}Yf*jhii!N(tST!T@gYr`ks5o%($pmI zC9h)rEe7j2Opm0IevT(8P6ejy0ZpbnR|-^MxfuaQJ%^Hs)xc0T-4hrDKuZ$i zXJ{qi8~6D|uWw~wu&itTRH^D46;YgjX*{<`Uos}TdSW65&m=yNZBs10%;%-#B&eD( zfT|Cp1i-%Hrt@)gusdsXhB-!1Ef>|ZrK*Y?Mgq6MNU))sY>b|KSk(DucQPg5i*f0C z(%*fM&7yig;Q(2+9=YzarQtv+-f(uZ_muPrhEEG6@>*O*gsd>U{z~6l3HL?1#e*a5 zNzS|DRb!yis7B{t)O$Q-e0a%4#Y0U}MXIU`a-f#{AhV@!bYiitcEJfBE+;P+kkmDl zqrYp)k79RQmQ|OMyPEZ+%3ydjZ#SaiI2DN9v&9(S>4rj1TlR`-4rFF91MM>V=_+uj zNV=!K9?@noH9IvkT@2j&0?0BozOW=4I^=e3?+ zb!^n^4`8E>$YV^zNbz}4xt-U+z(Uph zig)}ZQ37s_9B^PH!;HDY_Sf<$j2hx)Q5j7V1TA^A&bJB*QBe;FdcR*T!D5Th!x1oY zb@w^?cgLI#guy{?qrYdd7k1n5(`<;ceMq15zI;-e;L8?CE z(C0;K+dpO)PmO9ctZ>R;iUz`-UUD_%jDFMQREHF9E?sURTUX^m6Wueq+5l6=|Z1xZ(Me{kEnoX^UPqQV$ScrtlDKcY!vGtm6<}jLo6V|87|BQ2 z<{5X4S&z~JW?ASYO(y{WIF2;27Snf`kaNwEr@<)9#!z;J(RPAz5yagt~7d?I|vk)6j z9J}hdYG2}j(QsM)h0EGlnRYDfnI9FVf1ui;?v6CP<#xT3l@JXz>e7efpWPH7`d9r* zmw%A{StWA(6Kq*>Y%F2|NSL;1xJ_N*<4>JdApA^MwSBwDEjzGkgWqOstp3|(oN$9G zIcWnB3~u;gqw$}5`Ju+mVH73TVOy{F7A@WjWAR|hCP{s(Y)J-ujfoZ`dv7-Kt%K`n z5vt?A^eB_k)GV zD=3Fa!a$9r4P#at-IVclMss+sOwP>gp9-KH+Wpoi(lb^zSZPX(3wsz_PCn_%*28Qx zDO#I7We=KQz`L>9a5p#XEFD$;izm%=5z^>SMDgq(&4d0>c8_w8kevfE{mb^#?w%&WBYGSq=P3?>D3#u@?6nUIGQ zrJ?X5!X}iBm&8Ai7UKwP5AI%0qcTt(&Mak7$Ot-iX^z5`qj`NOA&U(-pwrx+qZQ4| z+;&%8a`4nmd3z(A$k?zv78eD>Y%QV-|CmgJ#WVx;;5-~%#YIVV5;fsAr~PWZsZP4R zO*jd_WqZuBEaWOxR4MuBM)nxgbW#qmEe_%JzhQukp~T75*UB1CU9P5 z4g37Khr=x3`K|G2;GCTP3m5_YHGUB4LrdX*4np?~|J=E9phoOvkf8RJeQO#gDq)PZ zbxTa3i{cCf_T@3(=4zO?98q_g4t7&(#BCO;8M0pill$_^+G-L*{FtD-W0`}T1<2!X z-WXYU{e={)Y&HkDpRnP&G&^I9ntcQj1~MNr8KoMCTsT&>bJJSL1E?!1F$2@c9uAV2R`2g+ofhHwLYGWF$)3!BNdr6c#NVA6(KIP_ zGB1y1m_zJl+1hAteV2oi7<7_`L)~6$>}jJ71x2~OzW4V>-M30oS?n(!loO=S686s3 zK_Q4lj}JGOI!-kP+@HgBS>JPX3vH7ecZrq8y!qS`t2hYOvv;;k^-g@qGc5 zM7P=s8d6<&R*naoztaN2-YWJyndPB^fN*hx=#%v)%zan~upfFwIEyShQdH@;AKXA7xN*fhu(&k0LT=1(WD+=Wc?T5q+DhRlMXd~_;m)! z40bmqSJxE+0UxYU#`6$t#ET3V)-NCuKfQ}&Ueo@pygn9%g*;Xqj3MB~tnukBVpv<3 z=UXq2bxE;0RGUruxv?3Yoiyh2DS^*zcIp-dNf!|W#l0BGX0Ws@sWj8?-1@;mEm=Yl z*^jEh&2%EGMxMf>mK|0r8WkwgfNebPe#x0+YZ;8;^p&G3hODbR6HjmTIkN!WI`sCG zHMkZnIlh1++1PZk%9L4obE;f^yo9)h4-KR9p0&RWd1n5UkfZ;PYdJ3#vk_D4)%iLJ zv#<-n;XBp--uBp{aCHj~AnBG*p*`Hax}mm|wr4p0mf=g-tkLb5c7^3#Iwv+}(cVJm zHGb7OT?AX7oEqKia5~l~YKLotBbtyfV(_msB-rLy3&z#SxM=?Y@ONNG9GI!OGO415 z?TviY5*Nj=)g4VIfv>t25KD^g8#uSEu7%adY$B^?$s}nIebDaQdf4)7d__7R0j@^z zje`sGPP;$Ex)7`&JkM-}_T_WgAJ$P!?kIhf`utrUb*i_lbg@Pu0BDeAo0QXO&#pfI z)Gk}4$s58bgY-MQN+!gt^%2$p4T^@)9!Q-(9l#Z=`=?o+s5L)0Jwxs}CSKFolC3C> zB~+x*G@flFE|mFIP&_Vq=Q?e$bjTQEW%co%M7VyoRLy=EzJjY#*+y{f?uh!@0}i@?D8fen zRri@t_G-XXan{`kLq#nXr~Oom82;u~X&HxzDD8s`1E&nXizMOYAyg}!D{T~QWCl2J zo%ZptM+s*pqS4iYo23oVFg4qrxKuCAhNLB@RJS%35n4;+8TXwa1nAJh}#%yAffdQjNkldtD zULR56ANdg(H1l__V;eKm!39E31!*6kqz4LPT==qqbhb4ry9V5LBAb*XN?%2|jfQym zQ~FA9GSuhW@@r^rj5j`}vRuPAPasO~2~_M2dtnDw_|=c8li&R~8J!PlptRY5iyH_0 zo?6_G-+dXE4Wbc(2J};#C+?lBUlOY=;fsRmH@yO7&+v}FU*mD#2#?v)z4s353KxH& zrZNe)R4Z#rnkVQAiq&}M^z8^gUpDBLyd-#>h9DG@)*0IVW_;tB)r$hKVm&G6`SXLWWQ+uKKSgDzjj#B>Z8MsnhgrAL$nosZGR=!WPC5W#^HT`W%KAk#c*>K$SOh+PAYp?>~$e|KR z;T}|E6rg*00qFC^urV{2q{6DsBj&|QC1vCUzW^n{Qfd*hyk9eL7Y;}t)Ano@uizmv zzfPO)oTXdX-M8zAx?O~Bh5HZ%kPmiVRQ1&Kw{$GHs9dnQ-^2(^nKeSh7^aAc6tP^7Y6Dz82tkBB;KYh3U7 z8Ha(ERk^LfD?J(#Kd*0{pMsO2sT|Mn-iaQacMB6j%feE{ruQrDQPWz|MH*h!(Na6u z6O9F`r5)dw{h^*f&KhrVr6CO5D0PD1?kM(1+rS9bX<0j%vi2n+s#j{L_`hOV609#mOMN{zIC9CGX9 zFnx+SLGxWUH;Rt=Ko>3hnG6bSixo*xchI>Gcx+g_wHNxG1upqJU*-#{IMbwnu>cDI zAmYuH#5K7O9eUNWl+ikgR8z<5k{^cy?CeNtMaHsB=Bjv3b|x z#<^I>${c2Kd3Y#c7c|o6cts3@S^pYfxxvmd_@j6@$&A?zwgnycE<+(oNQ~syboshZ z)+u2xVE0T07Iq~gS9Fj8;S0A{yQdc-4aiU-DZM}5}$oW7ypBOi8OtfX`#ftMd zNt!tM&WR-ec!5|>j*h-B$Hl?3Z>tb#ME(w+FCsQ5r?LjCsX6 zS$aP1CmJn_w{2cZCN?`I?j{W*(bu<4&csroSOo^ou1_jgdK)b4wIjsVulDj8OQ`G& zfL>9;0kl9O;iD0!n2bO0$7*!8Y_xW}{?OaRFzD_t5>kmhH=*55lZ@Q7PW~1S;Y&Zb zJDx35tFWxRj`O_fa*`fpT2Lm4-{J|YoSmDl^V(+g;3(2?xlUV=pYF^*JLs?9ITc5t=6P%IM(S}WxMXpe5zvY68RKvlVFBn3?7y0z;?>&M&_o-V zpqmj==A*E_MrEBStYS!K`h6uZ>rW zh$~IP4&LCsF3QW)~3D!!gm!H4_bO8pu;u}D>bXW=Z@-m|3AqijOQB#xC2DC3_=z7;1K98Lh8 z-UtAkl=@BV3lw{F1Bt;Y(B?r33>)s01_#A;T4{k zaaP46^($mF)$e_}f~QssF+0vD&_(Qie0ufSsA-Xj_jfIvM-cjH?kmmuqLAQ! z9IT!nUbG=I(5z#Y-wNAl@*!OAudP3ipKLivHV<{JIDN!)R^s2cXb2uWM}cO@ixjFc zjb)luAxvG4sUxdUHd28+F$lSK#B#pKvSa=BmkCsVbJj@gF>7|%6|=n<6#rRRTs*22 zjmKG&*1y+R{Zgg-)LgV?It06UZ58{|tt-Ade7^ij2a7;+{FMAMlJua~F#f$mYp1l7 zf#jz0oExw7HL3WgHRt)+w8%TBG?`i}foLPpiwS?pGzg+}Z1Y?^00?!1NocLPtOA59TE4eSxz?L-Dy6JvlrgB>#pa@`Sw(se4 zEbL#|c+a+lKHoDjqPEB$+;jo~$ZmhJ8{Z>FK1UFFomIaUOGm6w3tcXV%VdoJm+@+* zd!BgB@)3R`*ZXjohT{@`(2%XJ`8q3m?QTK&i&*{q)ug0!Btf~% zN1bM-v9*1;p+$$%-EqjylCRfp^!gGT%4gGw^+^-5#tOI2AvVp@cPY646w9W;t8P#> z*{ZvFlANZK*?IQk$L>q+SV_YLsZ5Uv`cribaz8y`PKbLZEP=QMHM$WQ2ysvx=AvF8JtAp}Un1^PFfDx@5y zXZr~FBi&{eY@(u~C@3gVGBnbh`duJ8>!!S~Wm;TIi|#lYZqnfOOsy-bM$+#ihoU7p zKMhsQVx>FNd108H9j2F)V)r?x(aqM!s?%6h8wZBEoI_;sQ|*)cYn5c`0x~#SQuW8D z`{(O$c(+_UO171pt%L8&o`*vWZTkymf;HN8>uZ!Eh7XBvAN3m4klk1)>K~+x+IdnU zSxd#mb+y|7()Gi691i5-O7@PW5z@6Sb-)cA6TbnETYOhb%L-rRsG^o!yw}1-?+Cb% z)$w)a8K~()AA<9JZl7^BW^>y1ntfj9@m-%Yx#`y=hd)C zE2^X2kxuB>2-8nnjhpNH3A34f-jkm^7MEzzp+pxV6@HtSDq4EQF8N+B5?0YKuGpCp zruP_Pd|$Pc7wwZW$&5m}{_sCHY0w|kV~t2ITNvZscD$A&b36@XlD|OApWyRyZ!T^_ zzYX#`!dSz3xIN9sT^kKsyM`XUculO}yI~aZQM=Uhsy#f|8cOowp@batF6gmGzs<87~W#%MoE&vu#Lr`?= zyf}SP2~C(tXxobtEaN=x@ir&$?2Q@vKvw#4`OGioc`(3*i98QfKlnqt1YPZOi$8TH zPI5l!)!che@>c4WmL+aQxTmR)xgDp$r$`5)Sv<&&S4f+neoGZ^w`*!KVnA%m?4pP~ zL;(A9WZLQaP>1NtI=n(st`v#i@$ot|`XK%K20T;^0hbEld>dMIO?SKG^mTxNp2wWr zTpqu3F3uqowesFFIcFFtZjjEvSb5!}o4N_s;@3T)1CKdF4ia|~ImEZ3P z@8oyB6W_;Uy>g5YFUy$O!Yo=&f1Q3XaPGv9fD^-so1mQ_9fATJUAYl5V-3^$b#Lyb zGy=lS3~gL9!JJwO~G0%P%&j<3tE$GE$x{V_Sh}kco@OhU)mRX^|01ePx={JSY@n6NJ z7_i0lp<_E}L&b%Sh8B|<4(+PIqSLdBkGPx&gJ(xS2MhZ)Ud+v572YOSjZf5HjjzSI zIrA>Dj=v_7d45k1mvc={yJX}#k*eu`vsba5wAr~z{e1l%;vPl7$7=jqiFGaq*ezygFz9vzlOx}fj3ub)Bo7Kwt?Syy5w>r$ZCVBL4qmSpb}Fip%1Zfq z|Ew}IYh#8%ugOjyzk?gn4#-WA-c(i1W@7mOq(6JGa02P6jVI~X% z#vHnszwZP1n?=QrHBRW3o%3m7iXG=~6S*sq@DU|juBZG?r~T%k`MF1^>S;!!F&_U| zeG>}`b`*CF`pertndlAYJA~%p6VrdRSF(gC_Epu?smR*VrC95o4>TQdOuFJyw0^&< zsu2cJ{Uayx_e^m9DO`tX9qaJM?|Z_f`Y#`CUfSCCHia_~8}Ds_R}(^r5HEk9*(7D| ziwC4uV04Pu-MQ3EZZb?N=W_K|RtbRIr{<`IHX6p^!gv7JUF`jR);@{DD$}^;$~U?h zS1Z0bAtl|1(*uwBh{;cQRNSM~k{4RoeS1p51nbDjPuy!mugT-l-q0}DGV9D)RzgjG zsI9lxof*q>G^q;j%mDka-f+L2+kWrdf^EB3pTURU)&cK`&m|yBfGx<`NP$HER12>g zlYVx7Ray!+o;Q@U*2(42M1To}=T6${N!~eq(PhYC2n-4|);YV%Z@b&O-pCv44q3e= zYx(9UqQGFG*)vlb$+7J2=5mwi@2PyHwzS64UmC|#3XYl z5{ZL9ef}`^% z`Q9E3zJH$=Q@T6Qds=p5Ezz(QAQW{GMDb+6vTWPRjHi^AdI?Ac+_$%(ZuK|A?T&AJeDW5z)H;p~) zE!5?p#6Cc!|5kq=+nZ5Hjz{qmP-<5{PN}tR@lpk{Bm|YrVu;ScNr%s1%@{8FoIfX! zu&|az0UyMP=>VV&+^l>yiqi<43=Rlwi`vU&Bsjt zH3ic|Q2}A(hfFgkaG23TUMq_Ieif)2(&SFZq3Vwg# z0P*ttW-_U58MMa%h{)%WJ(=?x^QP1!$D7d&c*Px?wLC>-vItBTgX+ZcdNg6Ke`?ND zCE#G0CEVZmQNjMoS5FU$`)QJWi@IY?3^;0T|Lk-^{JR?DdnJ0TUv@0f(b3_Uv^Q{c z+J;(9_Dd`7=B^?}JK?E_Dmm5mSB5Jrvvc`w;mNFerOjBl|21zZK*4?bS@NnN5J|N7 zAE{4Z&EU)7;vacOT1=ZEsDG)0HFJ}Cx(kvpr|*|C7ygPE0h`&B|2n5U;knXkKqn$f zoNv6=p7<+tr^3GKWX@rFQQ<6uqr}$$8jQanK$uVgl^>8e$wMOox}l%<990Y$lphWV zKjGQ?OBU_{#vU?Q49bbclmhx&-qrizb~##a%Wu~{8(x^_XP-%dF(<%3Erf%ARk95^ zCBt4XINgnxqfcH8y`TZm?Ki5=`i&`ZadC1StvYnu_^g(>4Ug_(-~Magiu|uA3y?^; zID$^$Qpb#|RKI?`luPG!eY`SK!xSS%8mUJP7XIuv)#iS`-saB5$+^4)8%NX7qK<6g zph@oE=xcg@(E8(YO9_t5fY{BwbrtpBy@S|b2PCPe|MXfvC_CALP8S7;fWhaZ$ukYs zYg@MLx1{706s~VCu9lW3U4CEY=H>!m9a4DLOm+sNi4|Mgz8&`V|1q;b7y5g#tn7>R z1Uj<--iR6Y525I4z=!^?SQ*#?fZ{+YE&%$XVx(`ie&@g5>gupip4sAZ&iWP=5;9B$ zWodN^4xZ4|;$AD|M6pRv6?E5cU~vSFzkCt?@o%RV9C!&}G!UbH(0}E|z($&HyV_n~ zUA?@%dU~3$4H*_mDI!^HcKcuNjk&+!7$^9c+F;ScThq9?xn;(`;ng{pNuf<#a9+roa3UT*wmM5Gl||DHZWwro0NzughUfu62dL|FVo zO_es=G^u4&YiBh)!;?$X`G>!B>*nN8?3(le!e1dYKlES54dp)puNaoo^jARo%Vr>K zSzMEHi1W!}R#8!v?dHCfyw+ctB5>)OrMlWSyz=nTva}Y7vE=iO_?0G|Ugv1^nrF=% zrl+TyOr%@&R~c|~8wCRAKpB)BHjr1YDX4sQ9 z!fC42j*DZTtdJH0jmS5qA3pDd4j^UcbKS{~ci1x?W!x$Q!JccYy^1R);XW7H0G!a{ z`qjLfqdpZ@ozD@Igaoa%!kIn@e1~q#8*iabezS;M32}r@u6tNK0bw;M)|nBFLk0K3 z{q2>Q8y}4Ze(_FTJ$5&I3Fv6V@qm-j-dTmc#1G7PfXD9mkyb~_w>`a%96gFUV_Eq3 zn49AMU)2`o?B~h6pLRn07CgJYe_8u=`(2(7^oQQ>&pK-~#7NOyx*n40DJdzGl)25Y zr823kZvdGTU?hNgYR9-6%h+Z@sP{pEni}T6q6-R4n2gOB7!Ef#8PK+hiAtu=ELNoS z*mOs`{95+&@-Gqf$@unG*kO(JL&p4gYea`6mh^M;3I@F0*e*pNBIuoPZ(UwdekHR{ zBymSQdLg2{x|1W7xUfx5GB&WNlxZKz($N(m5XDWn$?M%F=VVdC<}(eh=Q5v~eVx`n ziYFc59o`N_%+D*MeGXI6J5qqgCnc^if;wDQpWDNWH`C9g*F4zcX}P2_oJJU@-8sHJ zC`Z^>6RyaQ~5zp9vp2BGEU zt(pF{rtBiJiSNuG#X-Q?j7EH*2( zMt$jJPM|6Ca`Sn+O~p7h!YNHohXCnPk9lNjN&|P3mps7l50jvl0lEC z3jEWJBnx+gPi`MINh7MK`mvPiOCg6-?+T#QcC|pL@u;mkOf8J&_Q-^F)*iECR^=7W zCuI)qe8R7X4I;*u2v25VwTlL~#ITnu(YbP(d}eS4_oZ1Q^L zj!GSm%zicoBD)uJNre7W^nbDf_$4YsTd z?;B{Zz(nL9lZraK?S6DRZ4d4T8MoSsA@Hen#$sYr)YXNN)@jMFpfum8W~_J(bsr+* zx@!e(MFAz1*!y4oNuF!@_fFgUYn-kps=wFm(bc626;+fLD$`m$vWHg6zsz+y*G8Tm zgqSSI2Bc@v+&jpN9RVj>gTA+onB;0Q81fXPoqbJqb~vWKv*8zl@P3w+QQ}R0aK_k0 z`0$YKxmGu*{4o)e_KFV5X^A5l2Ic}=*uN1R;HSF(NsthQ{OVu>VY7yw3J6RUT5vzy zIVvMmkzSCyKcvtIO$$b7%j|KMFK^q3Z5eCxIG;Wojyc1hGyPe0|3&h?Y z4eK^c;z*n8^&X%msj04pf`&HIFvZA|d-w7?`2vf>Ug(L>PO#j<*i%Ycul#+kO`QT1UAfrUy?oYuCd!**UJ6TraG zy3>&=QCLv{pg8DjKuzcbqu|tXtnj`aYKXt39P&$RP(b|6+=iR$ZuMiz%7ah1Cvh=i zUoAHO>+w^X08<*Qk^i>Qi6o&>ADh3egmM$fb4k#CwVM!s?ty zf725SAy%x-eFrvP^5~xCcLu`BH)hpxkE>E#^Y{||5{^CT#)b4u zkD}u0tS%#>HaSP!pGu?BTv_3qoiV63T`dP~9D8(LEwx?rMp~WY%53^)2~5y(b(UKE z^cwqCZhA1)pPyQ)ShYq%*?-IdL!Pb+kQ#%mnR?)*(ZD>=zyNkMt~Tg+%+LMePuKA9 zunTL9IAw8h@wsJfWu@sid{2n;M;1dEqK+B`^4|N?m1pnwdy7zZ>O?x71RmF6v?J^N zOu{%k*bPLq^H9Z(>Xx_5Z!Dkq?)r{{E!hUmY& z=n~iVs_X7wn+rc$eJW@x#&t2VV?h#(Z^D z;p*#mNv0gRb&oDF4AESSMY$wPaw)UZ6UXz>w+!SttJZ{O=xV0vxi6a^lU0d8a~{a_ zTE?^)pVT zMx())_-D#U_$OnrlbN7v3GELRfh8$2Ivrl%$X`{vsOp7L`$Y>LmKPV8hbq^Z>Md{d zLmY@$OF5pHnK)TzRxmoAvS3b!Seaf|W9o%@cuTt`UWimq;{Jdi0h?bsdDLLcsS&~K}=Zl<>}a_+3EN$m~Gc<{MLzZ)vQpSdJO+?@{A({{2d-=e;FlIW^2!= zN+Engkd5iPDgg?UlF%=9O=kl5EUXNXk_LYd{Gq{SK4hLTD$jVb(m!7GLheMu4cA(K zoV_)aweh|vP#aUGZ!oaV6{YC(gDc_9Xr)xX>tzg(G_{YIPN5qxWEf zr>)w=4_i00o-^}qdxxVN3Uu-OCS=XfVvM_=@V&Dn>Sx5?sU&|z7AsSrIwX|}?IIWV zjdst~H6C7`cfFo~rIi&jQquO0m>>PLQ!F%xNy+CLFY|yLb1F` zrlf>~(e}u=9n!VGXVPNP(lV*K4Np!RIqD-0l`ItsM{2S4{dBc%jC!rD^cQX7r8tkYIo38f z*mtoznpOUY!$v%DEgb4=FhfTVflP6@2&f*E$)Ommcj{)r$%xNFxZcSIA9yJyv*bB5 z@(K$NMw6Mi@ghZYTwPr;v9WtZj)MNpR9Jv4l#b4%>TF>FiO_QK!Y?7=J2baLanb9| z%mgy&kIlw|G%bLzM@>GsuwXMjnL9W*D0*k;S^~NuJRXi8SzlKoC|~A}zka~14x~^r z5Q1OWl!dz^4frhi(-c#rChUuVtZ6=sgFNy>d=5 z_H^f}$f1oSu9lDR5{@+zZ@jO3y2eq|+@y=b_~Np>2pAYv;+!>=>^Nn+{u=)fYHkTo3MjzDhH2)gXu^C70A-5@x}h3a zk{1jm5BjF~3GkbPQ10|4WTnnjtC#z>(BHde!E9Bz3JfUYWMnjR#k+)nVFf%0udu)wYr;^mw>*$ zKF(-Gm!hJqxyh_=_h50h z@?`cAd+L%xB1o9Y&OYtWsdW$?K9sNn&UR;5Gto^XM$3=!i_!xx--0G-Tpv8`H?rhl zv((10+iUv5GJeq?oki@MC_?GTr-i~h3{x!TBd0gmK3(<4ZDE}=;6F2V$q;M3rDbG* zQLtEO-!J2-DJUeOvh(st)}OBP_C(Sf;}#|sDuj|v{QI`%&kQ&70@jC*`5KW)?NFKQ zvC8MLep5STeo`K4qBQI6dcEK9mLm4lvUhg2Uj2A`x^nN%7y5H&XNTAGw^x1b7kIo` zu4$;yzOZ1|^%fWW$`Vp}66ZSGM$In#*gmLsyJu@*;?$n1nzPl0hBm)4ShZkcbgzUC z!uf;>XVwEE3oo?B(0?s(Ksgugrt4V+(s(+)imVNW-)+yfBY$`3L!l5ig{9}&Kj97i z-jH(j4?UW|Pu{BDij$&QaawKoZTC(aZ+dW2v$|9e;F;8Wr|m`5R-C6k@s*QL?8Sa> zOQQk2Xne$vvJC@?>-iz$(uWN9o6F#@3=&$}Ghj3WDJd!N@ld0c0LdnW*gJy&>2JL7$nT-?`;j)N4p*t zLw)VWdSD}p7#c4j=?M%=ocoUIb)%3VsAw^aD8YyU*fjxOX#mmORRNSN2ZI+mk+^u> zFCJUjCsUTeVD=NytWT@oy9sG)!2SoxiR%$1`a&E4*5-CsNSDMWAe z1pd+{>4~SgzTt##RFvknYiaTanxb%F(nud>lZ+!1A&W*2&?CAkm&7tOHkOcHn4Gj+ z{a6I-ob<%V^Y_SUprN*iz@Ssp;j6`xS8QI6#^8TKm=&30sjT1bcUul z45YZ=vNCV?jMXEP#lxXYS2U{|Q8Iez_r{h(=S$Ba!4f?^b-thQ8GF&H`{u9CbP!c$ zF_A&(Y&R7xIBS6%u#Iy97|*q3m{U0i5;V0@!uY@qF@WO;D%_k=6c7*qKC{Xwkl)Xz zxC8*rAu!BVu7wv#;6ng-Gs@J zBtEp3{_J_dU$jTEBVPY0Z7c-e6U|UGL=xlpScP^P{n;|5CDwmJ>=B}x97cH#5!e7NM0viZX!-p_n{k1RV#qx*G}G747asgW1O4wgD%Q!96v`23xo08uADhKP zjcf4$GjiO=`iPQbj%+PE$B8|%d|mmcv>`tW&7%?mEO~F?S#8kpYG){R1b``o|P&)PWGTGIW{joYPA4J z3nW1VR~{0wBX5PzJBfYIkI&nhm{gvtoDniVKW{Cbi01j~Tp()=(PAzMs&o8Cs4tlE z(0jj;vSIm~IG>+yKAk@g6=JWWw*LDgq3B~oYeD59iqk04y8n(sVV(FgRB7z}LZw=>)| z+D}_F=6lvkV+z{Cp@rBVTu^cQ-JIsYKYlVq1>On^Lo~OAs`Th}KhNa#ehjqbntB0V zUY*54Ie`}%ux$E*eBta^k>Ffa4!EQ*vZU4U>23gARXO*{#wUc^q;`34nPNB%4N!oS5VDuqzs!w~|QA@VAn zN+%Hd>}>m#(-$7!UY~bIe7WNb2#fIzjqDi`Lod|J=6Jmyl!Zun8|vpcadMAM$<9#U ze=?!dz2Qb%an*NN$B&tR)>^!WO8HhC=AJBbzJA?&4M`iRj zpeXyk!MpY2VBOp*;lZodfPG7SapRNH)YN>;B)B}n_kKY{LLz+d2LAfs%k3+}r@~xe zUfa{Upn{L8;W956WVt5xz82q-LB;AIRYO^VWM4y-zgk+a&c@3Z)GbQ;o5OmxaH{9` za-x%}VU;SXiy)v+860y9Fb(3S`{%Z65DJ7dH%AJ9vtwT+5`$x#SBSvp)0Xdi;Y|}K z1`5-2Ii0+oPf^Fu4yY%l1~afH#b(OEY1G0nIZ;DcN6L|1<*DCkq2PI&jOJqAJaGmk zJ1l)0;5fgxkcu&5tBiDSkK3aE>(yHv!1Tv~1tw7)0cH7>R+i)M61VrsDy5|BF#gnWdyg z3Jb+HY&gB&G~eGoYHB{QULeqEx3UA)ti3Y*{r$jjxm)sB!K}unCN0205bqhz(Fyqk z&Ux-rF@RJNA%9&(3&?Dk8vYCmaCR1E`Ief#LDI;;2G~jcKtqIwn+T1kvXIFL1H5g}6EA`xD8>iiTR%R{{){ zAtCXA6MMLe%Fv;=m>(S!Kj(1R1NQW;AmsJs@p`i-FanRKdp~hjehBL9Oz4OvRWPd* z*zL2`CZ|Xw5z<)i_*jTiE#t#XgSvI8GlQ*RT^!9y!2h?HKmIqb9er!}7B8hWZXDW% zKKGj{u`G_E;8AuU4ICopu6IX|>bwT%o#{I=*lOe zQ`6JM+!Zy!!NK41JW9BKnkjG;QN%aAarHb9EgV8>=IUe#pqkK*AOpN)jfqcm1f-Vf>X*v&sIkVPg*# zWAgoCvxkL4?Groao%tWDUxHaDD-9J|EzV9ur4<|2_q6Xi2ZkRkzT^hN^mjp6{0r7@ zkq0yrE6t*okGzUevVCn_P(dQA36(F^GyEF(9yu{6`70Du$f)yoN-Ib6t-q-c349i& zQ#NV*%yZ0GX!ZH*64DEGmSRK<}=> z6D{(ZW7jkohY%KUD~zV|kP+`RFOnf4*j9q5(tz3E)LPBN><`tA8B$(eU8smZxW|Wv zIKF+0;xF_n*t+}yQ0?Jh+q(7_%wD=zn;fvpHCIbWl!aw*H2{FXufnFXAkZ;C1U^V- zM}Zhh36ZkK!SS;2#F8Yv=l2C3XxYcc#1N+c`JLH&tiPUoB%*DZuj@4SO9es=w_n5ya-POl^6Zzu-oT+z_d-d!fEj!ah@_zOEx3wgwz@4q$kM za4GNzu9wl?@0o>)O+TZeePOb{!!VWT^OFFF71?{CR##UC09nE|7D=7+P0Ar;0>mK4 zbdVoae|NY3!^LJ-S0coSPP^fFI**iyh|&1(Ccr2ah);z$3?T#Cav8jS!ZGwRFadgA zUS18>{_}lfTPG)$Hs_P%TM%W#Q#oQSO-($=1g6d*e)Z%8*{%!YtgLHRmYKU>nUR52 z19LuvgoGfV<7YylFBupnK6x858vW@MDF4`50z~Ucc~K2Yo^13nxbZRyA!#f&Qknu2 zt}XEq(Gbp_i15)Id%%oDo$GBm^#_eCk*wkmVi#ZKS8B)#Ylhi@lAF53SWKtUcXEeK z7_JZ$IcQTv1b|U%1{iejYRx4(`TSj{abHOM{rLuvSdeN1^Z^M~)d%2Il1X2@UF||g zsvL1KFftwi>yYtJN=?;JRc-Zn^pi~v3F)0HSzs>r5-6FL5C*E?6GqTz)Sp{olt7*L z^vQo`lNJCmGbXT4|MM3eAn|>B4gqz{&wlzdF6XS48+Kq-jV$IUu5=C@FaH7ziUQ zph>!99+sG&<6Gq4+7ek+=kL`_y!!K>02`Hz2J!J!}v|5JRS86+ar&^T-}2 zQM$hd!}8<(Psa}Jroe};O4 zp*Z^{`*;f+E`w4hl#h3}jkHEShKf5RF>alfuTVJSeI~y<8Pfbn%|Q#V*Z(oU5yO6_ zn|4)vPZuL&gcvjO!hQ-Fa3C^H zEJ33%To{6w#|C7p<3YS=qqs-ggx_C|e2M@Lrc)t!Ad6_2wF zKJRwt)1%3(p7~NWbSyU_Hn#mWz*7V+B8}6jd)wl;ajCsc@6-#et7Y*0t)Ur!CDY}! zhp;u^7Kxv|Ef*ErNB4;uXd0g_2<)X%$wD(Sbn2|BSuQUPhJF37ozIt_p|xmq|2M&Y zmKssD$P+NmEkk@1OGiUV9OD=6%);b}OXMsnjK*T9G&VNGS#su_i^)fOlS?_a<>> z{WknNSE(c?zK`gGsq?nyX>Vi}`Mc5tjwkL3H_q0TI4`oLJS-+9pUbo$EwP&HZ5>Kp z-r5Bpe$KIhkU;C`;ys$iUK3QTt6A5Fg~PNq7z_6n;uOdHRayOSA1Q|;!-p?5Sfx%T zHi@;^Cu}dSS&+)@VkRDSB(Jb71cf!9LZw$^c+awDvUINCWiL{72?k@ab?jD0SDq)t zupgHdh(mIl!Wh0bW0-M{A&NkdCH;8#8JnjDR^#(w+Ew`{C5H-$6)@3&*Cayc@d%h} zz{u1AAehtj*Zsp8lSn{|;(DEMwq|8PCoFOJ<+RyuyOm5Xef3iTQJda(-BbPC9X-X66_b^6QXy_zm~t>M^q1;*l!y(*y(Jf%YN_2BW{)k#!h_NKnzB^y$d9{x z7nqbsM_v@x5Ga&;X%T3SmO0~`Qa1C1_f?xQ;>!`kzo_pC7X%%Jn;TiEIdcZU<%g+I znc+@d~O|M~`ldJc1?Ak(r?)4C7!$iu(Y{eB2q z9Q6>LWZ2w}91cOjtz_;LQ1Ft=`zTG*pY4pswVzIWgICS8L=jN7gN5d_}`G=WR1_N;1crItF9K>P4&qhntMk@_A77OG2 z3en2)Q8w1r?VisOPx0KQlYjFvZ$5?SxTqs~s!C@wB0pr*cgo%>o8yM_vK2P(Icbgm zJ?pnhPkGyhh|ZG1`pvW%N@#C)8oBRgdd!+esBnG+KOjm6$qD==nW`?5b-%ZXhX5l? ze{l6mbE4bgU8m|w#6>A4#CH$T@fVI}$oHbS;7YWhp%gK7nY5Dgu&~pYKM{CE`ABZI zj+BunjOcDs+=gI`O8*#Rz*WDaE;$&iYME#;m|6nLIrP$RV$zy>*EsRL!kR%;3 z=ub#_gB>VHGEc~OdWOr~kKBSDGrz6{Z_H(m224Wb2Z&cdsB|*c(p6ghY1??Z;xd}i zk;%P#yl?_k8pR^Bf&L$fYPn98Qh_Ycq#{NNzQFzja?;bwbB)trhsWbCyuZLl3&ljf z#yRbUickt)Vmv_Rk9uWD@A|lpsA>gh!TwSoGOfU z8HE*1-C^|2bWsV1!~`-%c4bGHbVPad;)kxRj-W9V5+sSjp9)XPF-ckAS{glJ7mL+o zs>`pcqy1O=I-fYq?)|J|wbD8wsr}fhW@MXh+ofHu%6`CdeT)|R6@S5;uI0=Lk6hOh z|D+b99XGU@=RcrODr1h8jAc)>&^0qZ-qZBw%8B!fWvv?6E8s#G#joT%sak8QP#~KI zjFw4FNm1v(?eVhpQ+znn00EHg?H(X_;qzt(mq>ly{sZo`ZzTEaBnUv-b0N!T&_e7w zjk*Gr%W-0IK&^K041g(a_h%o`fH4f;dU8eDvzNzPuXgb-azr`Z+OT|cKC9ey()4=Ta_0W zl6+3=^qSloQT{bf5%f#;Kb>a3zVo9(C5z{)En=DkF>0_M;K4xV&cNXCXp$Hg@=tiy z_BGA;RAGGQJdc!Fds4ZwM=pneP=J8eq(v`EZh8)9i)$RTs{G?Wv#N^YA@Xy2IR z+Sm>SBXM_2PwJaD%mqdA=L8}b%uhkFqup)VZ$ix9@>B6qq4|C8?#P6V)v({*&JGT* z#{r-|ZED)uEFYY&f6kLiv{*9g4?Em7z!@|Ia`5{w%yv#r&W_ya#@|SlR2(3PYuI=u z;24*dmb#sYeu1@Ki> z)n??ree?z&B0-gZpFKFlBP_of}W-U}-s_wS?;Dd?G zy;7UaaMhmHg0r>D<$d^Mbsn=Jv<47(0c>v%#s_eCk$$o_@{fGj`t!YiY^)4VLH*`- zrKuSXiMmi}jx++(?(HI7rh|@|?eLqp>&;1Dtv?ld>2lHqEoSjw$q(4i1h5A+u%dq; zJ?JwJ?r7uLkBVD}mKYt-P=@wT*hHZ$c>ow*E9C@!N>+JmSFbe4gD#tFDLBna^M0^V zw_2zR{kE&NYOz65^76gZ`;8IrUz{B$6( z@h`>MijIq#Xi0V)C{RW~XWi*!F8kjEc33uHSk;Abqsh8&=#}f~*12=rw(8Pm@BJKp zqpt*yFY>>%w(c+Oy%ymDD0cd*gJGo+vUbxayRTk_{4MO(u`p#$&vwNeg9mD%I5W;x z)=(_Q|1Lh79aj+3!l!Ox6egTnU&rgIjEh5|O{8LgPnO2p;C;Lk5 z*#@?TBr&MDidp5EQ$B5cE3sk@0CV$7X?b^91e}XNdXr7hT5W31LGcBhQxyg(yKq zAvDo7OUve!MaSo?z+LyN_T7vQEVsx0G!Igx%H@&rl~RiJr{=P*e`)vj#ZZC!4(z|P zvkBY(6aw1-#*C!%N)P|{Wkw~c2!4-gBBO1A^eN^ncC&vK_)>D$+^R8`6m9TMW!_yt*+mTt0b+?s zl_?UF-3A-O;gq`_$5_`0&Y9%A*dDr?^yC!mPLs>zZOPDrS(8!(-9ywoV0I6r^%q+J zyAGTt{r?G#R~3>H{`#wHj4YdN?f!^Kjxy4#HyVgvKDNZTfXof8#AXzL0?GH!vbu9D zcD8QRi8TJSd=z?LKGN|;!0FT<^>uFSvFB8-9n+b}7v^q-h0XJ7px>2VbX0DnYcx;+ z19$prmP9`HEYR7bSxP~#wH}iSMgO{57ywzXtYsyuBBS%c#V#@~|66)rGwq z20V@f(zBfsA>6fup-(PIyi8`?#;2)9FfmcAn|LN$s>%GlY;R>stls=;5h(NVe^)WI z4mg_tu1){21_WHtz`(o+6!F+lUJ-DY!uMRE-|OEZtUnW>Ayc-I0C6ofkP-^i=Vi!O zX%gJa7xehTi+fVMR5&d^pFHVKgHbPh*7`!l@hTFlBM?)E%0kqp=xZc5{rbwLw1`S^ z?uUOIfy}A)2?6HIG6SLZ|14eQpQW4KnQ*j1w?!1G%Dj`mgn9+#)9|~UW*()o0i4gJ z!Y$P6ic=1zK^K%@Z~{Rp@=+6Bb{CVQaS~mXF$s_z!+T_?c4z?ZcT$C}!I2>YBYbEL zRA#28;Bwurl@S4xt?FN)C%p~1BJr$;KTej=+|PQ8H>Smnh4x47rv)%Eb+jR9G?1v* z$H!@zE5ZMZNad7ZVCs;H*EO1)YJEe5nL=W5u_P~C)z~y^L4$Fk?2_McnjO$Rjlbap zJZS#P`F1=txYUR!`G)2*ZaX#l7OqK?s6x255p7kruZ`k=XOiXL)5ZBgjbGry!c^B@ z#rtV$l|wW*9HB`f2pW&g?NK=_=<-n>bL`7+@bZe?-lo7)-@SHJY9ariu5HNFfpvx6 zxRl;yrt#NyXd3R;asFES*>l~xQ}uam=(4sDChKz5R<+kzC>q?Fc3zErr(YwVfSXN~ z-oP*69W%i4rGB&~cG7f{Bu%rClGtynntO2J$z-_fJ_78Ug|2Y#?rgdMr&mI4hCN~rE}W;861bf6C${A&)%<&t-5QSGq7I z!RD3%=d+BU`aBXBBx{v+Q(NPV0k4@^6vTW-2H8@L#RrQq6o<<7FsZLs0Qo1WK4> zPBibDPU2u;eUID*l6- zdR?@-cY^sU6x#afr^EMl)bf09%L4+!^K871tQ65nbdjuB&DqG{ay}tV+G+f7(O}N_ z+*J{_Y=n%UA2_*e7_i3?!}|uWC_d#P-&hqMMpVvzK9#6YL`DBB*A{7l+~g1+!U@Mx zaywufP$>SW7{-6878Hn(;Ro|2Cu_~$zqR#eH+~il)Ur4FW|8TEtWW7Y^Zq6-Pu zWKuaAKq`Duz6_c23uUWwG)sG0tX)hszq)3#yH9s3OZnr@iu=Vec~QUY zRbi_xYCq6fHq0`Fs*uHLB4ApiCwfIr`3ZqZ{Kk9#^sU?bvkg1LLyITu>h}}-kN*V& zf6Aqw$nN7nLm(?D>_TJ5h{k;GA}P!xjMChQzNg{gQ!io7SD#~?Lz7oOYsil=fmSy# z$gA0HuMKhQfx!n#LGOphQ|}DHSOLxk9-i*d&xDNDWmh~2zc`;impLM`TBe`jbX-$3 zoCc+FwGQt`2)ze8OPeSzFS|ZGHDmElxbd$~P6{fAV`lHe%7I^7vRs zjGrkSrjVg`w1nh&Lq1?leK8A(NW%4jOA~-DP$@(`hY?q2sNPZ_toide8sBIzt z!%jIVO7uC0tUzH7L) zgAn1$5@VnTyjBg{m7{;CHxhtvlRR%dH*PfWi3~|J;gw1?SsyR*=YZe8w`V4oRCuKU zQ3gFiH+9?aZ))%hl;U6TixASR0bkFGG{feX#ufbv*AfUVvw~6#^vK2)RxpmtCJ)sl z^a8=1WLO)IXLm2cDr>rG>zkPs6`ukJGjUPYw0IN#n0?rR^MgCsbx^+2I^o2@!-QIy zoesXf?>YR%$#UY(P6y3UB%U+R%>mhTTd`BGo81x_^_*KxOD{a@!tCaaUL|Z1c}BP* zktd7xL78f{4VJQ^R!k-Oa7U3W6fAa1{25V_cBNP^631!8lWeexOH#IcM$HsE5x6By zmx*2-Z<^HM;Perrjq_w_Q^J`cM2}!=-`Oc=MPug)_uslVRhl&&-Lad~S5-;BrDh=T zgFb>NVkC$Xr4MxtrlzfPah#{JAh;M6881E&M(090&mk!*h?`@#S#u`+sAaaG+d+|D zH@Q3CU%-gS7FLc91va)p;#}yQhdkN`e(687G(;YDXvV!YJ{QDkkOZ^iR z^hijU|s`*&MbbUDA^kL1L))Ptd-g7Hx3_g@0}!@~wIJt003d*1@@}92l%U zkSOY9jDk9sq4JPgh)p0fUzT( z^=%a8-lxZF0zxA~eHE$PEH5khNv|8m*MWTv7sc{SKNAEobQ#UiUS3NO6|>3L>HX{$ z?5W-UrnxThwLKss7-(o7s@@+x4%#Q7(QQtfIG?#2TU~Vc!8gWQ)>#hJ0k|V{ z7xaQUm)TxUow&}}J8~a2+_wt$)4815a2IN4YU2{A$Q!>_LHR=$nPu#itFl=f9?7`F zJ<)34(!CP>hr6Oy&k%eO{ljqyK?Cv)G~vFlmIO(`@&1#Cg41(zXfHjx_Ki*}(fX$P z*}}f;WTN`u(8d?w(Z8YZQHhOTPIdyG`1Vt_V4t$&%N*W&DbMiK*)U+d%+P!tmYmS6`41_p~S!w`v?nVGQ;>6NUlVP{s3?@-_iX0*_( z?6+rr(rV!)Tsux;86(xtTPoh_P26xqg5(v?lvNkDv#YeN`KZsnXkwwif zl?U*6Z3BB`h`hOi9*9l1-7FDXONT8<0Az*&LWgs?(3;0;`lo+PX3}>m(8!{-<@w?d z=WMh?gwo;iFbkQ z0WKFSAcXUBw&EyGq9BAI+R)UP#~rONn#*Nk`N#6Ha3w=zD+8DWmR1oh?Kz2N;YtY{!{U5B}$iy5Ui7E^r8+l*w>M;By zxLH$$Af`I>{>>GsqrKpyfjDx~pE5z5fTC_yMXt>J z$JAZlpq#{<6gKvin{0Pc=l3qOu*t!D>aRRL3FzY`_X64l!F0Vkcj+i3`C#p^<%vD$ z1XJlNdxxV&E_!-W7U%a#Z}i%&XX_VOZyuhy2an1z? z^Ewcn9W+m$xJ*kPwY+r|pWRbrw4>fHl6BMP1FUdsv^&aKNTbissJ|*l#)CEKGg2f? zVJvEM{&JiY3bwnX8JU=X=rt1hV?zozP-6#_^#f}f4?wDnW{b^S9hd@67f*j;z|LI1 z%fx4Bg$%I+X4?u23%*&?BF313D4xc|w7|%!>q4AHBrD_M;BcvFkd=ChQP`BUw5cTf z7WOGR>5=}5lYSb8wOA+``Xd4epA=X&HI|0B5VpJYlfBxy$FvE%!v``hd(===mBugCq>HFmmu9}PU(63EzlfOKlK+da^kk(a+{; z+vgFH^~p~oY?YNW-;n~{Hb|Tb@+#6?ymAkyURm9tc>&jjFN0H7k7SY9r9IuKCv0k>$ykIUvLe}y#LXC>6 zk|c@|a9Oh5>=TPD#JfLDb9c5@0kdUYy}HISPGvT?ay0*8Z5mLE&*8l-qmXc!FlZRk zCWFr>TE3kdpgC(R_azkRbNO?B5llPwi*V{>T7khM@hOTOd#A#^1&Sw6h7aI1{G6qF z=X%6zl*DCd$@W^}1vG zSoHpEvoTOzIWWH?^Hf&@mJ~Yf-aqt{naY-5q>FgEP;y54%FfTZy@vaZg!mhU^X(08 z=rU>Z$;Sy0n9Yiu_DhBHJP|26ezyb=6CG~Jp+i$f2);H>_iZoS_Vh$v@FKq$49F|3 z-iZ;nr`kw%MFO2R@yPDNDF8xX)$(qfHx85=V_`z7LNhH5xT6PVcXs;XscfP01;|+P z=A1?IuP2Az8ZsH!!?8p z#1$**d&8jNrbi!dZoJ-}>h;_Qpb&sHG(5b?ZKd2+$bNcqa&l+qdl+5X;ML=Nf zcc=+^PCO95++xbx&dO4X_?af~k!75UR^x>KgMj|q`P9O{0*}|Ho#~I7i)@WvTWr@k z=++MntP?p^r00hd>)C@qT-7Q0Q>%*Uvl1)B&PNA_n=mX34!X@Bna#t9{}lM(iU@sD zZV#tubUHDu)~ak}%e-?8S>xe5Mw zmc7H;X62Xk`N~mU*>?wNIJUTYE>T8ssG+CqLq3q5kZMEj9uqZDPlKu!B$OuL2&=1iT1!^!-Muh_Y ziDo;Lf6Fhx*;H2$A)}gj%=R)H#utTnkyhER@iXj}QYrQx^J_;+w159tOJ0)_&K=9_ zN)!R9&h>uW&HUtgXaR)>&f&?@)zkIvO&ztQBnfLr-NPHuX(b|}crM20BkTHV8|G0vRXoGbR#M35rAq9IUC`fm>?Q00cwvER!SM^iM7WU-1$B;68kuCtxR=rtiX z&u?fts=ECJH@alYIh*1A8gc`Wputvc?u7n%SLepuwQ+Dgz>bOoN-jy#*__of*c+?2 zmgeRYIXoR+EJQ@08aN;Xjeq_Um^N(8`9ZmJ#^YdcFuM@F zSudhH=&?WoW&w>;NsF_^oAG1ti0iJ|e;?N|xJ)!Zwnk#y^HwRAZg z?6Tgbqwz3=z?8+kKQ=JihYr{xC`({fS6AO3SF*@9K~Yvzq|yC&ILB^c2jMsQ^0)c} z{G&+I_Ge4+Ow_Lga=C<*Z~#a7K#9yY+} zlNqeHJX?e1l1%N~ea-G-v1TQoKTRtYE6;cdMqkR1u)C{aFDMA&TVOE7NFY6TWpP-3 zL!zZ5SVIYYH<841X>vM}9Lo2pUZ})sn~C+o<@YB1K{=VTefx{hRU^X1McJ%p^KK&xa=34sOqfDlc_0G04A6 z^v85yc}{%)91d8~tEP0>nF%L;vq^51PVd$?Q5Cx1SxX2(wfJG~oV321Vqj*xv>*ou zEas1a!k{FHxaV8Z}4mn?iu+sItU+nVhnS{vha(fim`$YR)PY(n* zod-?P5tUJ*4`lt=QpU_UmxbZ8BmsavDysf>Xe9}3?93WM!ll7+-*@NrHP6oWb`P58 z+uPe%TrQPv3u9yO9(9m7_5ao#Yb%!PGfSUDnlzm5e?uHD7Mt_Dw7Ea4FpGA=C;1*5 zueT!(HLI-zIg8BJ*U`RvDdJQKkI)~@Hiqg{zaF((^Xx(ZujxHmR0XSznHp0B(0S## zas@d#4BoPyLhTkluvjtfh@3X3d%30VzZl;#YYVw(_ask$4SZ9LGO%2ZdU4{_ z=~{GcG{>$7RC%_<_wx%jtJR;|XE$zM;@!y7!!*9nbwe9kcN|>>g;^juS3yy6ToD2u z=j%V;qPPB?02PHl*{i}}ec7EpfA_5m`^@45MB&X%#9=m^>>^kq#K@PBip=y36^wh_yJoNfqJZcS_d=-0! zd6XFja;qyAlTTfh&9vpX>)cv&K=kJtYefdyctHh%alw|_{twC?1NM-?1ad)%s4MMV zPffG33gxKzya_1O%Va{ig|)B}G6m;6!Q?-7vTIu=ABx9zfBDfD=fJ|Wl+b%DDok^= zPDORO{rw`(r8-PMQ7&_1JLOA8;_F?p$K#~l>{j7MOKE&9`FbtCGV*kdHW%;)`_*@| zKVB-Go{*Mybiiu%J8j1E@dDV^YloYZl=SuC+0!G&-Wm7m#o6n*o5%BknS~`7pBi|@ zft#rV56Nex`zIfaj>uuVNX{~%J1skMi%PKa-Py9=R*Nln#KZ&mn~}%EDrZJ_8$KWM zf_KkbTpt}Xj+^Q2vZ)#NPS1-dhxh1(xkFjHtM})Nzj7dt_%v02Af!a4KQ8M{##g4s z{H}T^X4O9=$<;ftB`gR1WlJktPvD&8O2^{ii2yqGWZ8M>^RUc>-@RY2U`u@>z>?*# z<30ODUkt-#0BOly!2ROGHD^dm)ArM4_m4z!-y9S?o4!3*B^hiUn<+_-g$#p0o&t9q68ZQD_)ylwvWjrfPYj8#l>P^tI% zE+8-*P{`!_Za#?jE$Tpml!?IdFULo+_D7y1p0cq~5mTIi7?={U8+-j;2e?Ff5Pgw2 z0gwQKS_e0TzrqA4w3{(IS=_S8(Z<5l~F!(F(B7sX1-aH@HIlW`!gDK zzAr=$j3&;a$lDt6M62s4q@EP&qkZ_;C(X3-OvUoGh-0617F}8lV3M`J8wjsMDLz7_ zFNk35ij;1R{or}{#7q^qXGWPHtv`Qtgv|A*o0n;waPrhMNZZH(`$-nAT+dYCSh^9e zVGRb{!dRIGF8cMqd1^H0QI&||4_;;Qj%F=yH+xI#Ysr4Ab1!)QPRkEGVIK~qUP7H1 zgwLV|0H&lQchGGa@3*8GY`}nHORD^PVj`7Vz1{t`gvaxP+vV&D2rDo$G6Gc;DwA_S zR+xq7MCM~Az*$Iem!aRq#;w9Y5PALglNQ;m3*N52`flf}vs^s_@y_J~vc{cL`3I&~KE;*X-g;?r(BZlEq{CMMe4$ z9w_C?5aeWbma~4!hHyPS-CDGXfox6>4lPO|6wg|JJkhuOnOjHM;D8+h<7T;_%Pi0; zkXTnMw!=`ib5H6DquS3T%r@rocY^&^D_D}VGly%|Q?6JJp_35d|#-I+jE&d(lt{+i1PU z{Z+%Cj3T~qsqt`%{FWSR5ZG>EplmWED&T9A&f<>KQ)IL?lcR+-YfabuSD4&r*#C~oH@gZ;ni~TV<>)4V07&E*GViDPn(_><*E#XyHzyiU%59U7Rjfa& z8#n3nq2YfQ7>i-SJgFRB+P*y0m|PUfe4 zR&uh3$?($+1SZ0k-KkgYp15aH3j)@;Sziy%SSuQ-UM;Iuen<3|`)`BH^1EvNSrJ*> z?}3_#kt6Ma{wE?c+tT4-JOAc(O{u>cu*I{kUFsQ2n3%kG?exLr-ZuA8Y^QE1W~z7j zTdX!8&#y#(A_Kt5S?zkZ+-jHiOqREhe6X9AYXi3cN*A(TGzy91k)$zA!l zy%r3-R(b_kGQKu*JXCwlhUbZ01yEcR0saFzJ8 ztm&*3g?09VVY5F2xP82MBmCpPrw%S$>>tmlKS5)962;XN4vCu*;-yyZ)8|<$xK-tO zv`YId^nrUta(|8)oPb(>v(4#rlHX@H+2p{-OidXWFK%vrz@Vs#yDM`PxK!^0y&Y}V zK|V7pcyZ^OcI7u`NM{2j=6d!xf=Qjj&w-Idq|`@?w-$?JRDHY?9!kh2#}LVm)z^4P znl=xiXziB1z6bwHW$LZ|NOU`S{=QcO=ZEjd z^I?~#Yh|=s-!@88a&mT7786*1~N#wP?Lz1llj7=$4`5Un>sjw7d(&Rr%b2wf& zm)+S1_T#M`WfAXm@ETv5$41`_gRML>NbVI_@SuRi^wzbQkXyfyC>vW}gCNbOe3; zOt7(4rHqcmjWoG)dX|MHzm-s`IeGyFW)Z1({{B}W*u$Ro-T)+ z3#Xm_aQ&_5Bbg3+?)KfBw74G-%5%w{oX&=j1HLvqqrqQ+*W#be&7E4kPu>_Ft15Ur zKF4?2r%PaAc9C=EiS#zN?*NO@Sbw`L0O zj3dovCjv6=vgi7Gzenmre^k#@UKhfqr3Y%KQw1QV`rThqX%=#F>V4Qtr*@+Ru{B*N zWk}D>uqd%0y6O|2Ov_2z?Dwg=wpbLP<@ z!2jmrch%wRfXu%>iIDNl91+E|%^C0_LyRkMuoNQBo}H5n_f+*`M8Y37PWs=^uaiS?nj_(pw&9pvd*$WM~q>h`m4 zhk=g6hIHIh{u#m%qY%To8f*s93*h}*=tw?S~~Fd6>5eK+E94N$p}O6n!*u}HkQ zzI3RPmN24Rn07CS-IxlE zQZAX5ruHIBQoOG6LQp%nUyq}PRVByPKKFvTPwntbD717HopQ7hpds>f~LoB6sg1h ztDS>-n8v<%)De2xArb!RY|HHLxD8;tH6baYWnc>vzXK`YM3^d*5Vvq}t*1!hun8Zv zU7%JPC=Xko!nIVR#-55GG@>hyZcXvAbQJuF$yuxQu&HfYDdY~~S$H+5XtCSwJPqMo zNbY|aI6J|Fz#Vy3p&2ccRyaTHZ$+L?JuMQ;HyQI*};}RY@y>bp{FG!3W?eIH1?04uuk)6uX zAA)>PQS&AEaLg0X!J!OK!i1TXdS+fJlsSXnKRKn%CTl1=0xnKp9u_L=eXdZ9U1%d! z%MT|X&8DYKIU&OfR_u;u>W>HTcHn;0Go;t-Q1!WC4Fj&KsOfpARVM7^2TS=f3{6WY zur6Yan3$LlqeZ`*LIR1+p+KF89I0akk`Ntv+D{I}6C;5>TB=1Jn)#J)!Rd&p{VY6q zDV*v-nq?(C)%VlJtk^aG;kvr~MFUq_DXDKpH~W3HJDfkacTF9=hY;+5x@G6ViOE6z zs|(eZgHj^-4p&!om|&P50ibp-P|j+d20sVE@TqOtgL8`m-wi5UbWD%AlJH_tsc3Z} z#bN$a4s>F9rO_2m#H`(IHRLNktT4$;PBktMLBqEt6d|eFZHh!5PAujyjl;K|(S~&pg?0>@EEjIET zC{X6qo&OClgHO|4c>LODfa{mPr@Am>NPG~>uyQ>H0c@1Q_D3DS0Q?-G=vWO7!;riU zLwWd1bu2huR54UGnNr{+8LT{G$?jPg3=Ah(hHY5o!O_0*ibyhwEZf<+{C<$fazWut zRE`TE(&P>KXey^cpZ%+K{0&XnR|q;#1;1Yu`EkZlNV@ZvAW8r&F535$r~gWx?SCX~ZDqTk;JMvLiR$cY`VtO+>g zcDksi<7wIW_MD#x6u{)+G$5IGU+(4cwK&mF$NHu+JB}uR4a+u0Mi@zwu&et-oI(D~ zM*_mUejb0dIOV{9|3$6=(#l@EpAJV|{iT!W^y$pj6I&%RCx*6E#Lo8?L$ep;&Hb8^ zxNU|9CvPWcK-xwH?%$NJ_mSTh?H`4c+<&~rm1z`F;X|>vSqaRhF8pb`m>L^YZ8iaB z69;!wj0}!47jvpt5elyD*~<_>1q1L+PHq`NfM_e&${E?ezW`(>v%pzUR(xvGzy4`< zhIsLqq0^B~YkayYhY`Vn?<5>S#A?>=1v1xxylS3apjsWx;)|J6#PW{Xe#3q1KI%Vw z;(xwNrnDltd9lv_T`Bno-Bzb3+T)9?aFHz4xr;}8k7RPQw<)PM?vJFmU9{=|(6Q|+ z8&gz~gR5ESwpC$p(oVr=ALxQ^NQM=BTVD^<%-i_k!xqBILZlgAJ_?nHY_uF1tpcVd zKHy-y2v|ZR@RV1&8^J`$e8SI+>a?>4C!)e(582g zq`PAcbZKEmL78?GyK-xAe7E@~@ul1& z%2L3t%qMa+{08o3v@UDb6I_P|-IprdrGAlRG6Q9=}+; zH6n0;1;=ZCQTiktP)zz4AqN%&NpY3YN`{6XtlE6Y&w$&?SGUKRi)hw!Z+b=u5;4GS=8&mFI9P7xigX(Y4kUz~?bx>m;lQ8X_)d*}C90`1rdA_BhRQdmE5+g{ z|1Qx}Ar%QUH3;6y>-^iaA^0D0T3M*r&PLa`{R!B63Y~hJ*MZ!9&@CeVNh|r{rcYVo zF@iTE2ScsHnkz#^{eJw%)s5jEvvo0lZ$bPEeoY7&3d>Vb1QPM(kg5Il2`C7GOln|2 z9}xUD1^dL|E!%2&3kV#ZippWVTs?oCcqx+Nt^2;+u?g-59&BV%uSZx(AFKX*i0V&; z&EO$=V>g=jYjC?UJv=RNn-HJ_2N?v!BINP%zH2+^0!v*&7dmpq$_G0Xk+|bMw~NW& za%T+xX`F%3h`kT>7Xi|bP#Gm6Xv%re=0EL%{%hV+V8HGYk*Ws%kvYLagXWh zWlEKLJmr}PYXO=vJqEKt(3AjiN^Ic(dO{Ph;6a-M>aa)gxt41+wTkf}HzhC!p z!RUjSov|$G(4{P}z5EhUDU}tk!#nq6RL_PPjYX6*IFl1NNSrQ|X22$-NB9^M8o&BR zUbk3nl7TlqK&3JBnAI8gtF+MVmj@|=tHFFxD4vJ+vMG_z(<%PtF55icoJPDA^I@93 zt)9ISLX+;hcPr$)U3rMmVue&b`MG^GY8j&BJmS#1Jd(=&@pQ4rBC>(M*77!hCQ1wE z+TnQId#!7uY$*=caGTEU**-G4JZ9p0zf3UEhVMXimZOQtl>=$~6~u42cNO}xC%Lwh z*?N|*dwPklZV@> z>m~3{v54)958zt_6>xT@)jjwo8+_hh8X6i7q=<-!fKJ#f`h8z*ZEafxy-|+JU+>h+ zBWjd+TNq7BNClM-Tddz1eqx+khPy*X^E_^J7gDH&ZHQ0lhj#CUW8$SDR&=-oYBk>} zv%v_D$y!NC(p}{oZ-^EqaUSQ}(rJp(dM>SDExg+kUHAH&?n{YcXtpOE=m_);+Q0@i z$$+A9Dt(MM3n%vesrS#S?cp+d4p?qyCYE7wIe_XM9HOy0ehBRJQi7-HzL1>4r^2cG zLl^mkL1am(Y_+~hbJNDxyaB7mplXKHjLB^hF=d5rDJ+g)#g%Ta$m~oHXg#?(?mRT@ z?slmo1sg+E^G8*49<`{W;`>N^ob_p|GkomU^i8Q&z4z2R2^dB+; z7VfkP7YZTCO!5gE@dW!}hkpfo*41hPyA;@`kPmY^;t5vl+JV1eP4T;a| z0N;nB=}QM4!Y;F>xXyZgpccv_4kWGb)E6G-Tp}vZ+IZd)7XlZrZsUtji|-A>X=`EE zLPcUzIZ2D(UHq9uI*0NCBh8wCN^*!@IF}i%G6l>{fXCN{D+@&1c?(5!qH)4}0oqK5 znx&|#7SX*+j-@J5>H{8vJfV@rblY@O6uqmU-5^!?;%v&9d9@c&Xm>TPX4uWLVS|mK#iqTsbfi9>5ZE%RDnc340(%S=^L0Hb3ROLD6}<%+mHeq_ zT)xRqHw%rO?m+OKPsGPbF+;I%)j{D71do9rVM$}$P{WlUl~R$LI9;!eRb^LJhM#ZA+?Stwjmm&OTAE7b_fsNRMje#DwU_Fr_P+XDDogj2+M1H9$MnJ=WInv zlr2^(vkP)C8;d1xdjjRdM2N!5ITXxw9u`$cR+cW2%1Wjlf*yZPnNQ1o)o-Jmv(NhC z!PXGWc1-vzpLl;!uT0>*94k2$Q5vrG_Zy+8AyK;Re~Q1z##CPv%$TKg*1PQQfKSEVUOxGI6#IwN_k2HBW@{!7 z^?C9IwnAgZ3bYx?<&5^p3sz_2pzpW0+s0!SqJjw2H~*UR`31!6W!z@cL9!TVbAJvr zO z244L}P-edL>JM*8rQ~?& z9_A2nE*!{yb@;=B&H1`A&AHO%Eg_LE>-){-165!T@A)BN?oHLOJcY^`y=QZpd6EwI zKf`h#mQ0$7%jB!FmsD4q0AKW$=5uq{$d}EV?B+;v4X8zvVveFyZXeak-ov4D#4ot`ZoYh6Lmxq{Pr)$b2+=4p*b^l6CN}xDOWH9ib$P)8*w56WB+Q-Lf5bsrflY3cb_m!Qf%)a8h@4{PTb>lUj}1k$qnyI>yRV zOS7HfyP-@e)3H+pQPri}j#5IAtY%?(i;0>ZtZ0jvCRD)F{gN&fCFRWx@M}9I-mA;! zVsa3-_l}qy_9$?M^P2+y2Dtb!jYXJkpA3YA`BJ0i3^8ttw{78&Fx&?`%mjz$xh}c& zSpcVWVt}BsligbPnX%Nn2=7q`=>F};{M6b)DokACjRh}<0R~{AI>5q|;j22{w>x~F zvFfLMIv&@vryLnRbHrJObFY$4=9HzXYRS;LyL67r&H7zkYxV*G4BlnigDUyvGD;zZ zd+D;&LE@@2(Wz`DXR|GIbe4Ihj)k~GG-(_w9LN@BCs$k4gQag|g(mhX89R60rnZZO zu!W7=Hi?*%agh^C&pj34p0V*|M z*?eznr;#RNWM`YM9|?QCGl@XWED8q)wZ|fJO?0$5cIp?|{!kB7?QR8^4sNT59bccT z2eU2xF16D4>}isuikr)0!1}fS*(+(wjN)7mm|RWg^F_XF zejr6eM_c;_)(oiIe17*ev0LvnMXZ?IY-?n8+owiwX|%Tkh`csSfT_y5TD~!PAC~&! zHo053eP$=-ezKb_6zF4Ad>9`JM51fcW@9!*y|^8QJ|zaG&>`u|IjxH2Pw$#6%1f0B zIa7XB`Fmyjy){~@ZfsHBT`&EsvZz8HQ&P?_k#*D1G4*YSS-g@C&}-a%iSw0+cC}^{ z`vp~L)u+9-AYb7CMXxV#$Bw9bpW3R*i>K;NSlJI@E|eNPM9wc9HNSh@)fmn0P9ykq zd4Zko=VmeowVgb8B7d!8H(7+SOvSD96^Xf`XE)|CF<@y2I#EWOe8kybBE%Lrx6K4- z(E=s?juH~1#4R}7%kSAOOZNu-XswVFbI^tY*s=JF?+JOsEE6R0RTZ z6?AodxBT{NEj-tk(Wa@jMiy`>K2}w1pKJYoGUd=VGHmIm*k8BJ0vO0ononC=dI%`c z>FH|fZqTtEEYf$fxG^#5po8J;OK-X~I?=7fTlt<4vHV6vCr!(1J7Ls_i4izimuJX| z@!FB8-lK%#BA-PN2DH0$AsI3Fb1;$6Q}ZznX07x0RsQIwy&o!Bzng*|kmbDX0!C%V z#rMz}?odBN!N_fWMF4e6lSYshPP6GZ3l>Wv_ z#5#BZGM8P-YD%5AYk}!29KhwBu0{COvkqjH%4t;};bcPL)%$Ag^e&nzso zF+Ijf?bf@6O)zIlZ36apTltkjJ<;agNWyXt`R}HKzx!}*cM-UKv%#0{h?WLx(Bu6@Ea@x$ zLt%4^Un*fWM41;&gdB#wU$`VEo7Uiw%XE@TJA_j(|9H6{`EO|YpXVM5lx?YHCk1x} zKto~%8z``3;f!EqSKG|`d=%c=!so$A&b1uC(Hnr}Q0%y%SA?#vh+ssUqD4C{S!ipI zR-|KTs?Xym11B_R!G&3^L)V*)x0X&2hVSF{+itXc9^a$#@%wt9_XaeGpATqPPj=n8 zVSTtuydxEJ*qV-y+K&RNG*L`u16|P7#V#= zmE=+b2Hs7BioCp$<3+9R2(*r{KD&*7N@AON6H{KAn&}SPTBmX>AKa(!dTK*K=j8KB zY5zN>$}NiCwPf~Nzl462Q^P7I7NuuZ>4c&oG{~LK@TNCs%+KM=zb?#~Yv|IRUgn^| zQ@6{C$@sL1e^)D&C;zyXlJTz2_plo8Q*{faaX0DIDJ`8br_5FKP*lDtv`PzKPf6MH!7>UR24N9r7$)zKB~*hX*%Bd`jL#av$u`i6l+zOj$l_4 zUj=l3Mo$zJw%OCOO$JuNQekbe)s7uyKDrfvQfh4IS&H?KHw9r4rfyC%lJ%O0Z7)h)4bFi5Dw%48=hf$f5S`+|3zf)F}rrh4xH#ic*Om^`r}S zfemfkv=tQ;zbE&=C*Wvk!kBz*6O0FG&)ke{Y;A7`T3`bj(P$NP zg-eB400$Hh|2t7!tIe3HxN5fVk7$yeodI9#Ef4u}e~-={Iq^h`?xDn``ifb=Etm?` ziov!oR2?2YuzGam4C$FwhPTi4SX@$I1iEFRzaODdzrU+}i;g*Jdg)MTpcJcBZowv; zWuk#($QCk_hM5P*N#WBt+aY9RCU<$~ddjLwJq#-fpsiCe+DvHGU&DwTAgzj9pyp9l~C!4TCB0qoadGIwiBql1VW;C_7!8JDr%)+a9BPWyW z765D5hBk(AgCJmstli4!=buN3#EeM0@HacBM!{`Au0ACCnLdr+i(UmI>x}Scyr#;af{o_C9~) ze_3>pFmbslfxhQsopiQshFhA9i7_gYPEaI+w;x>5^7o8nms!1dU-t)9ecjoK=xd@w ze$qK5&WfJQj2Zd;8KNZFDdp=K`~c_o=UIitS?3Rxw*mbs|vPY2(8ChS5F}G19I!)GfRh-5 z?*Wjt4K(#J4^(M5>@-CQuV341s(}9yk8j@w3{)l%m%<1g=FjnoO9X|d+Ng8+M_w2Y z_Oy}iijKvvhqVd>2Zy|VLl^rEgUTd|gg%RO`>pcVmm0T7_Q)NBF}jec97Ov_A-I+r zzQe-%;Yg+_)Jf+@i`5ZABUvNSYyZ9|TuYMSK|-bj;b?T`%c%ixn~4-u11(YsH*dp? zg4R57-0djo$+sW%^@>{R6hW@G8#!-Lcb#VOjvA6Np!I`}I+_^0q85af8wbqCD^||~ zVQr4aZ5)GxPLFl_^<*8E`YJAvEM&e?4ISkvLbKaU$uood1xNpV2GIJ!_ z&txazxs!NVvFNx;nxq+w-=U|!_M$aI09eN9I*O5I*{|DaqJ~Fd!?{dmcSkc@UboQ- zRi~4K#aU5{5ddzD|OW!Sup8Mn^XChM(n(jg( z{W}UVbG5#Dy-Y_#1{F#sZ(3XfbaVRhcgT`*7O3lb{k%HHFz@WzmTix zt#6uz6!cnQTPyWI%{uN>KCD&o5I!O;!o< z#N}r=xm{saB_#tLpCS`ZQMVWY_J9L&D`P_>Dyl_}+L_I28UfTU+<(kw!Dwul+wH(t=lTg zjaFV((b*H(=9h-zYCLRvYmH9!q^U9rI@Nbp z5~9A;;O~UBa&bl6@BBA#*!^NryL+hg3_JclZ0o=V=Z3TMH}lc9qXRlxMRCa})*!R^ z6m%#DC4`8b3MGKKN}DEx@yh}EKPl&ZD&LDVgewr_Da@`>DJNzLn?Jf((13Mr@phDb z&1N$zLGK5}|A3F)ts=LKlZ|#VdZp`XmMtxBywVwRx;t0UMjqbs{q%+}MUFOr#qN2X z^z2$l1DAZAb4AceLbvI1Q7E08Gk3yb$$b{0+ZC+H#3{fhb6C>(xoF0`VW;Qwr1`1F zW!f2L8yyC%j_Pk`>Q6!cJJnX}6c2E5$&fEdIix@_{M6rXxaCQ;p03dk<7v5ZV|UFj zt)rk;0=R|NdD>i)2Za!Su5G#D;u*_CsknKuGLI&(C-X0?74yn zzH-P&BJ%ZO_j1Dl13kr)zpU0T6!Av0figS0myYP7tCb*AEIVFozWvd#FZ6bvbOkdV z$sC*vgP7R`eg~pTqsOD92VIP?D=pq0kXDBd>4sse@6wwUEC5O?nuLx{r53W5aZ0#T z9!Z*RZk`TqVJiQ0vmk#VR|$sqNF|PtjqZ=fpFiCX>-#T$1Fq6FzO__sZF*js>$6HD z#-Q|eBwfPU)5Q5f|EyWReLZWi-neKVkFh0TTZYV-?pnYnpVKDYXmveams8dloo(~@ zG0qtsGNfm5fx7UXlh;ADa0-Zpny7O}yJ%FeQH<9aKfAGg{VL$~v>l3TzOZw^+Ugoe zEw}m?kKyqVHtiyLhB>x>DHQc%onLCbprvM&s$7yWfO}VBMmIDtN#35VHCXx#drPg2 z#J(IeAOHw9oL*wjU3d%*)FQb0MIcV&`}+v}ML~doDW>s0 zWK3Vo*RAx}*`p*n6tG1mym)$dUv@4B1E(JrK8L~FCAvnVEhJs7A&X^hsip-V%EaL1 z;PzCc1KR-dlN7bWvnhtTbA~PIbUbgyS58+|ccal*jTwfil$M6G-&c5a@NVv&TUu%a zrm>Oo`vEdR5CP1z?tgP3kZI+zGHAFNcC<2NM6|ga&LU+@A+jMKBq#Xj(ISYN>EpFk zo!aBcgJt%Zp1WC~XiWoFNl7U!qh-0WDZu`Vk5A$Io2Wn#_)&aEi6JhG8FNvR>dcw7 zOjo`_{9XS#7`CWb=2UptL5;fjg+y}E#V9Fah@-pLbyVxf2!U$r5Wg?@4mE!)2#@ox z|3%Nb)rf@EHIA6Z%ey)P){|HkAke~diV}LuAim?KDYG=$_Zkz(j>Goc%OmfkrQUr) z-LHIqPqIYUs7>ZsnnY38#seOrqnpx`#pq1Rgbeq?Dk&buQg)bz>j*UfFK7in$TZY| zx?j$7V?N}6PgKxKodf(+woTvKx%|d%MH)M4(%80bCyi~}b{gAGW7}q9 zv$1X4`F7fK&fD|8-~P3)Yp=ETteN}4+;h)7e`f<;NN4@SSvagib5Gx9BJCj|N;h0m z?W(U6sjjBHR|;tsA0x1*UEbc&mi`bef2dg%)%8ZxjI z9@%!Scy=R0-!+&yx;gG+V^uEK%ee_#rfqjmB z9!KrIuwy96RfW!>A-_&m;WuO);)WXO43}Uqn7{!7I9Q$QarKOLEV29NmGo212Y(*J zwxchd9pv@uZ1Zk|hN&92ZL4mmb|N3YalY?X{%xWUz^y;I`O5n=p=CYNGvgE|$SF|c z;q1v_m>dGjHOljC)s%eli^1WI5}~pLyK;B@@{FfTI_Qj1nr&tI9mx!sp()R%thrG6 zZP(|hoCp&o3_~}ZtBvNK4AU$DVga1g$isLa{4hr(Jq(~k23Wvp@V`DyQnF_CqxrK& zfRp#Ol)ph==v!LTKiJ$6gbDK*(sMX+o?PkGnYsHcMqScF)CbpJnS^tF$dQd) zJ@mHHJ-ISlikV}rW0;(F6Iwd7Dny|*{sniD2gS!YV3D1ARSMg3Z+jK>0cv7o|EQ`> zoL@}$ebcSM^yia#<py+cD+RbEc|ZK!NAJn#vXKEbJThv0a2;?RO~sY4<261#RDhvQ;yNhKN=G4&7tOyfER%Aq zR*^iw{DL!iP@`>u*KDKqxsS}vI5cqr7H+PAYRFUo=&&*-`&Rsp6PD)Dw$rvQnPwQbq+DB!OhXQs|mXH}gqvi@QJT$y${M;|Gp^9j=g*;Bq;KPyh#+5kXVq0Vf#jxW_#W!kRh=P&BY>}dL6+L8F6_@Qcu2M_(1r1Mzm z2#8ES<6P1c8g4?*vgLba$rjSBbajsUOE{1lTLzclk!(;AFnY#>UwX(Kzz<>%ba#LF zpfNLvo!i+6S}14M1s~#uClhxq1-!46Z{K7@fOmCOmK$3fN!9$@~p$1Zmi#@cIu9TAl{uQdv z^Q6;1yFPi!2SR$t4YpqbKQ|s1J$qs3Oj8Mdeg2t;fbN*du0!~epslTqOaUOQNF-nb z3Da1=SktMKtk~O=CSjSx;;V{bqhx*jNiZJ=`ZG!tz>>VJjYGXdsbYUL8r)`Mh1`$r z$FWkF5*F&34P*3N9p!2XASD2v_W#J^^8Y!X82YD*I=WF$|el&>)-mm)Dx+ zo4~HowmGs@`=cZ~if$?W>3Ro!@kOXRLfX>gTkc@2{1 z>r@g*kE2K^kQQ4c3Z)S=7>B&Pleyo7(q}WGmJpo5w+01-UE;Q~JK62OCHigMPfPH2 z(aOXp9bN;>v@j@WN$wEQPEh{2fn|K~Q<(9F212K=fHRL6uG&{*8a5Urfo=-Av`r3? zjaibXcKQ-pKRbNSCXitZj=k~THhmHhZ~-x0f!;g8`JlTkbdOLh4L8y_(D3hKsYCp? zRMF?umD)_oe4@v@AH)DkcU?zPONR!<%Ntfb9-RA?1p>97#=TfEpQQ5VO=LgM+8HmkRGc2=6~O~*5MQXBomEDx9X<4a4d?d(flo*6G3v;8euUN=jT ztcSi7ve(R@WRm84o@KCLC6S zWHKa!M&UqY`l*KVb)Bb{SZ<0>+C9s_O3+@ggd=<}A*iZ6%pdQXSNBSQW@k00q{RF<=hOD#nVi*L?q$PeDg+J^{)fudam!udVAgws!9dTUZ}CS6!P; z`_b*t&hMO}7x{6OD;^DNTaM$Q1?2=+q6tUVd(IEOlkQ?3ZU5fn+)9RY)9EYy)Dzqe z0?N1tSK5tyEkQ)mhtxT4iV=dI+p!5j^o^7#3cj-2#bYBR@Ft9U?Gj~y8LurhK94B* zemX+N(8Y%~;^ssw#0e+%Suh>I2H<04>~g(xwCL@og=|^|-lK30TDQ9;W<%rN{cbzp z-dhNm%gxHrgfUP0+ZSUSkxk>eWW@YhKfYxB4~xsG>^7{DusBB@Wrps?=xVt#^Nkoa zq6Dd3#^zEwp(8xkVTDlOpcZC7yyq~f!tZHtb|uCk!lx5xC3FqG4^1oNT!>l z{Z>1P&YTl^Kar>>Iwbb|aWiXwZ7Bsdu~dMem)sfDD76_dypD-KPq z+#&#+CKM8;%78HxNqg|axw56DV$01Q)@57xDN5Ez+zDJ@7n^<&_ ziI7-jaJ%p9q|1k=i$jmdC`4#+(HZ}28}zoO1gMF%xVYHNhM;f7+$5S?7sv8WBo1Dg z-dDg1Bwz<`!zM+2{GJVzWrdQWJ$J6YdOrd30&_<&d1Zl>l@Ti;&LL5-(BD-p!Mi-IjaalM6_a6##riX$ab zY&u>^&i>g0Dg-;QPDK*spmRh;Z;E6(3~y{|#k5W>lfU6d1wmd66GQhnylC9{NZ2#m zr66Slbbh>PZjk067CUIbA?p&Cfxqb?IH|}ar}A8|Us*4wsPdv?ZR(c@PAav%a1Y|B zER!)2wQh|4LUws2-^jlh$Q;SqQJmbGo|y9ofBtuXaN2;`SZy_keg8ykp@DSv!?K1WCnt= z%ECe?h#-fKRV(4}b#g|0`eK|@Fd9?TtzEA&IR(XRiL&j}-8mpdXK!+|VSas>ZD)<8 z+}ZKhkDtL|Dh8QdB5BrT>(6~|711L}ZnBGis52D8haOf^Y+Ms!#@=uxN36s}JFM6UqqCmurq}S#8r=PfGkyO;(lyQY#rp~@_ z65mJT{-|l86_d%bXq`py8N$n@rGS^ZCgin|F0zWCEtTHU7n8@;oyDS)mM?9OUW!%hTq*j8103 zv=$5wQNs!QPscRgX!V>MOFAl6D-AU`O?!L<=g~~Y^>8~y!gRiqsAER|y#~^*!*iRA zYpCSto>DpaFbWu@tO=!!q^gXdUX*SQktT828Qh#sc%eJPqZKN3#@o!ow#9{6O(_o* zUeNBmb7hZsEf1Qhfp|T+u<#I!)rG>kbQlWh)#$-FH#T2V74>C6Y@sxZg853+bGcDH z8I;ygKa4v{tKI1fIM<4u{amWp$;HKiSzD-*YQgr!K}u;TGPr1Om?7augTH|!zrH>L z@9p;*;@6fDJ7~rhYisT2D92j}dog>X7$#NsK9Y@8jayA{mr+TRQtX%?tG z!cV=n?ppkvo!|Ff7P{1ul6lznq&VUw_ZX}3c`J*kB2b$`8q=hGPh6Gi>ACB=Cjf!3 zhKkcb@?y`*;s|4Sn#T#a?ITJwSX_VfcaheXhzC@HF?4fF!x-&lKH+`k=_Rck5%zR5 zd`fU;AB$wc@eWLAxjFsXpQr0jO7GtYG6I3LR4&H$5x5dzv)*uV9mQai;KtS@|5)U3 zclv$Wc%HZpM<1g1+QY^6zP$D^%=;zl>Aq2~%2M<3;rUg?qy`4B-6J$cwjc8BL|Nyg zF{I(AEsZdBq}I@3>alS)*qrJ^o%85L)O)b22~9;#hmTEyE%@{)vA!xMDI9|l;FbmR z4i`UF8Fy`zr@%HkNhZG9qR^-9N)++i6R+%k=>i8l#=n9a2|(cz8I*S9RCN;qd2N58K@^ zV|0n!x~o1*nB|)gz(4?Q?Wp@{9ggNCJ}N^}t9d6-JRM(6V5E~p(V*sc zp!ZO9le_GAh<~iUhCs((uD>J?s>zY*`ogH2)^40T-7heaoE-qzqAIQK%gmQgdq_AOG}m%ev{=vib(l-O8-qI0;IWqX0q)d=I+?Od3}I9jX-sOnXX7 z1(}a7WLYFD+iMb>6W5RrKrt~jJ#DW?2Lcih7zhfT53=yOzozgjUs&7kb2utkHL$!@ zLV{AB@KR~Qtls411meE!f`~{3Ydo{_2{Gsnd~p1H<&mNfNH)R1z@$)q^EECvm17H} zdwrJJSePR2xf=`qR03$Lz9nxD#{9sl_ih~F&sa!TlT|&hBG18IP$RkBDAr&^{MKne zKu+ur?kRp;EEa7ru|5_cf4*&ITu~J(o*5Y6mN6`_$HvV*p2qip?GoSmJ}ms#O4{&P zu)g8pmCpF`5!qSEB=ieNy$bUJ-`|(N@v9mC;W^2wYiv|m^JyVA$j$Yl6jZHGqSK7u z0ocE{rvIYe-v7xCNs>VbJ>rldcUFrQKixk|PEXsd<8l#i~KiFH)vK` zlS6rX?HPG%vL2Mna!};T^zs~LdIkB%9lyGMygg%L{{yZ@fAVFMM!wII(covouG^uA zFJ=(^;D9SK$f$9Ci*ICCX_ILa zdw2I%=&X34ajuGb&nxbI0IlZo@ei$?{*Ho=W>VKTA|5>bM@c!27FIXO# z5Y_7u6zJO{cRS915mT1A(;1y3L7pD3vYsABQTgf(ac+~l+0$>h1GG`~+Zzo!{*UV8 zM-i10lnT45!+eZ28K!x64V`1^`zs91tO{=G-H^6dr4Fz>>HBFwL zU%fXccly33u*blLJX6ijUE1vuqUQ~BBYexUkU8$Qd# zczu2sxq&KT2h#fOyyC#-0_~?rC4ZE`%VHDB`7g;Dy#lr zPigyHrKZmmBIKpI&yACoe15D%gVw0yO7&5aSj=)U*iQmS7trVOFkY1aXafNODB-~B zKREigsBRDPM%k`uYq`DAm{Gg`kWs&301){H0H6z4K@?2;!{pwGa$2sy8c7`l898;S)z>qbrU*Jckp09!PHw+U3Q$7iKg8qP_X9L7wYK6^zw^>K1nB#B zK$qUmD3bcp$yNQ1=t-tXD7#h9|j{hR?fBj=|o0mdbrM-Dt zCS73Z@a7?9mN0G<5nbA^nkth)Nj zeBR%42o5!?U-5&<=|yu@mBAn4%g=A5&Yw>)5>CteK)5x4p20tH={$c%0Te!Lg7_ag znyq-F)HbhG8S~{NuQl_e0H7a>PRr#r+hiT_tM4_(u%#r4hH`#WcLOj*@IO9iiTHkf z5h7eH!sFf|kS39K$8W$^7fqqCEJeJ%5cR{~H|lG(0n9z)KgfyuqGGIaA6M#Tc@6_pdr+F6!E{P*zaChZ9)m^6w2BFNBX2cC(B`v^IfqaI^78abLrY-cpn9TzYJrz4rjH9&g& zi15`b{sKX0a-MG2&lZk!rB-WD6&cvlEyi|RZ4$n?i%O_9NyAiB@?zj9&@;}@!-;%X zbU|@Q-P36^I|ks(4~#)z+}TK2Se!Q@6%P>j&=V3l3hn*B1Brt|r(^ql z)zkKLwIDjIWHLsz)BU6fW-kg@%t%k+tEy7vd>9Xc!&!5)584XEil(jJEg5oIfkMM~ z1v1JP&a0)yv0kNGYy2?$bXRIJnj-6O%NOeD=B3Heb~lzWg#;_<1c*7nPp*qS30#-? zXp0!kRCj031Md$i?e?PQ3c>;$!sjmL)o^M{FCk3hL+6|DHd(=uv)KZ;=3O}Se%4AN zGs_mbz&!UhvSXWdmB~w6Mu4WW_2@sW^KFYMewmGXwPGHza1jkel1ZJfIu9Mv9D8*P ziWK$iK|Z7c)ttB9TaHXSeGnetT@zaMk7l1Ka0W@STQFfMzy<1914OxqAUt?uu<}r^R6V^F72)dmj$v zk}UnlSlBl6o*e}ROH5Rjn2!HZ>}zIo={KK z;XU}(9R){8@23cv&c zOoO)}SZ!lk=}pXbCxn&8nUZVF3Fgu85Ip^S+89`xG>;(=hJ)kd{_>#y z{n2V&Ms_N0vsUX>xMs(`JbPTkw3{o8h^x?KS${aakHTX-buBOAXMVNS?xJ-#J7DVl z*oK5u>O@Ak^@UA-`Z&56o_>9~k+&OKv$j*Wlc@4Ag)8`coAg4`Ov6>UqwY;_0O))5 ztmZz$0oH(xSFhNQTV`AV|7gI06%738#%OQX`pT(-QLtFGM>Xrz&|zazP_g!n)OMS$ zent=}67Yw@KD4$xvN}F+wJY!2>{xpFN6@W^IeHLncX2x?LXSYbkYA6B?iQ=+5P>`d zNH{xpwZWGXG+#PzH$Qam-}>)(uwLH7b?z#(2B)l^kv;OMg|* zO!P)_^?cFsHd$q>?v1#FP=&2O&g$%BZim;2cs~2lL2x%P+vSVbJMyqKn~B!0rf<^u z;Z1@NwK`-=`}ZoS5cgwOOfH~lO!csy@rAInZ|&c7z!zmuy2N@|4!Ue>}! z&HNpY%OAYFxzqwmY1TJA^As*e%9V{F-T*mcz`-Y=M7rfEOC!to*OsYOIQ^X+6U*Rs zn=WHb5gI{x&9^o$`smzzhIj$J5g(r6bg^SeKmM^o`8z4Z6filNRTLeqtF4>9AqRXh z(@25N{0u~G*hDf4f9qJ(JpPOw^6vF~FDcX9!ZL^psWD$rfJyqY+72}=B|}R3&#C0^ zOwA%+A=k;%b8CeuIJ5U1kbxLekFpv_cDs8hISUle=MT^lQ-=e5@fjGane&!e@502& zo{*731*^m0G-bjiJVJuw0=4h7LI|k@F>moWP3*Q^?M_g6$#dlKj+Czk&AQ2XUSHr^ zUy7_B1>5d{TJPT3kn(`E@!k6s9MJjVY~A1{AB2>B2}cUW1SIR%OA@#O}=GO;h zmkxvW9nf&n$#=_sM0siWJu}i|#A#C^-^T2uu`Hj}Y<;?dv%XewyKtZQq~vKhqu2I{ zABq!bCsRZ{1Xt6HHCrS88eF^eneR1g%owWx=OW>f)@7OBaziL&VY~`bwa!mj7O)V2 zj7!6}Uc9TK@gRSMb8m&}X`7%X$trt~agRx%7PJ)h4w01oN^|Ssoejg$9Y%Chc0&(T zN2&|(ByZ$jQz4)<#*(?zG-{6F+iiISE>BvQ16weWrMbj)$yYz@Nd)NTZptfX-hiP8 zS~!X@Fs;Onq#+r28#mqj#ya_r!IDR!QQFLXWaO;O zsu^$9c0DnoKY?V`^v-L-qlJ0)&TLror-0*Cr?~pa>JB`zFb=Cnv-W`NLW!J8VT>v( zNS}?FAcNFEn0pYR=og(kVu9~1iDimW2{7sky)iLa!3Jv$EWdY}Ea ztS;`s^}MuMqr2I=KKp@V%HK+-tfU;lA|Lkg7FFr^75`{_tmc)L@o#LtsZLLrR$kl; z0ygC}A6B5IYL!E&#_hLK2anm1)d&o+666-z%VQqOb1b;`hwQh=0|)InOW>!WDZotDiN2 z>}Z4;;)Xv$km)&sAgvzp+95Kxl}=GUih0aVx+SmY%=|%t@|JKJ(J)A5j(U6CTM3am z&E;})F_5+_bnizCZ zxyfD>e;@4SI!Z9jsQ39q)os)jIqRKdjx}!w6;68FY>wX_k>K}yFJj~NLXB5`*-b>+ z`Gm=kle?EVw2!Y;kv&C-UC=Vy;2l)sg2>a_%fiNUGd??;zyL>=YqXY=ASI~1xq`Cv zEYT&f`eu0Tj=O`ridvt5W5f7VTMuqhS{-jpoO5rOuTBTQSRUQ@p&#fvkjd% zV7sF<=VIKHgKVk0*JKXWP?s<{7?kOnZY zFbKN8Lcb|0c;p$u0E!B|K@q<#DH}logMM<~=1V)M_WoTAaRsU%C~os30mMAa!2%uI z*!n6uy{C$`@p0n~gCudpG*JmE0e>@m;l)qIjKInIp2jMZ=V<*_8=#ww%P{T*A(BL? zlmh6V3bjD-kUusmh{{OlBdhhnJ0{#ohWzjpCWQ=va-`l8CQ>kgIFeo2Rk|@TMglcQ1VY6$y+lmK^SSwgzcm!`0p1Lh_b^J^k zN{l@{SEE(n8Olz@3l}5{FUd$mOEz({G0sTMw6S+RgH@m}S^=$1kTtTLd6EQ6O(~!M zvs0f7er4pIz(H<59|$L(qo&ZNlRMdH)_)Nm?~5Jgub!i)mdY1Zny;tOV5lIkl3pg3 zPO)Ai-8{8>qMqlYq+cjZR`uHW&lj3dbQOW@SJLic^i2)e>BIePx#hGh7}yLlv91g6 zxa*dO8~n^`%&nn&z^&p2rOsh z>!eIHT_PDFlBI-5oLg{9jckCz=XD3En)I7WDTUa41@IS72dbC0I9(iGT%6fbRh^|* z4Ta7$xl{RdwJw~S9q`Y6pXLDj1MjM@6cXcFBn^m0-`Rt|QptU) zOP0G#R2jrk9uHQcVze)Wh*=q`0?%3z`AdYN4ya_5=rl-X5zMS37zf$dOMa1Mlehn4 zlDEd9l!S>?b~8fcLqW)*iA9Y^iGg%`Xij=XzkX#)9;=P=pcn7Nq*9c0;}O4`4K!CT z3{wtCTnH0YNCssnzV#Ur5LIHm zS{Vrx7{rR1;d%Ae+WqAl0vb0t*~r`k>FiC{i3UXg@zMW6&Prt&`s|X0HmkDQB@j4h zEB(wj^aUjgfg;}2GV7HAaLVFjZ9H}t0?QJ2y$WSZe8|UCGlnjttoe?;13%6aN9MB}h#}p=Oo2@|E40%TDQ`NfvcqVGQ9U&UD zJ3zslDlN!fC5X3XyiHMObE)&}pe9phL4}Gq2dl@k&MS+t$zlln#nSIC5vYYm(kI8QA9$|+ zWt2YhT)@Y~rb%k27fjzP1zm|I2D6anb7}p!Ob=3kGQ=y4g!$vr*ui5>9dfi{)g2U` zw3YfX+yrjd)TEjW`jQ^fhFS`RDP19<9(*EWM_e=b@VIWm~F5nmz%7G>|7 z)=>>WgjXP%ot!#$wgtzV9H8XDHwlGklz@$4k=bkVyD84tnA+(QoXuswB68wM^X!Yv~Mtdk8RNQj?p?X0ynuLgQO_^z25 z$V2?7F1Uhj46_=&D0a|z-SDL{AZY13Y9PEk*u6|nvA~4X2uKMce|DMcSs^mM$5;u` zP#&jW9j8JGw)d=geHDeBZMEjtV4z)!4VQgTO9B3q&_*M>kTiZLi@(96@y5^k4?mr{ z?)NunnfYpw%On^QoC?QMMPzP3FmzkU6!XWFOG15u3@DZ?*OqBek!&(>D$bBohH8Vmlx%oMz6)hqlrjeGP#8ef(Z4^N0E5issBDblqYg!i@; zCk9T(cL)99oKWhFHj1G>Y)d#{q`pF%zfM^3;g+ImkIC4wqcMJOK*4zWWKILM;Dgit zMfmCUXcm@0o*7Y%|kp6-5fpLYEKvT+q4GLb%@k?h+YBZSp}Ph~*qe1~_jBev3>s}2p1 zBDBLLR%S?||12RRx4Aq^U3sfW_yD43fc`dZIg`W?BIBYh>@(Ry3*gJxudT)Wai8laxLpu36RKiVq^x8}o zPD9C0;tq1NxKTy&hi%8EFwv|H8VSxun+p%i36648JIf8b(=5s6{5$iVlp(0)Lyczk zan)n3&Q!8pEL1JT;hX)}YVY}9e(+tiZzH2i3w)d`QYJ|_5fKeBBlkS#W=OXmpS^6H zqCYljy~`%VST0v1jUmbBNQ=2k3>Xxr>-{xB0R3D3Kes~gOzu(VA z4hc^FdNI11hq$$#eshZ^bdn@_(ztP=O|w^32)w= zrgM%w@wJx|YjcYHXmP0$A#nQ%G-R79L$yH^y563)kW)y z%G5~idyE0&fn=%RNLwLJw^N^TiTX=QcxD}&1-#0>m6tWLy63r~I4rR#V@YvyN{?;b zhss^7wy-8MWH9Y8&2)?U+If8hgFQf*Er*)<35!$EHd{dj8T9GuW(q8Hv#lUzyQ8b& zsdMev>isp4?+%#87LIVim~`7wB_eotvi^?sCuQa*`1~mBH(tQTn?3{k?%(S@TrSjz zRz-OGo~=&0P)}dKEO@x>HlNDZ1KZW5nV9Xz`gX6}-pM||9TRc7j$NK9NVezzl;1M+ zVXHuQD@&ihwA;LlUbDTPmBzo%?H9)A?4b>W4*{0m%IwcjPF-Xr)E-qh6gXUz6t@TKoxwsB4O&G0`Z|64( zq0(uCezqUV^B}ongtgsHALhfB?=Z$yAK)kLZCnzgIiRrQ)&W|Tj#B80ZmroKtXL(F zChicH_ozbM94;s>OrSPj>nVvi`)b>AZX17&&`+g_`!x)iY9gAscGo9BOSn^1T`m(` zzJM30=+jqu!(+vfpK0zFb=;kF%`ego`jS^q1052FLYA$wPPeilyF(;t_fN-6GYKGE z-)VQgkF798I33;hAtA1-jx7+_S=lYL_GxgX-HGQduu-gwT+ONG;!GhmQ`Zx6eW&xs zEp>&^q+H;%<~fnA?CNiHKfK_2cGFqf(&{`S_c%oxdFo*Sw{aL#^+R)raC&Lm!BCYn zO;e?3kmJAGA#?@BIR@b@Gm><=sYmeUu&hZ=7OA)$;OiB4FhPsA_jvXh+)Pf= z?GH-r0pX)d8$sne@8cqOO32~J#o}3LcR3vyD@>AnRp4l8byzH7;);CTg8Pc?Q7(1e z8m{t^9kSp7V!!Q&HFKtJX^N4;mp3#Rc+HU=m&wHG3wkZA%5KB-7|vgHd=x z{jo!A^XHE9NzCDfxnDU_Ki+1{E};gal+=ibE@L^_U1ApdJZ>Q?3muJ2c>>~2Ij!#` zB2v~QA-~(e_4D#OOUt){OOYFtH&RqDv2w)m5kyFs3#25zvZ zJAbXQt{0E6Gp3j{&4aVZ;7c#7EZqhZ@~+~o+lthNT+j7KHvG)t@HjH){_Yh94BFv^0G~6@)_$5*ZZqCGf&veCEoFCzx`9%mHG?TD_%c`5~mvGC!~0L%~LQ9rr;rgh2xLEz$xWN zDyQCge?1dN@Ghy{t>Ure`NsWFG5JKPM(e%mO}piStmlK3*{!FAO8xU`p2S#!&1Tc= zjsFr89gDWO)fn~mQd;M11kp>2>*!>Hn4|SZm*R?pKDkG~D}2q63g7{b23s8T!6c8X zQ09ePsR)Y~62A!9PwQU+w~mgWIKFsjo4^S|Z+wJnkN9 z(yTsz7h>4Dyr!~W&S*+E_gtdFYG+$p`%ZNnrX1@kIcv9LcW-J*ef!F8TLRjySnzN{ zeEvhy4tnZX?aBZ4^+uDX9hY;b4egvZCH|}F2wj>)6x^QAc{=clq~_=4xz;*4q&Bfh zWc-^aDQ4Xc6~ayDr_hIvSR}Wi!PM-lm=vHV{vXI6-^}=}r!q|{3G zy>RYx%K?J`2XyCiyjor5%0~Q%I)~Bg<{E#v7P3TYr*P*KLKgS#1Nf7UoUObAm=&jL zdNXU2-FT*30rm&ElSFRbh1<6CK3#Pfbo*6u`HtRM#RzaDknk3&%eJT$cP9mpK-4 zJYivYSP^;;1~x?q`P$chOEYoXPtvx3>4}Bys3#C2?3!IGn+KXFyhf3Y`<~146rUfR z*qSDc9A`z6XH8PB%~dLy{_^CoHBm=0R?Ss$jf-=IPCHFweV)ZL+!$eqk1a_S-!9hB zup?MHbE;Az?d+?#Y=#>@c4lXmkUzYwVSaY*G7<{uWw^wG&go!KLvva!UUZT4No z&5O>w1AQFyr!O>!I3w}F^)K!%sZz00w6xj^$(_}?DqQ-ji?2_cFIry@=mu^OYIoMa zo|b1HNya#&aqrzPcg|EpUT=TSNFalOVz_z=?tE-Ee%#>@&o|WMhj+JB;YG1_Z+&fz z_8DPE;s%C{a6i+w*D~6DZpwajd5&l%@Zy<>LFf%gYEFxQfsCHAc-X9Rd=H9*4b{qU zzE1y~_=9a{*woeY5Ut#-(h(k>Su`PygCnsMfZZqYNPdTTC?FIyD-5+WFf|b5zZ_Um zhxGQ7%i$^~sK?X&Bd$f3uKmf6?*hH5yLu9+`y8o$mP?lknIhLmvN7HO02RI42{~MQ z|3izY8|Q5$;oEo7@3j+1jHD%oBU1ULxeVCMa-o}m#Z#vDGf-ZVTDg?JO&`#^4=Y88 zvW819qch%dsqfysE0e&*umzkK)(72H$%_Q^BkUUs#N9`QgRI{pA;rL4#9u$6_Vo1C zvBbNNvqJ0W!(Hp!g||S5{y&KR<5qrO52UsCYdFLH8OvWj2OP*Bb6EKYQH9LkZ~XiB z_up_pZ9kuZl}rizW4~|U_b0C_OFL}bFHot?Y5@-T=np&m*G^R}pi22>X!_fw&!)Q^ znJ^tRUO4<~y%orA8HHf&za&>yF%S@FzGSheMU1VUKLn+_N8$8Edm)j&2I@Ml`zrv; zTW$;Z)}O=O8stPII5Tkq4#i(S33HyGDzU3IRMl}SE@1I5VzCOBvL1klE>QPdGzpRu zHzSuG8<`NY*}*^yuA3b?l^YN=`Dti0{7P;`-v1Txy4_TP$NgvRp@PL-D@&3}GV{R@ zlSSdWv1tFLo9{`b9R(o+wbPnyY6p=oFR$1$*}yjLvBTeR|LvBEi>a2)X4s>OGn?Dy z;x$_|qhh!bh;-RYG;XS6%hthVZ%nM^%`9+7VlCJ+QqsSp@Uo-Dwkeo}j!cMx8Z(?& z3_ecNiCW*vN(>F8&e(67%?ICpeE>$CzS6Tdul3+DnX*4=QfPg;M?75~eTbe`rP!p4 zn`SH0wYkWfxby1BaAOPoYsWq{z7o`DKMh+CH#i3Mp;IQ77AE6q{L+$Li^dm;J}`&{P?pyHhD42XvB81qcF-YF(lg({*O1!GvbpsOBBH}&}@B97W~ECnidR< z%x4Hb9iY3_-z3)tFaP#%zYIQZRhy-f{Awd1R{TKE%vU%lF^@_@Vh^^`$2L;IpTmGc zO>y4VvY0RLpa};u`kGZekVCHS8Me#SqEX4V_<`*xIa-aT25feBv|cVA8JQa2U~m|* z7LtnUFo;L_ZOo5NZawUF@%yiUVmN^j4kjliq~Fivs7n4IAKoQqcMD+QCMxIE_R)cD71pQJa$};`1dST_pHl@=fnSJ3r}IZ`DO*w zX_||&uR&3w<91Mr=sWJarw%iU=oz)7r@i2&enHI(mn?~b)^_5zMOY_+9ZL)LV*DWU z-_1u4m#*c5^or)NXCCJTpzFMPSaDGhtvDg>e9`{Ndj9IYAV5af3drc9vOve8_F_YQ zPjEyE2>q9v%Ekp-(wTCcsEZGSNz%gP)54&5g9AYYdiHh+NCXntQ<-rD8X36&yD#FU zCQ)iF>~7Ho%0lXq+7z%OLM79_JC&;6dNClDE)RVY>5k18LQQ9kfX#2*qC#3oxTw*2 zg6&vcf5Y@>s;$_EU!cB$_WrY{<)9En-C|N$H|OLlc;dg)VV>#&eFFM#pY)cQ00Z&C z?vTGZv$xNO53_&m5l5NqmbJCDLInB*Itmy|SkT}89nxx3_22sOAI#M2+a{=cjIq_W$@qCiggDeDEXh*B6j)5ET}n zZoJ?A)uWkz`_iR<*$^Nx`t5q(9|rsz?La8rzpUUv``_;j-X6pBPvrTXe|m1xBR}Kg;o;@~*n1A9`@Hi^ zOm;d=dZH3R*A~C&JeO5ZKD!V}XQ^7bHD3}cR9$70fOuR;`Xr(-6LDFr^5DtcV>3o) z_|z8LBMD;h`5p7eH3+4`kbia2J)0|?;VxL;`G;%YnX1mPPa!eUt3^4^jXL;-TFxs2 zRUaxojJBBbO@xz*;R)r~G|J?PiHfD86ZFEsJ^g~m|2&b!6W0P4?FDM6gFc?Yl1l>IzBj;f65+&2qvE4FpsrQqU6LyhQG^jfxU3T595porUNT5+BN_+|}cDE>V!p zPIRs^>O<#p0As1zW1%CMjPCrT&`|%~QYU+rjkZ@%fz3rit}nuk)WAYLX^|vtN?^QD zQ4L*{O`gCtyVBBA&x5|I1ExN@jvph#ZlT>*;Zt?iVUX|(Ut$AF)wJBJkJbue)g`-? zX7&mLmrgcItCn?b5Cy~g+Brh%soyA$UEEuu4Mr3c$9ql{EJ>CI`UMA! zkqsFTcP;5zDd&roKJP3esmkhe9zG{45N9{t92d-AZKMa2M$pSfk?P}*q*h5+UHWAD z4?b~iCn(Uhh@o%IE0G(R7JVTVE&XYV=X{u1_0%4JT3C17bi9wkF|{y1e1d=Xq6WM9$(sYvu&mo*mVzZIRA|(b)x#8`v(;vIdeTfclTVjQfe=@G+Wii5yTR1?#B{O)Vmod z>s-Xj{G{xyFsQT+*;xVkNo!z4DPe-Ra3_5FIw&MG(LKy^KUAKARyO9!B3NJQGz9JA zY&cQnE^pkfmdp}fnYIR+DR;h*_9ZUomXt{JdmT5H`wQlbTvb`ah8TqHdR03v$m(HD z()h$+8BwOL&X!(#?S0Mk#Gvx`mWUEJhVqLI9G!UuUTQ6g ziKNMAMMZ1aG+>j(LgmVRo)XzN(qAjs>#gGITO#AQ-!_*(+nsl6*g4T&|3BKkDypt! z+m;Byg1ZHG7Veth?(XjHP9V4jcL?t8?ry;yf(3WC#am<_+c~-Kwfph-;$yWbHLGg$ z-bWubh&(Web9$b%Q4(s^D4+xn^Cm}yo*32cm$+-G&l*vwNaL-Rl&<#>oRJ zeDZcukj`q$d&PxG#xzseirYM9hQZxOb5p2{9;H?o$@biLiQuBOT$IY}M$b2%xVUMU zot?56zAN17Ro+#NYI0wo1FLf72n8pRF`Hn9qaaHm`>}Z%_Sb}7Hr(H%r*dzGc^zgt zcjbD2L|y<`@acR}hFmxdp^!D#^?f!<%#50o_2^K(Z_c)bCb@IT_4J~<%W%e08_y&6 zyI}`cD($g}cgRLN`!mh+$DFuI^4lMfHydWZq`?L9f{9E|^g*Nv0 z3)VB?yfj7}E-G&h2`ddoVq1^G=8xF0u6}Tn@kE11aufxv(Zq|T?5kO)08jcB>0c3L zKuNcpxWCU6rr3%a@{hzuZ#AN8iPFwfOGeVAP_48GjU&e!*5(oSs)eXVrG3td4V9-L z>ak>AO@b&5iq_uQATqu{KPg*2FS_ARGt`SP)p6ycK(3MXgG=)&xA10fX63b(P13_K z<{p%O@btO{d0W1Slzo-Y@nN;{Vt@WR?bb2iqAaM{G+Mc_24El;2wT|qyf>AP6A+Ly zHl>_PmawMQwELL-jcwM9hjN_aF;`0h?B4(44=k;T)O9~J{9&x;@hcpZwH*_T$g#yX>PdAH1TFUa;iYt#>yxq$sv`RkKV0Rkpgz33a!TIaISTYO@>kpU^J(bBu$0zy+M6wI_hlP5V4MX#_c~Iq~#=i&JEA_3r z0;hM-a)q6rK1X|BZP*tsPWZ7as5Xu(oXC-qlG7RZ6eBl4tO#c5(CMhM0(9#1)q!XjVpzq3KD_g*HTtFw8y^nRWhlRs&N(y;R?N;;hHHn* z<~&o{C@`&>-1?r?rfxU0^&W0h?NHb<{ooC}_(zOu!(%zH-C;cm{sOtS_r5(lhALx5 zt$BPFUq-@c`wWin?U6BuZw`cYYyJMnH6H8nst?h-AI~{v$hv$!8tyA(32yIt8kgU0=xO3Q9gW zH@o!v3;Uaq*&gGuFRAZ+rL_@*`MlP3^@o+SA5-1^;m7Uni0(wtv)f@;8lZN@gOkX@ z&G~YE7Qa!X;Jr;FuQeyG!y}%okA5{#{`m1KwO!woaUC#2;)v^P)osSUz54+UjMtC- zuhPJdaC;+Z!HO*ZJ2g~{!riw1Ud~1H`ba4*jA(pPE896wtEg|5E-nYBh{j`ShLP4B zw!=9P<4T@LEb9=sN1FxT(A=M7aYVtMK^MIB8x~YsdZd5i@=8W^Tf)OR@u?-trZlRYwxZ;Z-^|Sz>#Whak~I zEE7z;7BwjcEDbgyTcJw}5vxdhqt4pqeZr4t8)rH@k^}W0aG84bBLg&ve_`qd+qZfr z2m>p!CpEP)D2_kVYO`l>xu~Mn0Qh6#Yo($C0DbT@+@6^+|1dUvPOq@jmJZn^H9Y1G zABSH9F^>Iu?Q!^xa=P%}xFV>@ClJC(2_MtQtTs^-F0lEoSeHTe7WehW{((QF$j5It zq<|fN-_Q0l5Pa6?wP|f6x3iV3oz3scvcByOeCCSjt}4a%megGj7;+<94ez{p|0nH3 zF$1L#b0nZWBFMVfyOnq=Q#e2twhOFtJ>1GPM+Iq-$D$zy8U@lXr@DK^R%+f$$;nLm z;Fb(Lnof5GLfvl%h>I`%)nWA}?Pd&1JcQSrxSM>a_d-GAd{b1cy>&8lMh-7C3`x5` zK4Kr&aX)qEvSzI&We#1%XghC!X0=-p1!dUIzQ)hh`)(*%X2vE2A^qT+!XPGvZMsdF zog)2{Lj)T7D(2(MhVfr(8U~fCMLtXm4M`ozv}GqcxOq#S@mPM+a1uYXF>Zi4o;&>bQ%7Ul zJ-#(=7ITi(xRNP=@tqfc!Y! z8!jpo_=gBiBTQkxx<~>Tb86TA4kt)W0tfCxaYG&)T51sEH*cOuBhR z)O>77A8)}_k?xLLaujc>TFv~JIVtxg%90cOQ<|*pGuSG%|!QP*Og`XN-xO>e!JW75KE{u3NAy_g$SH}Z%o=wmm$ z`(2W~=LZKDe%h5B#$M>mE)vwMFOKnB`-Iho(3mRa!x@yN7ZBWg6v3hOUE(d@o#tzw z9m$5TRm`rd`)jNmUX#vim3;YD%T%yG;p%DI>?!qdAL> ze;_ae$eh*YN~;a2R$2GZO(53++H_3`c5)<|)p5=j;zbu#GkSrx{J{X%vl!WQcXoZ< zbtlDD{`1vlN}ycag+|tmYuZ(=w}$An=1ri{;Q|dnF?+Cpl-| z+Yne9u40W8j^+iiEV~cNuI)z_Oos`yt~|=C%#A z1w2}qO6AlSUPcZ(kpafj)>mTg0OK*ma&GXFeD{mY#tbnaGC$B9DGGeZ6F8y2o5h)h zCdwU+|AD(D2|m{e{@<{Yl9))S4`^VnzBI*Gc~e~=+yldBZ4c4QWh4IBMZf48RX-jP z@+^C6*)#xk+z{BB!M}wj+Ec&G&cS`dyI4$4x!0H~u;a7A4DO#Y3;<|4tHJ&ZGDQ!h zUg@fJbJT{hH4tlzS#PrLJ7D5|^SaV*Nab1vmjG^4ODY9r#*>y?OMEV=K`YXzooOlt zY43vmY!g-a@MB3>X3DtcHnWEPo2=BFgqze48+5ooYHtQVPDqo3-Y~(xd z{vq+Ly@?{GGrN`_iD$M-aAt70u_wf??smE+O5n$bw1I$i4o@US5*1j5cXw(Imf<*z zAMlw6l%}Q@rApwyD)tt`B>A%kmyT|ney6I-`r_u<`+;)FD`=g&UZbS3_5lS2N#F*n z=dCzYKcb$#dTHONzvGHN9N8L|#ORG%dYw`w_3=i+r_LSct{G)71zppN)h%hY!Wycg z^2Sabla6~|7mVt-#D4Z~grGnlyv*D)rMr-oADuhSc zbeD%ZOXe!f;^azIDz@ZSfVz8-nicn-;#wP?p6a~KiZ$^%UngDWNBnP~135w)n^s<% zO6>Yo_$)gb=-u}WbrBKMM?0ljlIA?eP@1unmbSU;Dv8bM;mNOKq?~oSK2Y^Bo8r zKPHf`X|rv{_CBIH{vCVrf;gO+eKCJgi$nJAG)lC#TE1jt=NdFC>L2vTf+kYgZwf2~ zCvJ5pNJtDF!{RDa$}M_%H(3Da133NA^YdK(oqtH!V83iM*An0sGw)(Fr)nJrbXOgj z^!SQ);z`pNPbp&9T2G3*hNi`w0)w&h_u6=decmW#otI_n9)|P;l>t&m&>^Ua7v1Hl zO{*(=I9>ORzZ~GtO5{L0&mI^%01VCOl~m#eL#W178beN+4hn+B4(cecg9TJ?g7SMC zvZd&cGO5ldYfuk{7j{6Uqj|o6Qii?q%UVvmvDyyO+P~NjIH+Q-7JgE=*WZ`KzJNuM zY;-B)0D!u_9EBju#hPDS6!V)xk%Y@02svl;@lo_q?X#d`rLIOE5>)P~YQp%MX!h0M z_JW&3ZflnE4{jRgT?0Q83EHVy*sfE)IZ7KFn3lg>Wt0>cBGwd~FbB`rzd*0|C%9eUrmvC ziaWln<-D>gr)GDXGo(1 z8ohetzsbI4-()sA?SGDZ)*piAQQRJr-DrjHFg$hT|6gh14WK;4eG%r_xT5K1q18o==i?~$iI_rkjxFzr~kiY7(sG2bjPpcDHzx-+<&O^Y*=9Y zpHx)lnd?Ca;}^dC(Tor*;G=0n4)){q3%B{7p7ei8Zoc{(vjk=TG2C9@luh=Z74zQ% z@BdwR|Mi4mU>F#Q|2UEW4D9{;KP}_`Db;Ev_H$I+uF5X{M5jfYDVD~SiHOMDH5V7+ z-8W${xL3EnV1Hu=d~s{*L(j45HlIP_3gGsk3bQJ`IW0a*o0_85&1&MF#NTzXnCnaT z#N@~s-0aVeiMPARe@eIjL+de$<}oR_zr;6*@pn7&$dtHNeX~jblGn=?1m^k2R0A(b z^kBD2pT(`Or070Yjmk9!24R9Qoy6vyCC*h`O6YJKojPVb@*ql4&*hz}o4<3No(I7A zjz)^!{cgy0XgEEQ-h$GjzRMwcJ8kDAdKVq_=ehUrAnWOnD^>h>#QCzZRzkA_CzZR7 zX=e=9$xybS;7CvEvdL7ubQoIvfDl<8Z(0gcC_CJHr9UnJV90Ekvh1Vmm#7P`Z6@Vo z{Q>e)Cxddy9=*D}zi%ihf8so?fpLmM1XEnx=v!upEW~bovmUUU?3k%+o zNS5u0(J(yO4__el^c1O_hw6!EFN`1=vUw@ck^d`Yk;4{5%|o9zprL8Fb!m5!MIe|g zB^q(L=*JvaLREG<*=6G~&P@p6J>wJXl0&FqU?8Q0ExnAZ4?4{pkuYYs;D8EAuyC^p z4cdR$kdIG;Ykaf+?HANPad1zn)CG?`591}}Ku(OlqeGOyHKYQCRfZ#oZZcv>zc*f; z5RF#8RRwuXX6>?V83fr3m)W3JvGGI9!=X19mkra)3&BGl<<~=df12|cYPhXhT%0>o zU3kmxJu#@n-3#24ga(Y!f^QruznArk#hBCgNdf=Ub7}|uJgwOd2OCQAIno`6v$iAu(YX*up(@0XvP-IrS_WEsq3i9G84d<5#^t!!_6%zG-i zJc$rOzt&qolm`-v;nnA;v$E~RS!52?YaHqMq3F?B8F~V~LN1|{N112_ zKXOI=!WB|0=gy64&fRL^gOt+%C@Z5E7T@sqjGuz$aAiXL+>>_H=dU_fxb;sSQ5JH9 zjSj&wOHtYJk8@)lkAr(R;;wmTW6Z^V-2t%Q$KryaLI7l?1~pRo4TtJgId|+c%i2+em2D|v$5A>?dCmcEj928^Exmv&uUw<;_5a*O6wNO2e z{S9!Bi4eKesn@JjKG=9}?|5vEQ`NC{wMA*rE1ePkE7(2nPAAD*TXLKS8m8yV7>u-9 z?pgbxY&&tN!++R{m}%s^(~XMC4Cc`Nb`z&EXIBTOkKbz!rjmJqgG$biT`aK{E|)Rv*%3@z>0i?v}=@I$SFwFiXdrU~7Ja!K*5EBU(d9^FxBdDw`8hEXHw-6C# zer*FNF&ngh0p6nY&%(w|z-CivMN6PLloy*ZBE?k3ICp*JWvFzh zq!XX0L1wmMq4c;!Mp@4|RshyZv@#^U>vHS#Jv z^QZk&bo(84S1p$HV_VwCPj2cZ#wk+qwJrB*PkpT1&$^}eN9EZc#?9`!8lESpeLm8i zX{+y6+Oh$`fKV?Z_dkn7oQp%gOoc@?&DI+rW?anG753`o>fNUC+gGog0!+CtNf06V`quUu zrv}%n-M}8vU$%_bzeV+;w#GxJacvSJVK?BuiBTF$%8t-!8rHaWWVxC?uVYbl=~2%? z@iNUEU84qJf$C-)M)oS|5Q_S6f9ZKMkw4Cyk$gT|TUtKs+lV}B5*g*HX+1p*vd#wXGcwCmYE{I3?L&ew55F4X|LGn|{`g+8OsB?O9)U zZcE2Q@=owDDk{$AXS2XqH3z$8PaXd&k8hJbh&(+@Wvx38tGw3AiCdG|qhW366gO)Hr}&-7;dOkg8~;Kv&}JLx z4IHjZE?1Y#Yv6s9I?Vut>V=c^W-v$VqHJ%tQqtU=5$6uT!^11L=@owBV5!NM#9}4l zq_}V9j>|Sv!p9 zz5dzgWsd&T#{OM5*PNm29k_LT=DO{X$CcRZ%ts4YVN^Yv=UVWCdR4W)>b#9JXLl;o z9q*k6)M;+6mM2Gp&-i+Z2d63`Zg^%~6}B^J_~r`BqwXUg^-K;|S~l)y%&#h(Cs0TX z-?2kcuog9}Bt?}q8isXOS1>Y>Zl1p;UV=koouCGS{R!qak*c94oL=W;KR-QGXDG3F z*7aWqi^#s}Zncjei$^;N2CSs4pm82Y4ufXOz4T2#Lj?-DKbC3Flb812P^b(B5P=`@Wwu?GgLo!H7S*~%a zW9_e4bLPw6^{5w2bWAC>bu8;8hLcM(q3FwKE64+?$t?U%HHsAylG4cAo!V|>qqUoCfHQXQFD+BlAX&&_jK=?{y|yH!%+ zpx|WuB$PT^ngTM9AA3T-H@{a7uGAE(ojqi+DcUlfik9DUhoS7>&g<7GiRO+fC3t#b z1G7iNVEEnMS=L=KFmU!vZL-?Bvvr^KliOVr7dL{@*i9=)1jR1jXp6|65HFqNTfwXO zq<`yuPo!xSD=r_|d0FXuEvXsEEK@MmH#f+| z$Ft^}liSsK+=XiR?F6i-=+12OWFI|*dk3*RVOrjtcGi@Ai=N1F`7oL^Vz3ttzA`Q! z|J+7l>+^H*X;~KLsks-XB?fv}T^t&2r{y;1<}Fe`u*>m*OAsxqyP&Ngnx7AM1^AsVuIViN<9Q__|hyl^dNJ*nUU2)}usiVn~? z46Amejm^y;&X6OjjaQ_|m^~VpjNYSu;Q88&2=dM(=r732wisd4hNiF(iDL2v&Dbzm zsF~g9VMb}7567huFRDZ3e%~L{M)HqP`ua5LCzEHE<^K4%OUNaQF2C7I5FET-e^{WD zS5)4HFA7rfBzGXsxO_x%Q(SBXM{cU3dmMr^5`DS0;Hw(-5HbQj$63OAF!K(4bpsO zq^^kRE3hV;u6-YQ?Z&Zw7lk}9TpvxnfT56?tsYV*R4s6L|n321vB)Ejqp zc4E~=I`dJHqPah81sv1Fu`oD;_wtz7F$^FVw5OL-5m(?+%30U#ekDT zZRO*3gV{ak(r0^v2gce@A(O(^#U(_t^j87Mi1(NY9+AI{rJJR8l|N_OyvaAt?$^yJ zlL`yZ7wt%mgir`(FV^gsFq}cHpqhDBg97^)0;p9sd(mZD7dAx9b zlzS6xB7CO(9TaYl|GP#~M0MIlbL>!@7j#-b5sna%Q@J*5D{%%#MP z14OmC=DL&WY8Xs@RK@l|{nwh7!?J^v3Qp*{`%`Am1J6OS-b%b=+#ZAQ+5Bsu&}d%m zj5Fmz7v=B8xtbmbDrOt- z@!Uc0Of6-YWv5yCSSA;g;?A8=om5E`i?~vsL<^_laqHXG-DVI>={^JZ2Ag#+`brQk zPZkGvikojE0%N4~iRaU_!d2rD_IrW+BTqQ^mkPVaoCi8=*xCp$-{RuiSV0(ushu3M z-YYIGtWRm2wm>iz1+0CS$2>R=P~u^DsAwN60!}!vdpp?;no8cEknm>7;mKMq zs^}od2?&r=3h~m4E{r1&ASFm|Y>PB&+q$O~_$zv@;ifSp-@ktL`&JKjPc)e$MQ^Dp zPafw4tL#E3J@!4sR>SJupBUk*OX;%Vk(qV6WxrpRRs7!|D9CmT_owy$ssrJliS2*r zgMZ<4Fdd!$y&V?>%Kygx{^jcZ9kqgDV4r>dOYZz%*>V4W%AFud?LP9%Hz6<{BAmS(EVU*GBt+cakV#aaHxQ%lSjEP{_Cc$Z;wo>5}mM1 z*-q)+f<)$1sNYx^C9mMH0o=8P>qNZiK6Tvf&{`b>`F$Ly0H(lfd~j`gyfAb3zHA_FN!u zSo2;(F4hkL!o18>egMi=tB!H3O20ecNE^rUhe(Bo1)rFZQ3z#^eS5Z@pj4ZW@|b$1 zhg0m1`9;%m?vm=l8*!NQX_q#G@DwHVBRYCGj-RUz%z}trn-DO-S1&yHcbVmmEJ}ix zOpt(Z_DC?7tn8M?nA*j58|@8$q_kL45n=xO-g4?S1>v6kl4W9cTQ%{yqXOe_!Ibwn znaoM-qMf6be%Em3AXn9NPL6wiK>)Ixyr~Jk-(=4C309R$kRhcbif8ZYetoG%Q{D1( z$FQ~H5S0FX&S|f**nZla8olY2#31>$@Pzm2-G!JBxF5?m>cNDU! zO7$Z%GTPrF98Vce-12X195)~xg1Uk2c1fq=5^}scXk{DavyI0W<-ao9^%pq-d7e4bHL-Q|C&IJu7 z@JkP#x~;jZ?p7j_XVJ#?_8Fs^bu;dgaYl6u58P@gwfLe!F2!*qNQys^DdsS&gUV1R z%_>=3S>(-*50m@-Or}BAXQ{OjjrW_0)-BlIH*2s?Fd6hlT~Na_C~sjk{u!BqY>@~- z;VTZHW?iGlt^3^daJyhI`6F>B{4Wx27d6Y`{ht4CZk2apD)T>pkxqAEsna1O>QeCu zhfOXVBC!l1gw*5nCbddao~Edr{ZP#;IzuolNo<5u#C{)EtSO$1Z8yXhRu6WsQ|r6z zo%otEM02MGhNx6GJ^n;EQ?d= zZTSTimQg_K*MjagZ^iPV@%BvQJSrVlVy=J~q<-7H?a+SN&k0bF)b?M5I@9{U zCqHb`MUsFnL&w{O5(mBX{uW|tlMi`S0ae@WRc49|U1|qrxbj}j42v#)x)w+Alr*l`aN(n>)0<^IP-Mz^bIV47B)oT|^p}g6e#eqYOqMy8ar$r9%1fFhY7YyeukOnqKnpSyI7Y*C+Fv*hxzZ?CcQcjL%e zR!xsok7%vx1&gZqw%ZG{+{49#Ffgm#C0(4qlz`8fqFg*g<{PCP-{sSP`MzQ?GA!PLC3H6tfNB$6Km0B4bFZXfb>l9a!is_2Zc)*=a?B|t zva{ki?7f2c0l$X{LWl0fkn?-3%6?PrY1v)MXvTDUvkm-|la=vN-o?s>AkeQmgWVGR64`%;2ML@D8!kF4 z%4?|yyTJ1fin1=dS{8AuFg_8nb%E>n-nq|4XphO$Vc`&Ovv09^S<5MV%^~gfOz-lrZz3#gvduwSYkQo zeu9Dcl{oIO>uauh-;r+h^m@AWQ&EZ4P88{QA3L(}Ba5kniB89!Qopyi3(g5A*dqcc zV*U!+4WD?*47t6Zo-QY(myd7@nQEQe9<~>hny2jDg;EcPj<}tH!wz*T>x8dA?c=U{ zd(!pU8yf_%S&JXFAE?=Iz50(!$u6(*9$(DuJ8GMU$2>$Y9c$e*JpuWc!1z)A6rlIT zZq`WMcZL~hV>5IXIj|bXyggmx8&(K>UICnToETF$b$bk#tZ%e%65fXRAHf;#DNFJ~ z)d;)`R#s+q(c1DHT6bA{^&{}5j_tql$I5O5ByI|jGkOOeuKBe#Yo-ZHptW>;vrw77 zdtSc4{2+a^dRt06-eP?Yr>EsgM@m66a?!MUo2uGYoo;y&_8#qdJdFNd*Z2D^aXEvU zm7~xr?_5^Gb+vbf7A(Sca9v^SKok)CmcrRD!PD^?eQCcA&(h08BT?p~Q{n=ABug_? zqcDa0<(N9d^g8&1)bqjV-MHgyBvvpy_UqGe@3)OBD$@{eA`Mq+ekI!{t;l}OV8;vP zG($g#;*qh5VSgRn~$+B4Sq zfw!R3d5-_p;~DU4J5cQQQzfU+cH6^;ITzf_FK0^;lnN_7ipuC*5)JJ9wKF4VnApYA zOUpgnG<+Nz|*D)Cum{q&g?{u7HkZWb+D zbt(%@1g3z)UfrU9|3-BBcuOrDyj^fw9ht_PdeEjB`tnZuMxgo^UWB*4s`~ug-c}uL z)Y$PyGS8IC2mMr$hgWDUVwii~+@R7F&4uVEz4-kuS)Drc*#2#!KCe(R-GlwYMDqKt zIb+G(B`5VG6cXZ=P_l_Z7$fpC+UCr6F9mm>J0EFUuPb(+!e5)?!u5W8mi4k{&^dxu z4ja0X$x|=vdi4qnuL_%Ld=Tg1eX5eQcR7K=^O{{E73G~p#+YglIc1iqxu~R;(Xw+o z>q}K}0Vo$YU*ubBw1V7NxXya1;?w@i1EBN0jj^1Ydhs;*TU>8ugDK>}h;Np1+C{o; zf1xcCSch=K(>vhtp(%53oblE3`08B;v*XIsb4m|3!5ptFu_CgCoS033>^WN}Zz1Zv zT)|}7WWG)@O74NlP9#qn&(Y)2#M*4BXS$WUY4e#wJm^F>&0eKN#Tv8Fd76+P7Kz|w zy73CZwV#MXpc2iawXI$UVE|1)r?k1YyDX%{*pGSc_$_UDv9H-jFA=@2TlDnLNGA6q zh=`?F5RG|^Q$R=cvLcCROChH5l=THwhkFdWqh7C<R8SYOS%=n*YwZ8kZ(=&rK^AzfEj!o$R6_!f129(WB z(=C`0D%uJ|VLo7=%k(W(V!sGaLPngk!1lDhCbUebKtgW)(K(EVikYFB1GmQ@64CUe zev?5O98?{`1cQEnLOASm)qAMF042Bah7vopA&|kKANnfpO2|0I!IuR<()TF4w|l9$ z_`#PN$OUk}xQel#jI})Z0vsjWi%Fu-I_`tUAA22esO)X9-vzBy;j8$$)DOZ5>ZXm% zrtvhBah`{tR#gz&YbQBlJ-HgJ{iwosMyCpkPUSK#`Zhm@zG@=ZyI9(5zIiK9vHeQk zXkFUg0M|U|7Qjxf*}x_FlY7iv8Sf%&%&tOjgs3qo%_wbpMaB-+So2J(`Zx*4TBcif z`5shVwA&&*fE2$)eB1Rt@VxHB%=UJu#;}rK5bNeNRrCOCbHv!Bh-u)u-c@ zF8%YvLTYwEfPAuYy5!H$v@1dD!=g~loF=O%l(Ei&DY^E6=Z*poi4}H-O*#ue)wuc{ zvX_lTfUj@s{xWz~j)P<8!~Jf??yih%EeC)zEcao@R7&w6K9{nk{5bTTA)f#|(ONj! z%Cw3P{>LB!Fdwj_rV3xw5*<_-s!yY~>;0Sv2ADOOXbIvC?#(~8cu$w$s8nD{i+#FG zTj@2>iAY1H3%UBGtWUdSMn}Y=9Ya3dQqg~B(p=g?DO5Trx~_>66g9s-IIx^6T2Zls zt?uL!o#U|=C;>(ZxphC07c0<**L}ErQlL|w9(1HXI)msDtqa(j4>7RAT> znANKtxKU7}aMspkCY;X@xE>^#GF`Vzja_UgsrX3I?O=+fzaA(kC?vT^y$8#^aM(p} z0Ux^Jbl@7Q5B`oiahz{wJcHNf$L9$XF8iPFy^s9-3upE*8I1&lh1XaDlarIHsu&{q z_4TuNGKKMJXvQ}2qeOfi6u^It19)+VDzgXCVl-KYWzP?|+J=>^(sFIjYr$yA4rWNu zSilwg_+;;Y>zuRz%&QDRyeknJK4Yp?T6T!anWF{n=WqGe0*6b%XO2FBrwH^T7LR&x zl(bxa@m)z}0{SQ%xtxWQ%r%+R1?Q$I-NjW9Epjr(A5*cqXOoSR4*NA=^J8!;R%opO zh~n4)PSQ8TMtt{Gry(j=Iz{W@T}1;U*C=GowF?&WvO4GBlBpU~Hn2O~v3+D>UQxpA z2RUB%x+K#E_DGKCW_x{8yeko?A5#r=^y`;GLqo;I#5mAdSXkm+Ws7H2RaH0p!eMH< z@2?KRh4gp$ve6?`UPglkqkk5bN~IIDW}p+}Ox)YP)q-p5z4I=}ouP3%0qmSJH@wpY zFYeT3#;?GHTZmQ$SZf2Xqj{{c+p&&=qTq_-OeSkXyu@-CYDqcm&tWJ0MWG7Ckp_&G zfkFf^DVRjp@{?L|j!fF5W|6WQm73SUdSLeJr`wT3lGXDm!Miw3=?QUgiUYOD`=3)A_sw#sni~{^t|KBb z>o|p5E2+8e<5JaXZ0g1JLah4WzP_29Hv?Anfus-*^FHd{Uxz~o`h9CZXhU4}w}Cmt%JJ#pG| zRKnz5wAyx~4i17UpPH)$N5$!DS5|MmD_08T|F8!`&bHr1r_$oTfzV`OIShZUo;kIn z*|M)un8}?`(ALu-5#G1}>(?_>GmV&wmwI_c7!j;b`Rv4>Jh8X2B3aF_fjo&s>s>MY zz#(uLG)wBfL>{a!w|P)!S>80gvQy^USIE)yGy~1Cy8Q4uYQwXXxmra<}AAOMSsa1G{_t)Pi zh|OqEd+9G~0#^rz@dm|ukdjCV_EYPtjN(0>uBiNlusqr3M?F}qs z05LQ?V_Fw(Z5AFYEjM3154mGK&-M7)TqeFlOY)5LD6HY@Ly8FTnHKr4#s+t_ntZXz z6cX^DiTT>cT?8myke6q)(&X5NKRgsiMMQY3nECv;!&_fdb8F{+==88yYkqKWz`b2q zSP0K^tMdZ~1=_!Vy-#Lry*rTAdR1a|Ta63V*W9mEiIuZsHr}?eAU96u*LNr=^)n>N zO2c+H!=cQ~cudX8yHv=(e%pFQSv!BmOs-80@n6#u|#^m-E6-+(o&!z_LG zSTHm+Z2NL~>7f0!x0@Gu78wy|LV`5NILta6sR%eshTqL_&~o zo?7zr`AK4%n-cItA*8{jYh|GN?7z00yf73Dk@piVnGqziMq{b2z{fin^s>>;qVjTd z$hVo9nTUvpX0?mxu4mJGHzT+iU-&YEA|iO~e@cjp&!NI{OOGbfcPo#|uR9%kPs6W4 zMis*R+M}MV91BHleio-EbJzN!c&hvL${&&O=C>rANfxLZ@kocpUjDRK=ymj@ytY+i z3(TQbjg$-iGMPV7mm$ZF-mwNT{@_&^EEL)KvbBTPqJrrJ(Ftr)<1Uf80|68i>^46> z_RE9T0se_cyt1~ZVy4V(7f$;9(+A_dM7B*%(*!^uSBXkxmcH!j{r&xqkZOE+bP(7; zExBJ2Q+X1Uc4sFW$!qquIa=>*eUw%s?UlOH<~4&}%V`M=Mnbw@c(>nD-V>KybI;s_ zw5vS+eI&3@mv!dak|y7Qrc_l-v3#np+j6v8i)=h}^&AKH>bj$6B6(+T5>D+#`TjT* zk^xJt;d8*;7s)yqlYc{xxBsm7Xj~c3o34*(v6@AJ0!5GJRIngk^cZ z^-52vczXjHXmk77Lea-0NQ9RnC40buy@68M({~DX73H?^;$*wnS)yioa2IZ20feno za6;z01#=o`f<(dZ$qKZc%rMZTvV)4Qr^r?-BAga2@6+hK!~fa=sK7c9bc6a-J)k~0 zH9tMxT`;z~K-=5d4K%BX^^ZhFu#@^Eif`DTni?1w+?GyN7TWg{GIaW4{~BzanG>Z^ z^kz#smX;;U6!T}%b|Adl4@4e>DMLxL-ZYPH7+CH?8{@s0n=+VkVqi3y(8MviX-2T0 z-H>N6iuROwJ83m*rV)_3iC>I`5!W-)3D*+Fa$x6sBiP5XDho#qh}Dv|Rpl4^ua@xzw{d@RXPK})8P2%l z0uZ|$Av`@~Gl@!A^E$8K59~~x_d4jZKFx@Qejp7HX-<7!czW%)aLITro6H-C=R<{7 zw8+oNn;CC&AGs!ftLtX_hL-V)eJEKd&$7k}UK?Z!VM@qU*8Voty25mQdc29{hg#y> z0}VzefRT0Zqp!F34sO-`r1g0JaIto{-e`#XW*J!~FghBk9d#^~-F|BTaUA4RF0a!Rtl<0bm?_*c8Rt((rxT)1tm7)!C1O!eFF);B)GFBe^0HO= zu(SzfUd%!9i3hPAt zYqT99^W6D*rj|EJGL{W2h-q|MfrDWXzMr_GW;7Zhr=T#kuqY`hS*|gin#>jF_Agd0 zuUk55E2Ij9#{+?_Yel%8yfGdKZSsEmtcnV{wbbOXPw}HEtPgdVVv>@uO^8ByG$k)9 z^QwKu|6+yZ>y7T6x;EBCDvH-c{1|NHb8Eg*_vY?%-IMNTDa&l$V}gXF)#tD2;`zb} zQ$1`PPXxGhXNYfCaO@N(IlVK*1%%B-uiQh@gxn<^#&MwSze9L`o8%FIrb*hZsb8$0 zF*yCOOrM~ab9rZ8Qy%X4pNkIM5ky4j;r`h(W!8dQ{&rwppfVyX}#jAD3G&VR@#I z!@Tv4j*cE08j1+Z1a&6^35y9>>xrwJFrpc zwB7|iUb)tq0aDgJr)(M}+%?FFpPX33aBG-Zo3HZcp2aeRYgcYR!QavO$OvubddU3D zwVYd2%YAf6ghC$NOPjJaUnU?!l-($&sFYuaj4zlv{GiS}!)X#7AXMqG#rF(vwzZqm z%XIglNKaJEVT4oID65`oD!5K5yw<_Tq$S#~jV&}h0(ZYDVEBnQTU5Y%<>Sv)J%UHb zifNA;?wBB=Db-{ie^NRHRmp^%Xglb%_xY7hJZY46G=hbDRRMgs9w;x3sldJjWj4cE z2?+Frpi+Ub+uG#hhgh{Jdgy z)1auNzx#pr|oS36#Dz{b1!0g zB&=SU{;32i)6GB+weZN|*|xG2PL1UwdBT(Sj8-Agf{VM*lq+blO?`+%p~}=n<1i&f z7p^s$G!;48>+Hh2@h_{B!!pkkq9CEk(=U&@Qd(=EnsHeC1kEg&E}{XL!73aZ3e=>b z3~tL)GuK}8e0jyUZ@MxcH$drG<#H`<{k|~ljHaD*cP?p134IDGs-+rJd3+oYBB{Y9go_%Mk8*D6*3pcYO;%C(lf9`6u@VPT1($qa8DxlgBr zRIt|F@iT*wo744F+_$z%lHiv`lFV1!yl7UwDX0(coc2)Np=wF2x(p5?Gg%Ot#yQ*_ z)J3~7;)%*inVJ)ZXH^Qo8aJ;64hWq*CD*y5`-^5w4JVMUL53;FEsaUNmpqr+x|_8Z z<4n6x&Jhbya$`(vff-uP%pBm{Yl*sNnaI!o_qzB@&{aVIyzdXkuAdf{l$6Xy2Z41& z#Pt;R==Ah9rz5pBvj)(Zq<;bJQ(hSg8|*6}40`1Bki=*lCX{VGqpls)FR!Aiy0_%L zh;9ZIuL>BL_6I2j;KjiDeO;k$|94(up=*k2dfoys2{I}T8Tr7gaV-|?jc#yc)8lN* z{?y>L&7_I%U5(QoLBM;xP=;z$o zQ;11Cv?2$Yg$qQ=1)FtifUo*~E(ObQdY*jQ)xXDt!#@|j^+Z}^9Wo_=#hbbg^ z(lRE}`MRuYfI@K9$zz+`Q%m%sdKZq$@(u}f9%_ij;Vcy0&yB+%fm)EG!Dsbay&iQJNK+QskK$V?Ns%han-gcx{q;SaWq{ zTlo5;gO)2=hpf5AES#f-j}z@VchXU3hF+ox$8@ejpKlQpdBfRY6s@vvcunP>e!gX< z#V>3GeA%0Xx>u5b#%_nS!n4LATlob6u|6wFYM05-Md$x(YJ&u z>RCM|!u$G!#w*oI+n#jcnpaj4&F-|`TD>Zs8HKAbWlbM%i&uJ4vA~P*>#95$58-Jc z-EMzqrEZcj7H|&n=3Cy~#==7{zwVr2cD-DY3;Y$TeLf=+ftu)kMIKJ%lYOMsVR_)0XYWelQCwsJ{jqvh}e03JUb46-EepDXwE!5*f#FZ-Owm<4$ zgS|)m$~sL(j5L_cY~H5r^DVK@?g*RUpNtqx={ON@Vo!dB#k_JVL+iY{PzHU7cjpT= z;8S%S$B#AV7Y?R)s{RTr0P?R+vY)<~kG)=xdgJ>6t=zKmV&Nld#*`<#%;9v|_y=C0 z3{Q#L=N^%Fh{u)d={LBWzxKkH9h$%m!+X&Hwh%_3hiA?*Do>9VKOS?jty;UgOap&vdX*t@pR%zlQir-wV9_b%R&G zU`!{ajnX-1T2iCcW22MIrmJ_FG?zFs+4>6>eHZ@1zd)^|AxG-@|FJmmOP41q%OKD< zH;oWLzh2qL+o;D?s=$&^0)?g5v&NvqO8+Tfno!LhqTIOn@22rWtErzhGA7yt`f*gJ z+Y6iirD8I?kc?LssvmXQhzS3|(_UL|EA?xDx?9Sc>66SJ3gRyb(V!O0wnt zBEfGP!XLsJrX(CA@<%VXTM6h(&Rq)V^k!(ZBmYota7-LuVeP1idzD$#`3`z(yV_(J=$rfXY7}*`NZ$%{Q zZ-}mQzUTb$oB!sz<};t?ectE2-{-!ccWF)Dq?4yLbVABh(}@hHy}KZP?9L})54%s6 zS4$i96NJcz+Vx**1G13iM|}cFMfI!O7YSjOZmlc`qQpkmG({+&aVqZg8;7})0XsRg zqSr-gu+S@V6r{!KwY=AMA?pW>2h8eZX|KOZDtojmRSlNb&t2PTH+gKE;8w?FRX7o3 zJvMee?vq}k?-1*f^Te>HhQ}zQMx>WSm(zAx{TXP`fX(Z)1j2Y+2&09_*IX|}$6Rc- z2e*c}y?)`_jmwx4EQgX>SvRa`ROlnq*SBR$TVwYSC@Xb0ZI30o+@8T9pK2>&w02O7 z8NG_`rQr{JH|UytgLu#@PsPjTqBOXpNVi^FKvKHC`Ko(KWIIYLeRaK9~C2p*414! zz=%5xCov>W^sJ9=@1788F?+~I#Nd7~HqN&VC5>7(KkH$7mqb|RkCwB*A~hzLq53I( z`BZcGLBcvCQ4%-(40L*M_H?jAS>;Cyyri~6O1uEi4fI6;g@v9BFZ7&eK73V^F*t{Q z3Ol1yv5VRs6)T<(NPR!Pc;W&l7wb|qnRQu`)d;J|$Aug{3%z5#mYrv#TLhxa6)J6`twtd(?R|Xg7Dk0rT z*D^uJwUK=e*L76^F)qNJl8ThnotjqAMwl(2vF4(T&G;7VO8Cjn9Ct`mLehL2Q|M^Y zj8?^FbSoFGG?B^~D0EMD9{>;W>BuPD?N8kUbMzh%1YUgl)1t?#oheMxr7O+Oty0U8 zk=3?e3Ib}JAKWTK8^6kgC7)LstSi-c;+Jz5OIdL-$+kiY|AJ>?&0aB}8~Ft}oVisn zAbEO0*uTANWyO^d$n{GgrP6%O5Gg&|66s~#oWTNRdkzN+IUQIc{u?R(STZ6Jxa+q~ z%~~(S_6T{eV53k`E&k!oN-gPS9E_@ZR?C6lyUkj}wCg$E&V_TDJcS8s9^t_bYt0=ZX)j?1A6oiy@fTMRVTNEk@G9!3J;VUpkF z^*o^^tn&Ma91^mY#=JAAtBWDwv5`m-FRTYCH-Ew|O=U-(_c(=lC+kGi;&)@yN94O;awaAE}3Jb#PE&p&;| z5z*@KRouc?y~KK*G#QI*8xx(+qAs*ZW#sy57K)FUK;m`1B4zulHJDc1Q*3QP-%m!9 zcWQ*f+BeN^O;N;}|*|6(GY$7C@+RxXqf%&VvzH^;C_xRZPaRCmFBVqGUB`lcZUz9uv&YJI1 zVy6v*hz+DUPc&t6^KBK;7=9Hak;aWqkRK$~Q9k*VapzFNIB-3=b?&J0p^mEMN}O7n zwl^PpC~d4VhqUL?;Gp>j-aL?cgZ()G;rLYX#dAM`U>)z~pg|yVLI_IslW`91LfE`c-@WOpQ^B zG`TJ+9-&b0htf}m<=@^4M4S5om^a8(>)#y(I6)cz`5HqQg~B?TWLnQ`{8ujywl|$Q zs-Ai|>iaR1v7l3;sH_TO7G?kh}HZaT%uZL>n=T`!P!Y|qxaxV~LC z>RsvLpSNkwRuV~$a4oqpM|rUYDUbp-*z#^`<(b=Mzd(NftH>}JE!{CyD&|mfFdb3+ z!!W6{0hmeE@GQsLw4fwMuMIinN(I%(0XHPhuCf}0mB!psN#97XyyD}knt%UAYDX`D z%`2lgwasKxtl^EY^Mz@F@E>dhsJ(aT#|^o+2C3U$jx#K zR#<`9Q-rHg#(eDdx9s`G?b=^#C?=l1QB#y@r}h_<2lPX%GIrffByFC*Q1@Ud2YJ_F~OMe8ahl|eVKeYkf_Idu8129_P7EcRaWR$c>@o5S4MOG za$2cva7RAL8DF5Bgkn{TmZya0x=gW%@Nwn!i*1f9C=M*8U*@!7tm^!L+cn6^EDV^ct2{arVJWF*HqhZ~;~QYFtr_07oPE6E2;Vpol$8_KBc@?zD2@||PbBvZOt&fWZ-`BiOFOJXxVeV5l{+3T-1jkL41U;_K?`tSM#J zokcg!02^Rub^GmS1$W`@9MP)nVK!&y8Fs845kp*Yp&y}^8KO9Ji{J2+zfN3%z;l7E zGqpO8u4|3iC$BYL&%e^XRtDW61$74#KU<7URC}%ocg}oagR7W7pyzKxH{U>2Do9q^ zimGDPBS}mmIRgp#ga&TUSy?}M%Ea0y?grFZH}j=23-h%zQ3&_K5vHXJ&9mA;%9z=U zC2FjehQmzgjlB-dNu}=l1d;q~i1>pmd0PbqOG*`zs7AlPs=O7mpx|&foyDO^W&K2v z7M2j~M`L$s&_t7XpZ+v!G=+vOvW_-=tqFr|1>enV-Hrj}bz)ADZ;<$nKkt94kZ;Kr zm=DX;vH-)AM6J6s0I4|@VRzk86ZCI;-PL7;VLg!=>ffdYVwHjH);Nf(zh#c8mhB`^spT)gajnv)8W^eJ#BZoKWb`O71LfDH=9I23NBLL)aWCrV|y#HQ5_@j zUzrzj8$djRZiOMYee&KnsrPyO$iEcO)jjF`=)sy2B_+$*Gb5m@_?KY68#zQN?x={E z)7COwc&mA5ddVoCVvuuDd9IMr$UTIXCMr#mH)p3WBq}1pbo*V!VyCUSqZ{Z7e#;SXu#h%<}rN6vgrAEF<2(D@d3mP@+Tq^%U z-6=V@69o!$>L0rKpP=`iQRNoZ^>}D9gv`y}Ht8!Me80gt0YWQ!71m0!!6@=NKBTqUh5%pTCNh|OWc#X&CG!aM zO(Nl{X|*0ERj=gM^-*%NBn?(+U+ea?sCI+j7Svp{)^Tnhl0GiZ+xrCI^!*K-4pQ*a z5Nz{K?jLD@Ztd`~2x0e=6wJgLztNMO9A0-uXsD~m_T zf6`O3v`c;KfVdvYO-EFMELl-KB}-QS53B#th7eQc<1|$N-pw5ewZPc z$>A7)`Aj6MR9PS7KgRMoa_wH3VUBWi?-+n}JMh>^fXf41mXWd^?LL01C~7L?$(cO; EKL8|sWdHyG diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index bd53af45e6..0000000000 --- a/docs/index.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - n8n Documentation - - - - - - - - -
- - - - - - - - - - - - - diff --git a/docs/key-components.md b/docs/key-components.md deleted file mode 100644 index 9caba3a0b0..0000000000 --- a/docs/key-components.md +++ /dev/null @@ -1,25 +0,0 @@ -# Key Components - - -## Connection - -A connection establishes a link between nodes to route data through the workflow. Each node can have one or multiple connections. - - -## Node - -A node is an entry point for retrieving data, a function to process data or an exit for sending data. The data process includes filtering, recomposing and changing data. There can be one or several nodes for your API, service or app. You can easily connect multiple nodes, which allows you to create simple and complex workflows with them intuitively. - -For example, consider a Google Sheets node. It can be used to retrieve or write data to a Google Sheet. - - -## Trigger Node - -A trigger node is a node that starts a workflow and supplies the initial data. What triggers it, depends on the node. It could be the time, a webhook call or an event from an external service. - -For example, consider a Trello trigger node. When a Trello Board gets updated, it will trigger a workflow to start using the data from the updated board. - - -## Workflow - -A workflow is a canvas on which you can place and connect nodes. A workflow can be started manually or by trigger nodes. A workflow run ends when all active and connected nodes have processed their data. diff --git a/docs/keyboard-shortcuts.md b/docs/keyboard-shortcuts.md deleted file mode 100644 index ace6db8d8e..0000000000 --- a/docs/keyboard-shortcuts.md +++ /dev/null @@ -1,28 +0,0 @@ -# Keyboard Shortcuts - -The following keyboard shortcuts can currently be used: - -## General - - - **Ctrl + Left Mouse Button**: Move/Pan Node View - - **Ctrl + a**: Select all nodes - - **Ctrl + Alt + n**: Create new workflow - - **Ctrl + o**: Open workflow - - **Ctrl + s**: Save the current workflow - - **Ctrl + v**: Paste nodes - - **Tab**: Open "Node Creator". Type to filter and navigate with arrow keys. To create press "enter" - - -## With node(s) selected - - - **ArrowDown**: Select sibling node bellow the current one - - **ArrowLeft**: Select node left of the current one - - **ArrowRight**: Select node right of the current one - - **ArrowUp**: Select sibling node above the current one - - **Ctrl + c**: Copy nodes - - **Ctrl + x**: Cut nodes - - **d**: Deactivate nodes - - **Delete**: Delete nodes - - **F2**: Rename node - - **Shift + ArrowLeft**: Select all nodes left of the current one - - **Shift + ArrowRight**: Select all nodes right of the current one diff --git a/docs/license.md b/docs/license.md deleted file mode 100644 index ace732e676..0000000000 --- a/docs/license.md +++ /dev/null @@ -1,5 +0,0 @@ -# License - -n8n is [fair-code](http://faircode.io) licensed under [Apache 2.0 with Commons Clause](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) - -Additional information about the license can be found in the [FAQ](faq.md?id=license) and [fair-code](http://faircode.io). diff --git a/docs/node-basics.md b/docs/node-basics.md deleted file mode 100644 index aea6bf6e33..0000000000 --- a/docs/node-basics.md +++ /dev/null @@ -1,76 +0,0 @@ -# Node Basics - - -## Types - -There are two main node types in n8n: Trigger nodes and Regular nodes. - - -### Trigger Nodes - -The Trigger nodes start a workflow and supply the initial data. A workflow can contain multiple trigger nodes but with each execution, only one of them will execute. This is because the other trigger nodes would not have any input as they are the nodes from which the execution of the workflow starts. - - -### Regular Nodes - -These nodes do the actual work. They can add, remove, and edit the data in the flow as well as request and send data to external APIs. They can do everything possible with Node.js in general. - - -## Credentials - -External services need a way to identify and authenticate users. This data can range from an API key over an email/password combination to a very long multi-line private key and can be saved in n8n as credentials. - -Nodes in n8n can then request that credential information. As an additional layer of security credentials can only be accessed by node types which specifically have the right to do so. - -To make sure that the data is secure, it gets saved to the database encrypted. A random personal encryption key is used which gets automatically generated on the first run of n8n and then saved under `~/.n8n/config`. - - -## Expressions - -With the help of expressions, it is possible to set node parameters dynamically by referencing other data. That can be data from the flow, nodes, the environment or self-generated data. Expressions are normal text with placeholders (everything between {{...}}) that can execute JavaScript code, which offers access to special variables to access data. - -An expression could look like this: - -My name is: `{{$node["Webhook"].json["query"]["name"]}}` - -This one would return "My name is: " and then attach the value that the node with the name "Webhook" outputs and there select the property "query" and its key "name". So if the node would output this data: - -```json -{ - "query": { - "name": "Jim" - } -} -``` - -the value would be: "My name is: Jim" - -The following special variables are available: - - - **$binary**: Incoming binary data of a node - - **$evaluateExpression**: Evaluates a string as expression - - **$env**: Environment variables - - **$items**: Environment variables - - **$json**: Incoming JSON data of a node - - **$node**: Data of other nodes (binary, context, json, parameter, runIndex) - - **$parameters**: Parameters of the current node - - **$runIndex**: The current run index (first time node gets executed it is 0, second time 1, ...) - - **$workflow**: Returns workflow metadata like: active, id, name - -Normally it is not needed to write the JavaScript variables manually as they can be selected with the help of the Expression Editor. - - -## Parameters - -Parameters can be set for most nodes in n8n. The values that get set define what exactly a node does. - -Parameter values are static by default and are always the same no matter what kind of data the node processes. However, it is possible to set the values dynamically with the help of an Expression. Using Expressions, it is possible to make the parameter value dependent on other factors like the data of flow or parameters of other nodes. - -More information about it can be found under [Expressions](#expressions). - - -## Pausing Node - -Sometimes when creating and debugging a workflow, it is helpful to not execute specific nodes. To do that without disconnecting each node, you can pause them. When a node gets paused, the data passes through the node without being changed. - -There are two ways to pause a node. You can either press the pause button which gets displayed above the node when hovering over it or select the node and press ā€œdā€. diff --git a/docs/nodes.md b/docs/nodes.md deleted file mode 100644 index 8b9c7cbf0e..0000000000 --- a/docs/nodes.md +++ /dev/null @@ -1,247 +0,0 @@ -# Nodes - -## Function and Function Item Nodes - -These are the most powerful nodes in n8n. With these, almost everything can be done if you know how to -write JavaScript code. Both nodes work very similarly. They give you access to the incoming data -and you can manipulate it. - - -### Difference between both nodes - -The difference is that the code of the Function node gets executed only once. It receives the -full items (JSON and binary data) as an array and expects an array of items as a return value. The items -returned can be totally different from the incoming ones. So it is not only possible to remove and edit -existing items, but also to add or return totally new ones. - -The code of the Function Item node on the other hand gets executed once for every item. It receives -one item at a time as input and also just the JSON data. As a return value, it expects the JSON data -of one single item. That makes it possible to add, remove and edit JSON properties of items -but it is not possible to add new or remove existing items. Accessing and changing binary data is only -possible via the methods `getBinaryData` and `setBinaryData`. - -Both nodes support promises. So instead of returning the item or items directly, it is also possible to -return a promise which resolves accordingly. - - -### Function-Node - -#### Variable: items - -It contains all the items that the node received as input. - -Information about how the data is structured can be found on the page [Data Structure](data-structure.md). - -The data can be accessed and manipulated like this: - -```typescript -// Sets the JSON data property "myFileName" of the first item to the name of the -// file which is set in the binary property "image" of the same item. -items[0].json.myFileName = items[0].binary.image.fileName; - -return items; -``` - -This example creates 10 dummy items with the ids 0 to 9: - -```typescript -const newItems = []; - -for (let i=0;i<10;i++) { - newItems.push({ - json: { - id: i - } - }); -} - -return newItems; -``` - - -#### Method: $item(index: number, runIndex?: number) - -With `$item` it is possible to access the data of parent nodes. That can be the item data but also -the parameters. It expects as input an index of the item the data should be returned for. This is -needed because for each item the data returned can be different. This is probably obvious for the -item data itself but maybe less for data like parameters. The reason why it is also needed, is -that they may contain an expression. Expressions get always executed of the context for an item. -If that would not be the case, for example, the Email Send node not would be able to send multiple -emails at once to different people. Instead, the same person would receive multiple emails. - -The index is 0 based. So `$item(0)` will return the first item, `$item(1)` the second one, ... - -By default the item of the last run of the node will be returned. So if the referenced node ran -3x (its last runIndex is 2) and the current node runs the first time (its runIndex is 0) the -data of runIndex 2 of the referenced node will be returned. - -For more information about what data can be accessed via $node, check [here](#variable-node). - -Example: - -```typescript -// Returns the value of the JSON data property "myNumber" of Node "Set" (first item) -const myNumber = $item(0).$node["Set"].json["myNumber"]; -// Like above but data of the 6th item -const myNumber = $item(5).$node["Set"].json["myNumber"]; - -// Returns the value of the parameter "channel" of Node "Slack". -// If it contains an expression the value will be resolved with the -// data of the first item. -const channel = $item(0).$node["Slack"].parameter["channel"]; -// Like above but resolved with the value of the 10th item. -const channel = $item(9).$node["Slack"].parameter["channel"]; -``` - - -#### Method: $items(nodeName?: string, outputIndex?: number, runIndex?: number) - -Gives access to all the items of current or parent nodes. If no parameters get supplied, -it returns all the items of the current node. -If a node-name is given, it returns the items the node output on its first output -(index: 0, most nodes only have one output, exceptions are IF and Switch-Node) on -its last run. - -Example: - -```typescript -// Returns all the items of the current node and current run -const allItems = $items(); - -// Returns all items the node "IF" outputs (index: 0 which is Output "true" of its most recent run) -const allItems = $items("IF"); - -// Returns all items the node "IF" outputs (index: 0 which is Output "true" of the same run as current node) -const allItems = $items("IF", 0, $runIndex); - -// Returns all items the node "IF" outputs (index: 1 which is Output "false" of run 0 which is the first run) -const allItems = $items("IF", 1, 0); -``` - - -#### Variable: $node - -Works exactly like `$item` with the difference that it will always return the data of the first item and -the last run of the node. - -```typescript -// Returns the fileName of binary property "data" of Node "HTTP Request" -const fileName = $node["HTTP Request"].binary["data"]["fileName"]}} - -// Returns the context data "noItemsLeft" of Node "SplitInBatches" -const noItemsLeft = $node["SplitInBatches"].context["noItemsLeft"]; - -// Returns the value of the JSON data property "myNumber" of Node "Set" -const myNumber = $node["Set"].json['myNumber']; - -// Returns the value of the parameter "channel" of Node "Slack" -const channel = $node["Slack"].parameter["channel"]; - -// Returns the index of the last run of Node "HTTP Request" -const runIndex = $node["HTTP Request"].runIndex}} -``` - - -#### Variable: $runIndex - -Contains the index of the current run of the node. - -```typescript -// Returns all items the node "IF" outputs (index: 0 which is Output "true" of the same run as current node) -const allItems = $items("IF", 0, $runIndex); -``` - - -#### Variable: $workflow - -Gives information about the current workflow. - -```typescript -const isActive = $workflow.active; -const workflowId = $workflow.id; -const workflowName = $workflow.name; -``` - - -#### Method: $evaluateExpression(expression: string, itemIndex: number) - -Evaluates a given string as expression. -If no `itemIndex` is provided it uses by default in the Function-Node the data of item 0 and -in the Function Item-Node the data of the current item. - -Example: - -```javascript -items[0].json.variable1 = $evaluateExpression('{{1+2}}'); -items[0].json.variable2 = $evaluateExpression($node["Set"].json["myExpression"], 1); - -return items; -``` - - -#### Method: getWorkflowStaticData(type) - -Gives access to the static workflow data. -It is possible to save data directly with the workflow. This data should, however, be very small. -A common use case is to for example to save a timestamp of the last item that got processed from -an RSS-Feed or database. It will always return an object. Properties can then read, delete or -set on that object. When the workflow execution succeeds, n8n will check automatically if the data -has changed and will save it, if necessary. - -There are two types of static data. The "global" and the "node" one. Global static data is the -same in the whole workflow. And every node in the workflow can access it. The node static data -, however, is different for every node and only the node which set it can retrieve it again. - -Example: - -```javascript -// Get the global workflow static data -const staticData = getWorkflowStaticData('global'); -// Get the static data of the node -const staticData = getWorkflowStaticData('node'); - -// Access its data -const lastExecution = staticData.lastExecution; - -// Update its data -staticData.lastExecution = new Date().getTime(); - -// Delete data -delete staticData.lastExecution; -``` - -It is important to know that the static data can not be read and written when testing via the UI. -The data there will always be empty and the changes will not persist. Only when a workflow -is active and it gets called by a Trigger or Webhook, the static data will be saved. - - - -### Function Item-Node - - -#### Variable: item - -It contains the "json" data of the currently processed item. - -The data can be accessed and manipulated like this: - -```json -// Uses the data of an already existing key to create a new additional one -item.newIncrementedCounter = item.existingCounter + 1; -return item; -``` - - -#### Method: getBinaryData() - -Returns all the binary data (all keys) of the item which gets currently processed. - - -#### Method: setBinaryData(binaryData) - -Sets all the binary data (all keys) of the item which gets currently processed. - - -#### Method: getWorkflowStaticData(type) - -As described above for Function node. diff --git a/docs/quick-start.md b/docs/quick-start.md deleted file mode 100644 index 0c33cafbe8..0000000000 --- a/docs/quick-start.md +++ /dev/null @@ -1,43 +0,0 @@ -# Quick Start - - -## Give n8n a spin - -To spin up n8n, you can run: - -```bash -npx n8n -``` - -It will download everything that is needed to start n8n. - -You can then access n8n by opening: -[http://localhost:5678](http://localhost:5678) - - -## Start with docker - -To play around with n8n, you can also start it using docker: - -```bash -docker run -it --rm \ - --name n8n \ - -p 5678:5678 \ - n8nio/n8n -``` - -Be aware that all the data will be lost once the docker container gets removed. To -persist the data mount the `~/.n8n` folder: - -```bash -docker run -it --rm \ - --name n8n \ - -p 5678:5678 \ - -v ~/.n8n:/root/.n8n \ - n8nio/n8n -``` - -More information about the Docker setup can be found in the README file of the -[Docker Image](https://github.com/n8n-io/n8n/blob/master/docker/images/n8n/README.md). - -In case you run into issues, check out the [troubleshooting](troubleshooting.md) page or ask for help in the community [forum](https://community.n8n.io/). diff --git a/docs/security.md b/docs/security.md deleted file mode 100644 index 5682b2c29a..0000000000 --- a/docs/security.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security - -By default, n8n can be accessed by everybody. This is okay if you only have it running -locally but if you deploy it on a server which is accessible from the web, you have -to make sure that n8n is protected. -Right now we have very basic protection in place using basic-auth. It can be activated -by setting the following environment variables: - -```bash -export N8N_BASIC_AUTH_ACTIVE=true -export N8N_BASIC_AUTH_USER= -export N8N_BASIC_AUTH_PASSWORD= -``` diff --git a/docs/sensitive-data.md b/docs/sensitive-data.md deleted file mode 100644 index fa7b0bb1b6..0000000000 --- a/docs/sensitive-data.md +++ /dev/null @@ -1,18 +0,0 @@ -# Sensitive Data via File - -To avoid passing sensitive information via environment variables, "_FILE" may be -appended to some environment variables. It will then load the data from a file -with the given name. That makes it possible to load data easily from -Docker and Kubernetes secrets. - -The following environment variables support file input: - - - DB_MONGODB_CONNECTION_URL_FILE - - DB_POSTGRESDB_DATABASE_FILE - - DB_POSTGRESDB_HOST_FILE - - DB_POSTGRESDB_PASSWORD_FILE - - DB_POSTGRESDB_PORT_FILE - - DB_POSTGRESDB_USER_FILE - - DB_POSTGRESDB_SCHEMA_FILE - - N8N_BASIC_AUTH_PASSWORD_FILE - - N8N_BASIC_AUTH_USER_FILE diff --git a/docs/server-setup.md b/docs/server-setup.md deleted file mode 100644 index d34d076a6f..0000000000 --- a/docs/server-setup.md +++ /dev/null @@ -1,183 +0,0 @@ -# Server Setup - -!> ***Important***: Make sure that you secure your n8n instance as described under [Security](security.md). - - -## Example setup with docker-compose - -If you have already installed docker and docker-compose, then you can directly start with step 4. - - -### 1. Install Docker - -This can vary depending on the Linux distribution used. Example bellow is for Ubuntu: - -```bash -sudo apt update -sudo apt install apt-transport-https ca-certificates curl software-properties-common -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable" -sudo apt update -sudo apt install docker-ce -y -``` - -### 2. Optional: If it should run as not root user - -Run when logged in as the user that should also be allowed to run docker: - -```bash -sudo usermod -aG docker ${USER} -su - ${USER} -``` - -### 3. Install Docker-compose - -This can vary depending on the Linux distribution used. Example bellow is for Ubuntu: - -Check before what version the latestand replace "1.24.1" with that version accordingly. -https://github.com/docker/compose/releases - -```bash -sudo curl -L https://github.com/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose -sudo chmod +x /usr/local/bin/docker-compose -``` - - -### 4. Setup DNS - -Add A record to route the subdomain accordingly. - -``` -Type: A -Name: n8n (or whatever the subdomain should be) -IP address: -``` - - -### 5. Create docker-compose file - -Save this file as `docker-compose.yml` - -Normally no changes should be needed. - -```yaml -version: "3" - -services: - traefik: - image: "traefik" - command: - - "--api=true" - - "--api.insecure=true" - - "--providers.docker=true" - - "--providers.docker.exposedbydefault=false" - - "--entrypoints.websecure.address=:443" - - "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" - - "--certificatesresolvers.mytlschallenge.acme.email=${SSL_EMAIL}" - - "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json" - ports: - - "443:443" - volumes: - - ${DATA_FOLDER}/letsencrypt:/letsencrypt - - /var/run/docker.sock:/var/run/docker.sock:ro - - n8n: - image: n8nio/n8n - ports: - - "127.0.0.1:5678:5678" - labels: - - traefik.enable=true - - traefik.http.routers.n8n.rule=Host(`${SUBDOMAIN}.${DOMAIN_NAME}`) - - traefik.http.routers.n8n.tls=true - - traefik.http.routers.n8n.entrypoints=websecure - - traefik.http.routers.n8n.tls.certresolver=mytlschallenge - - traefik.http.middlewares.n8n.headers.SSLRedirect=true - - traefik.http.middlewares.n8n.headers.STSSeconds=315360000 - - traefik.http.middlewares.n8n.headers.browserXSSFilter=true - - traefik.http.middlewares.n8n.headers.contentTypeNosniff=true - - traefik.http.middlewares.n8n.headers.forceSTSHeader=true - - traefik.http.middlewares.n8n.headers.SSLHost=${DOMAIN_NAME} - - traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true - - traefik.http.middlewares.n8n.headers.STSPreload=true - environment: - - N8N_BASIC_AUTH_ACTIVE=true - - N8N_BASIC_AUTH_USER - - N8N_BASIC_AUTH_PASSWORD - - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME} - - N8N_PORT=5678 - - N8N_LISTEN_ADDRESS=0.0.0.0 - - N8N_PROTOCOL=https - - NODE_ENV=production - - WEBHOOK_TUNNEL_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/ - - VUE_APP_URL_BASE_API=https://${SUBDOMAIN}.${DOMAIN_NAME}/ - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ${DATA_FOLDER}/.n8n:/root/.n8n -``` - - -### 6. Create `.env` file - -Create `.env` file and change it accordingly. - -```bash -# Folder where data should be saved -DATA_FOLDER=/root/n8n/ - -# The top level domain to serve from -DOMAIN_NAME=example.com - -# The subdomain to serve from -SUBDOMAIN=n8n - -# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from -# above example would result in: https://n8n.example.com - -# The user name to use for autentication - IMPORTANT ALWAYS CHANGE! -N8N_BASIC_AUTH_USER=user - -# The password to use for autentication - IMPORTANT ALWAYS CHANGE! -N8N_BASIC_AUTH_PASSWORD=password - -# Optional timezone to set which gets used by Cron-Node by default -# If not set New York time will be used -GENERIC_TIMEZONE=Europe/Berlin - -# The email address to use for the SSL certificate creation -SSL_EMAIL=user@example.com -``` - - -### 7. Create data folder - -Create the folder which is defined as `DATA_FOLDER`. In the example -above, it is `/root/n8n/`. - -In that folder, the database file from SQLite as well as the encryption key will be saved. - -The folder can be created like this: -``` -mkdir /root/n8n/ -``` - - -### 8. Start docker-compose setup - -n8n can now be started via: - -```bash -sudo docker-compose up -d -``` - -In case it should ever be stopped that can be done with this command: -```bash -sudo docker-compose stop -``` - - -### 9. Done - -n8n will now be reachable via the above defined subdomain + domain combination. -The above example would result in: https://n8n.example.com - -n8n will only be reachable via https and not via http. diff --git a/docs/setup.md b/docs/setup.md deleted file mode 100644 index 27e65e54ea..0000000000 --- a/docs/setup.md +++ /dev/null @@ -1,35 +0,0 @@ -# Setup - - -## Installation - -To install n8n globally: - -```bash -npm install n8n -g -``` - -## Start - -After the installation n8n can be started by simply typing in: - -```bash -n8n -# or -n8n start -``` - - -## Start with tunnel - -!> **WARNING**: This is only meant for local development and testing. It should not be used in production! - -To be able to use webhooks for trigger nodes of external services like GitHub, n8n has to be reachable from the web. To make that easy, n8n has a special tunnel service, which redirects requests from our servers to your local n8n instance (uses this code: [https://github.com/localtunnel/localtunnel](https://github.com/localtunnel/localtunnel)). - -To use it, simply start n8n with `--tunnel` - -```bash -n8n start --tunnel -``` - -In case you run into issues, check out the [troubleshooting](troubleshooting.md) page or ask for help in the community [forum](https://community.n8n.io/). diff --git a/docs/start-workflows-via-cli.md b/docs/start-workflows-via-cli.md deleted file mode 100644 index 6327f32963..0000000000 --- a/docs/start-workflows-via-cli.md +++ /dev/null @@ -1,15 +0,0 @@ -# Start Workflows via CLI - -Workflows cannot be only started by triggers, webhooks or manually via the -Editor. It is also possible to start them directly via the CLI. - -Execute a saved workflow by its ID: - -```bash -n8n execute --id -``` - -Execute a workflow from a workflow file: -```bash -n8n execute --file -``` diff --git a/docs/test.md b/docs/test.md deleted file mode 100644 index 02a308b3ad..0000000000 --- a/docs/test.md +++ /dev/null @@ -1,3 +0,0 @@ -# This is a simple test - -with some text diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index 7e6b4b058b..0000000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,58 +0,0 @@ -# Troubleshooting - -## Windows - -If you are experiencing issues running n8n with the typical flow of: - -```powershell -npx n8n -``` - -### Requirements - -Please ensure that you have the following requirements fulfilled: - -- Install latest version of [NodeJS](https://nodejs.org/en/download/) -- Install [Python 2.7](https://www.python.org/downloads/release/python-2717/) (It is okay to have multiple versions installed on the machine) -- Windows SDK -- C++ Desktop Development Tools -- Windows Build Tools - -#### Install build tools - -If you haven't satisfied the above, follow this procedure through your PowerShell (run with administrative privileges). -This command installs the build tools, windows SDK and the C++ development tools in one package. - -```powershell -npm install --global --production windows-build-tools -``` - -#### Configure npm to use Python version 2.7 - -```powershell -npm config set python python2.7 -``` - -#### Configure npm to use correct msvs version - -```powershell -npm config set msvs_version 2017 --global -``` - -### Lesser known issues: - -#### mmmagic npm package when using MSbuild tools with Visual Studio - -While installing this package, `node-gyp` is run and it might fail to install it with an error appearing in the ballpark of: - -``` -gyp ERR! stack Error: spawn C:\Program Files (x86)\Microsoft Visual Studio\2019\**Enterprise**\MSBuild\Current\Bin\MSBuild.exe ENOENT -``` - -It is seeking the `MSBuild.exe` in a directory that does not exist. If you are using Visual Studio Community or vice versa, you can change the path of MSBuild with command: - -```powershell -npm config set msbuild_path "C:\Program Files (x86)\Microsoft Visual Studio\2019\**Community**\MSBuild\Current\Bin\MSBuild.exe" -``` - -Attempt to install package again after running the command above. diff --git a/docs/tutorials.md b/docs/tutorials.md deleted file mode 100644 index 527274be53..0000000000 --- a/docs/tutorials.md +++ /dev/null @@ -1,26 +0,0 @@ -# Tutorials - - -## Examples - -Example workflows which show what can be done with n8n can be found here: -[https://n8n.io/workflows](https://n8n.io/workflows) - -If you want to know how a node can get used in context, you can search for it here: -[https://n8n.io/nodes](https://n8n.io/nodes). There it shows in which workflows the -node got used. - - - -## Videos - - - [Slack Notification on Github Star](https://www.youtube.com/watch?v=3w7xIMKLVAg) - - [Typeform to Google Sheet and Slack or Email](https://www.youtube.com/watch?v=rn3-d4IiW44) - - -### Community Tutorials - - - [n8n basics 1/3 - Getting Started](https://www.youtube.com/watch?v=JIaxjH2CyFc) - - [n8n basics 2/3 - Simple Workflow](https://www.youtube.com/watch?v=ovlxledZfM4) - - [n8n basics 3/3 - Transforming JSON](https://www.youtube.com/watch?v=wGAEAcfwV8w) - - [n8n Google Integration - Using Google Sheets and Google Api nodes](https://www.youtube.com/watch?v=KFqx8OmkqVE) diff --git a/docs/workflow.md b/docs/workflow.md deleted file mode 100644 index 344504d158..0000000000 --- a/docs/workflow.md +++ /dev/null @@ -1,111 +0,0 @@ -# Workflow - - -## Activate - -Activating a workflow means that the Trigger and Webhook nodes get activated and can trigger a workflow to run. By default all the newly created workflows are deactivated. That means that even if a Trigger node like the Cron node should start a workflow because a predefined time is reached, it will not unless the workflow gets activated. It is only possible to activate a workflow which contains a Trigger or a Webhook node. - - -## Data Flow - -Nodes do not only process one "item", they process multiple ones. So if the Trello node is set to "Create-Card" and it has an expression set for "Name" to be set depending on "name" property, it will create a card for each item, always choosing the name-property-value of the current one. - -This data would, for example, create two boards. One named "test1" the other one named "test2": - -```json -[ - { - name: "test1" - }, - { - name: "test2" - } -] -``` - - -## Error Workflows - -For each workflow, an optional "Error Workflow" can be set. It gets executed in case the execution of the workflow fails. That makes it possible to, for instance, inform the user via Email or Slack if something goes wrong. The same "Error Workflow" can be set on multiple workflows. - -The only difference between a regular workflow and an "Error Workflow" is that it contains an "Error Trigger" node. So it is important to make sure that this node gets created before setting a workflow as "Error Workflow". - -The "Error Trigger" node will trigger in case the execution fails and receives information about it. The data looks like this: - -```json -[ - { - "execution": { - "id": "231", - "url": "https://n8n.example.com/execution/231", - "retryOf": "34", - "error": { - "message": "Example Error Message", - "stack": "Stacktrace" - }, - "lastNodeExecuted": "Node With Error", - "mode": "manual" - }, - "workflow": { - "id": "1", - "name": "Example Workflow" - } - } -] - -``` - -All information is always present except: -- **execution.id**: Only present when the execution gets saved in the database -- **execution.url**: Only present when the execution gets saved in the database -- **execution.retryOf**: Only present when the execution is a retry of a previously failed execution - - -### Setting Error Workflow - -An "Error Workflow" can be set in the Workflow Settings which can be accessed by pressing the "Workflow" button in the menu on the on the left side. The last option is "Settings". In the window that appears, the "Error Workflow" can be selected via the Dropdown "Error Workflow". - - -## Share Workflows - -All workflows are JSON and can be shared very easily. - -There are multiple ways to download a workflow as JSON to then share it with other people via Email, Slack, Skype, Dropbox, ā€¦ - - 1. Press the "Download" button under the Workflow menu in the sidebar on the left. It then downloads the workflow as a JSON file. - 1. Select the nodes in the editor which should be exported and then copy them (Ctrl + c). The nodes then get saved as JSON in the clipboard and can be pasted wherever desired (Ctrl + v). - -Importing that JSON representation again into n8n is as easy and can also be done in different ways: - - 1. Press "Import from File" or "Import from URL" under the Workflow menu in the sidebar on the left. - 1. Copy the JSON workflow to the clipboard (Ctrl + c) and then simply pasting it directly into the editor (Ctrl + v). - - -## Workflow Settings - -On each workflow, it is possible to set some custom settings and overwrite some of the global default settings. Currently, the following settings can be set: - - -### Error Workflow - -Workflow to run in case the execution of the current workflow fails. More information in section [Error Workflows](#error-workflows). - - -### Timezone - -The timezone to use in the current workflow. If not set, the global Timezone (by default "New York" gets used). For instance, this is important for the Cron Trigger node. - - -### Save Data Error Execution - -If the Execution data of the workflow should be saved when it fails. - - -### Save Data Success Execution - -If the Execution data of the workflow should be saved when it succeeds. - - -### Save Manual Executions - -If executions started from the Editor UI should be saved. From 559afb488bb8960adb60755d9f4aea5e1026d0a5 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 5 Jul 2020 19:18:01 +0200 Subject: [PATCH 38/44] :whale: Add Raspberry Pi Docker image --- .github/workflows/docker-images.yml | 5 +++++ docker/images/n8n-rpi/Dockerfile | 20 ++++++++++++++++++++ docker/images/n8n-rpi/README.md | 21 +++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 docker/images/n8n-rpi/Dockerfile create mode 100644 docker/images/n8n-rpi/README.md diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 8d034b5b91..b05b6aa714 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -32,3 +32,8 @@ jobs: run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-ubuntu docker/images/n8n-ubuntu - name: Push Docker image of version (Ubuntu) run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-ubuntu + + - name: Build the Docker image of version (Rpi) + run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-rpi docker/images/n8n-rpi + - name: Push Docker image of version (Rpi) + run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-rpi diff --git a/docker/images/n8n-rpi/Dockerfile b/docker/images/n8n-rpi/Dockerfile new file mode 100644 index 0000000000..b60d50bdeb --- /dev/null +++ b/docker/images/n8n-rpi/Dockerfile @@ -0,0 +1,20 @@ +FROM arm32v7/node:12.16 + +ARG N8N_VERSION + +RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi + +RUN \ + apt-get update && \ + apt-get -y install graphicsmagick gosu + +RUN npm_config_user=root npm install -g full-icu n8n@${N8N_VERSION} + +ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu +ENV NODE_ENV production + +WORKDIR /data + +USER node + +CMD n8n diff --git a/docker/images/n8n-rpi/README.md b/docker/images/n8n-rpi/README.md new file mode 100644 index 0000000000..9eca14e3f6 --- /dev/null +++ b/docker/images/n8n-rpi/README.md @@ -0,0 +1,21 @@ +## n8n - Raspberry PI Docker Image + +Dockerfile to build n8n for Raspberry PI. + +For information about how to run n8n with Docker check the generic +[Docker-Readme](https://github.com/n8n-io/n8n/tree/master/docker/images/n8n/README.md) + + +``` +docker build --build-arg N8N_VERSION= -t n8nio/n8n: . + +# For example: +docker build --build-arg N8N_VERSION=0.43.0 -t n8nio/n8n:0.43.0-rpi . +``` + +``` +docker run -it --rm \ + --name n8n \ + -p 5678:5678 \ + n8nio/n8n:0.70.0-rpi +``` From b4b65bb90624ad00e6e3336c276a363548fb60cc Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 5 Jul 2020 19:22:25 +0200 Subject: [PATCH 39/44] :whale: Build Raspberry Pi Docker image correctly --- .github/workflows/docker-images-rpi.yml | 28 ++++++++++++++ .github/workflows/docker-images.yml | 5 --- docker/images/n8n-custom/Dockerfile copy | 49 ++++++++++++++++++++++++ docker/images/n8n-rhel7/Dockerfile | 23 +++++++++++ docker/images/n8n-rhel7/README.md | 16 ++++++++ 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/docker-images-rpi.yml create mode 100644 docker/images/n8n-custom/Dockerfile copy create mode 100644 docker/images/n8n-rhel7/Dockerfile create mode 100644 docker/images/n8n-rhel7/README.md diff --git a/.github/workflows/docker-images-rpi.yml b/.github/workflows/docker-images-rpi.yml new file mode 100644 index 0000000000..c6db9ed95b --- /dev/null +++ b/.github/workflows/docker-images-rpi.yml @@ -0,0 +1,28 @@ +name: Docker Image CI - Rpi + +on: + push: + tags: + - n8n@* + +jobs: + armv7_job: + runs-on: ubuntu-18.04 + name: Build on ARMv7 (Rpi) + steps: + - uses: actions/checkout@v1 + - name: Get the version + id: vars + run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14}) + + - name: Log in to Docker registry + run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + uses: crazy-max/ghaction-docker-buildx@v1 + with: + version: latest + - name: Run Buildx (push image) + if: success() + run: | + docker buildx build --platform linux/arm/v7 --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-rpi --output type=image,push=true docker/images/n8n-rpi diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index b05b6aa714..8d034b5b91 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -32,8 +32,3 @@ jobs: run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-ubuntu docker/images/n8n-ubuntu - name: Push Docker image of version (Ubuntu) run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-ubuntu - - - name: Build the Docker image of version (Rpi) - run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-rpi docker/images/n8n-rpi - - name: Push Docker image of version (Rpi) - run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-rpi diff --git a/docker/images/n8n-custom/Dockerfile copy b/docker/images/n8n-custom/Dockerfile copy new file mode 100644 index 0000000000..19f08a16dd --- /dev/null +++ b/docker/images/n8n-custom/Dockerfile copy @@ -0,0 +1,49 @@ +FROM node:12.16-alpine as builder +# FROM node:12.16-alpine + +# Update everything and install needed dependencies +RUN apk add --update graphicsmagick tzdata git tini su-exec + +USER root + +# Install all needed dependencies +RUN apk --update add --virtual build-dependencies python build-base ca-certificates && \ + npm_config_user=root npm install -g full-icu lerna + +ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu + +WORKDIR /data + +COPY lerna.json . +COPY package.json . +COPY packages/cli/ ./packages/cli/ +COPY packages/core/ ./packages/core/ +COPY packages/editor-ui/ ./packages/editor-ui/ +COPY packages/nodes-base/ ./packages/nodes-base/ +COPY packages/workflow/ ./packages/workflow/ +RUN rm -rf node_modules packages/*/node_modules packages/*/dist + +RUN npm install --loglevel notice +RUN lerna bootstrap --hoist +RUN npm run build + + +FROM node:12.16-alpine + +WORKDIR /data + +# Install all needed dependencies +RUN npm_config_user=root npm install -g full-icu + +USER root + +ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu + +COPY --from=builder /data ./ + +RUN apk add --update graphicsmagick tzdata git tini su-exec + +COPY docker/images/n8n-dev/docker-entrypoint.sh /docker-entrypoint.sh +ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] + +EXPOSE 5678/tcp diff --git a/docker/images/n8n-rhel7/Dockerfile b/docker/images/n8n-rhel7/Dockerfile new file mode 100644 index 0000000000..949d436602 --- /dev/null +++ b/docker/images/n8n-rhel7/Dockerfile @@ -0,0 +1,23 @@ +FROM richxsl/rhel7 + +ARG N8N_VERSION + +RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi + +RUN \ + yum install -y gcc-c++ make + +RUN \ + curl -sL https://rpm.nodesource.com/setup_12.x | sudo -E bash - + +RUN \ + sudo yum install nodejs + +# Set a custom user to not have n8n run as root +USER root + +RUN npm_config_user=root npm install -g n8n@${N8N_VERSION} + +WORKDIR /data + +CMD "n8n" diff --git a/docker/images/n8n-rhel7/README.md b/docker/images/n8n-rhel7/README.md new file mode 100644 index 0000000000..015f7ac07f --- /dev/null +++ b/docker/images/n8n-rhel7/README.md @@ -0,0 +1,16 @@ +## Build Docker-Image + +``` +docker build --build-arg N8N_VERSION= -t n8nio/n8n: . + +# For example: +docker build --build-arg N8N_VERSION=0.36.1 -t n8nio/n8n:0.36.1-rhel7 . +``` + + +``` +docker run -it --rm \ + --name n8n \ + -p 5678:5678 \ + n8nio/n8n:0.25.0-ubuntu +``` From af4333f268bf3de5422bcdac4ec3e41dc25ac983 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 6 Jul 2020 15:05:05 +0200 Subject: [PATCH 40/44] :zap: Some minior improvements on Zoom-Node --- .../credentials/ZoomApi.credentials.ts | 2 +- .../nodes/Zoom/MeetingDescription.ts | 155 ++++++++++-------- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 11 +- 3 files changed, 86 insertions(+), 82 deletions(-) diff --git a/packages/nodes-base/credentials/ZoomApi.credentials.ts b/packages/nodes-base/credentials/ZoomApi.credentials.ts index 3db4aadbe0..dbef996429 100644 --- a/packages/nodes-base/credentials/ZoomApi.credentials.ts +++ b/packages/nodes-base/credentials/ZoomApi.credentials.ts @@ -5,7 +5,7 @@ export class ZoomApi implements ICredentialType { displayName = 'Zoom API'; properties = [ { - displayName: 'Access Token', + displayName: 'JTW Token', name: 'accessToken', type: 'string' as NodePropertyTypes, default: '' diff --git a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts index 130830b9fe..f412235396 100644 --- a/packages/nodes-base/nodes/Zoom/MeetingDescription.ts +++ b/packages/nodes-base/nodes/Zoom/MeetingDescription.ts @@ -50,6 +50,27 @@ export const meetingFields = [ /* -------------------------------------------------------------------------- */ /* meeting:create */ /* -------------------------------------------------------------------------- */ + { + displayName: 'Topic', + name: 'topic', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + + ], + resource: [ + 'meeting', + ], + }, + }, + description: `Topic of the meeting.`, + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -88,42 +109,6 @@ export const meetingFields = [ default: 0, description: 'Meeting duration (minutes).', }, - { - displayName: 'Meeting Topic', - name: 'topic', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Meeting topic.`, - }, - { - displayName: 'Meeting Type', - name: 'type', - type: 'options', - options: [ - { - name: 'Instant Meeting', - value: 1, - }, - { - name: 'Scheduled Meeting', - value: 2, - }, - { - name: 'Recurring Meeting with no fixed time', - value: 3, - }, - { - name: 'Recurring Meeting with fixed time', - value: 8, - }, - - ], - default: 2, - description: 'Meeting type.', - }, { displayName: 'Password', name: 'password', @@ -284,13 +269,39 @@ export const meetingFields = [ default: '', description: `Time zone used in the response. The default is the time zone of the calendar.`, }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Instant Meeting', + value: 1, + }, + { + name: 'Scheduled Meeting', + value: 2, + }, + { + name: 'Recurring Meeting with no fixed time', + value: 3, + }, + { + name: 'Recurring Meeting with fixed time', + value: 8, + }, + + ], + default: 2, + description: 'Meeting type.', + }, ], }, /* -------------------------------------------------------------------------- */ /* meeting:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting ID', + displayName: 'ID', name: 'meetingId', type: 'string', default: '', @@ -433,7 +444,7 @@ export const meetingFields = [ /* meeting:delete */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting ID', + displayName: 'ID', name: 'meetingId', type: 'string', default: '', @@ -488,7 +499,7 @@ export const meetingFields = [ /* meeting:update */ /* -------------------------------------------------------------------------- */ { - displayName: 'Meeting ID', + displayName: 'ID', name: 'meetingId', type: 'string', default: '', @@ -542,39 +553,6 @@ export const meetingFields = [ default: 0, description: 'Meeting duration (minutes).', }, - { - displayName: 'Meeting Topic', - name: 'topic', - type: 'string', - default: '', - description: `Meeting topic.`, - }, - { - displayName: 'Meeting Type', - name: 'type', - type: 'options', - options: [ - { - name: 'Instant Meeting', - value: 1, - }, - { - name: 'Scheduled Meeting', - value: 2, - }, - { - name: 'Recurring Meeting with no fixed time', - value: 3, - }, - { - name: 'Recurring Meeting with fixed time', - value: 8, - }, - - ], - default: 2, - description: 'Meeting type.', - }, { displayName: 'Password', name: 'password', @@ -735,6 +713,39 @@ export const meetingFields = [ default: '', description: `Time zone used in the response. The default is the time zone of the calendar.`, }, + { + displayName: 'Topic', + name: 'topic', + type: 'string', + default: '', + description: `Meeting topic.`, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Instant Meeting', + value: 1, + }, + { + name: 'Scheduled Meeting', + value: 2, + }, + { + name: 'Recurring Meeting with no fixed time', + value: 3, + }, + { + name: 'Recurring Meeting with fixed time', + value: 8, + }, + + ], + default: 2, + description: 'Meeting type.', + }, ], }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoom/Zoom.node.ts b/packages/nodes-base/nodes/Zoom/Zoom.node.ts index 7f735d33b8..f7ecff0de8 100644 --- a/packages/nodes-base/nodes/Zoom/Zoom.node.ts +++ b/packages/nodes-base/nodes/Zoom/Zoom.node.ts @@ -252,10 +252,7 @@ export class Zoom implements INodeType { } if (operation === 'create') { //https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate - const additionalFields = this.getNodeParameter( - 'additionalFields', - i - ) as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const body: IDataObject = {}; @@ -310,9 +307,7 @@ export class Zoom implements INodeType { body.settings = settingValues; } - if (additionalFields.topic) { - body.topic = additionalFields.topic as string; - } + body.topic = this.getNodeParameter('topic', i) as string; if (additionalFields.type) { body.type = additionalFields.type as string; @@ -347,8 +342,6 @@ export class Zoom implements INodeType { body.agenda = additionalFields.agenda as string; } - console.log(body); - responseData = await zoomApiRequest.call( this, 'POST', From f4022c6cd543886ba150ae3da554d973cb6aaa70 Mon Sep 17 00:00:00 2001 From: Erin Date: Tue, 7 Jul 2020 10:35:20 -0400 Subject: [PATCH 41/44] :wrench: Prompt User to Save Before Page Unload --- packages/editor-ui/src/views/NodeView.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index de89534e09..573849eb39 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1328,6 +1328,10 @@ export default mixins( document.addEventListener('keydown', this.keyDown); document.addEventListener('keyup', this.keyUp); + window.onbeforeunload = this.confirmSave; + }, + async confirmSave(e: Event) { + window.confirm(); }, __addConnection (connection: [IConnection, IConnection], addVisualConnection = false) { if (addVisualConnection === true) { From ad1228e0ea310b10c81c9bbea3562396f2ece8e9 Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 9 Jul 2020 16:54:50 -0400 Subject: [PATCH 42/44] :sparkles: Everything works except refresh --- .../editor-ui/src/components/MainSidebar.vue | 27 +++++++++++++---- .../editor-ui/src/components/WorkflowOpen.vue | 17 +++++++++-- .../src/components/mixins/workflowHelpers.ts | 29 ++++++++++++++++++ packages/editor-ui/src/views/NodeView.vue | 30 ++++++++++++++----- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 71388c2cd3..57096c0f29 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -443,13 +443,28 @@ export default mixins( } else if (key === 'workflow-settings') { this.workflowSettingsDialogVisible = true; } else if (key === 'workflow-new') { - this.$router.push({ name: 'NodeViewNew' }); + const workflowId = this.$store.getters.workflowId; + const result = await this.dataHasChanged(workflowId); + if(result) { + const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); + if (importConfirm === true) { + this.$router.push({ name: 'NodeViewNew' }); - this.$showMessage({ - title: 'Workflow created', - message: 'A new workflow got created!', - type: 'success', - }); + this.$showMessage({ + title: 'Workflow created', + message: 'A new workflow got created!', + type: 'success', + }); + } + } else { + this.$router.push({ name: 'NodeViewNew' }); + + this.$showMessage({ + title: 'Workflow created', + message: 'A new workflow got created!', + type: 'success', + }); + } } else if (key === 'credentials-open') { this.credentialOpenDialogVisible = true; } else if (key === 'credentials-new') { diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 9592ecb4d5..9ef5e94eb0 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -33,6 +33,7 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue'; import { restApi } from '@/components/mixins/restApi'; import { genericHelpers } from '@/components/mixins/genericHelpers'; +import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { showMessage } from '@/components/mixins/showMessage'; import { IWorkflowShortResponse } from '@/Interface'; @@ -42,6 +43,7 @@ export default mixins( genericHelpers, restApi, showMessage, + workflowHelpers, ).extend({ name: 'WorkflowOpen', props: [ @@ -87,9 +89,20 @@ export default mixins( this.$emit('closeDialog'); return false; }, - openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any + async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { - this.$emit('openWorkflow', data.id); + const workflowId = this.$store.getters.workflowId; + const result = await this.dataHasChanged(workflowId); + if(result) { + const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); + if (importConfirm === false) { + return; + } else { + this.$emit('openWorkflow', data.id); + } + } else { + this.$emit('openWorkflow', data.id); + } } }, openDialog () { diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 507d6d31e4..76ad77cdb9 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -22,6 +22,7 @@ import { INodeTypesMaxCount, INodeUi, IWorkflowData, + IWorkflowDb, IWorkflowDataUpdate, XYPositon, } from '../../Interface'; @@ -30,6 +31,8 @@ import { restApi } from '@/components/mixins/restApi'; import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { showMessage } from '@/components/mixins/showMessage'; +import { isEqual } from 'lodash'; + import mixins from 'vue-typed-mixins'; export const workflowHelpers = mixins( @@ -478,5 +481,31 @@ export const workflowHelpers = mixins( node.position[1] += offsetPosition[1]; } }, + async dataHasChanged(id: string) { + const currentData = await this.getWorkflowDataToSave(); + + let data: IWorkflowDb; + data = await this.restApi().getWorkflow(id); + + if(data !== undefined) { + console.log(currentData); + console.log(data); + const x = { + nodes: data.nodes, + connections: data.connections, + settings: data.settings, + name: data.name + }; + const y = { + nodes: currentData.nodes, + connections: currentData.connections, + settings: currentData.settings, + name: currentData.name + }; + return !isEqual(x, y); + } + + return true; + }, }, }); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 573849eb39..8ce1378d52 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -126,7 +126,7 @@ import RunData from '@/components/RunData.vue'; import mixins from 'vue-typed-mixins'; -import { debounce } from 'lodash'; +import { debounce, isEqual } from 'lodash'; import axios from 'axios'; import { IConnection, @@ -330,6 +330,8 @@ export default mixins( this.$store.commit('setWorkflowSettings', data.settings || {}); await this.addNodes(data.nodes, data.connections); + + return data; }, mouseDown (e: MouseEvent) { // Save the location of the mouse click @@ -1309,6 +1311,7 @@ export default mixins( if (this.$route.name === 'ExecutionById') { // Load an execution const executionId = this.$route.params.id; + await this.openExecution(executionId); } else { // Load a workflow @@ -1316,7 +1319,6 @@ export default mixins( if (this.$route.params.name) { workflowId = this.$route.params.name; } - if (workflowId !== null) { // Open existing workflow await this.openWorkflow(workflowId); @@ -1328,10 +1330,22 @@ export default mixins( document.addEventListener('keydown', this.keyDown); document.addEventListener('keyup', this.keyUp); - window.onbeforeunload = this.confirmSave; - }, - async confirmSave(e: Event) { - window.confirm(); + + window.addEventListener("beforeunload", (e) => { + let workflowId = null as string | null; + if (this.$route.params.name) { + workflowId = this.$route.params.name; + } + if(workflowId !== null) { + //const dataHasChanged = await this.dataHasChanged(workflowId); + } + + const confirmationMessage = 'It looks like you have been editing something. ' + + 'If you leave before saving, your changes will be lost.'; + + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. + }); }, __addConnection (connection: [IConnection, IConnection], addVisualConnection = false) { if (addVisualConnection === true) { @@ -1876,13 +1890,13 @@ export default mixins( async mounted () { this.$root.$on('importWorkflowData', async (data: IDataObject) => { - await this.importWorkflowData(data.data as IWorkflowDataUpdate); + const resData = await this.importWorkflowData(data.data as IWorkflowDataUpdate); }); this.$root.$on('importWorkflowUrl', async (data: IDataObject) => { const workflowData = await this.getWorkflowDataFromUrl(data.url as string); if (workflowData !== undefined) { - await this.importWorkflowData(workflowData); + const resData = await this.importWorkflowData(workflowData); } }); From 70a584a46d419f83b05034a7cd3eacfd8bea248c Mon Sep 17 00:00:00 2001 From: Erin Date: Mon, 20 Jul 2020 10:57:58 -0400 Subject: [PATCH 43/44] :tada: Works with ctrl s, now working on a user saving from the side bar --- packages/editor-ui/src/views/NodeView.vue | 55 +++++++++++++++++------ 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 8ce1378d52..5f2a08e3fc 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -186,6 +186,38 @@ export default mixins( // When a node gets set as active deactivate the create-menu this.createNodeActive = false; }, + nodes: { + handler: async function (val, oldVal) { + // Load a workflow + let workflowId = null as string | null; + if (this.$route && this.$route.params.name) { + workflowId = this.$route.params.name; + } + if(workflowId !== null) { + this.isDirty = await this.dataHasChanged(workflowId); + } else { + this.isDirty = true; + } + console.log(this.isDirty); + }, + deep: true + }, + connections: { + handler: async function (val, oldVal) { + // Load a workflow + let workflowId = null as string | null; + if (this.$route && this.$route.params.name) { + workflowId = this.$route.params.name; + } + if(workflowId !== null) { + this.isDirty = await this.dataHasChanged(workflowId); + } else { + this.isDirty = true; + } + console.log(this.isDirty); + }, + deep: true + }, }, computed: { activeNode (): INodeUi | null { @@ -259,6 +291,7 @@ export default mixins( ctrlKeyPressed: false, debouncedFunctions: [] as any[], // tslint:disable-line:no-any stopExecutionInProgress: false, + isDirty: false, }; }, beforeDestroy () { @@ -433,6 +466,8 @@ export default mixins( e.stopPropagation(); e.preventDefault(); + this.isDirty = false; + this.callDebounced('saveCurrentWorkflow', 1000); } else if (e.key === 'Enter') { // Activate the last selected node @@ -1305,6 +1340,7 @@ export default mixins( if (this.$route.params.action === 'workflowSave') { // In case the workflow got saved we do not have to run init // as only the route changed but all the needed data is already loaded + this.isDirty = false; return Promise.resolve(); } @@ -1331,20 +1367,13 @@ export default mixins( document.addEventListener('keydown', this.keyDown); document.addEventListener('keyup', this.keyUp); - window.addEventListener("beforeunload", (e) => { - let workflowId = null as string | null; - if (this.$route.params.name) { - workflowId = this.$route.params.name; + window.addEventListener("beforeunload", (e) => { + if(this.isDirty === true) { + const confirmationMessage = 'It looks like you have been editing something. ' + + 'If you leave before saving, your changes will be lost.'; + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. } - if(workflowId !== null) { - //const dataHasChanged = await this.dataHasChanged(workflowId); - } - - const confirmationMessage = 'It looks like you have been editing something. ' - + 'If you leave before saving, your changes will be lost.'; - - (e || window.event).returnValue = confirmationMessage; //Gecko + IE - return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. }); }, __addConnection (connection: [IConnection, IConnection], addVisualConnection = false) { From 5f32341a9ea14a50b405ed0606949ae660706bd1 Mon Sep 17 00:00:00 2001 From: Erin Date: Mon, 20 Jul 2020 11:52:24 -0400 Subject: [PATCH 44/44] Remove logs --- packages/editor-ui/src/components/MainSidebar.vue | 2 ++ .../editor-ui/src/components/mixins/workflowHelpers.ts | 2 -- packages/editor-ui/src/views/NodeView.vue | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 57096c0f29..5cb68b4ee3 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -435,8 +435,10 @@ export default mixins( saveAs(blob, workflowName + '.json'); } else if (key === 'workflow-save') { + console.log("saving......"); this.saveCurrentWorkflow(); } else if (key === 'workflow-save-as') { + console.log("saving......"); this.saveCurrentWorkflow(true); } else if (key === 'help-about') { this.aboutDialogVisible = true; diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 76ad77cdb9..b3c2416b28 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -488,8 +488,6 @@ export const workflowHelpers = mixins( data = await this.restApi().getWorkflow(id); if(data !== undefined) { - console.log(currentData); - console.log(data); const x = { nodes: data.nodes, connections: data.connections, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 5f2a08e3fc..c75899424d 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -187,7 +187,7 @@ export default mixins( this.createNodeActive = false; }, nodes: { - handler: async function (val, oldVal) { + async handler (val, oldVal) { // Load a workflow let workflowId = null as string | null; if (this.$route && this.$route.params.name) { @@ -198,12 +198,11 @@ export default mixins( } else { this.isDirty = true; } - console.log(this.isDirty); }, deep: true }, connections: { - handler: async function (val, oldVal) { + async handler (val, oldVal) { // Load a workflow let workflowId = null as string | null; if (this.$route && this.$route.params.name) { @@ -214,7 +213,6 @@ export default mixins( } else { this.isDirty = true; } - console.log(this.isDirty); }, deep: true }, @@ -1373,6 +1371,8 @@ export default mixins( + 'If you leave before saving, your changes will be lost.'; (e || window.event).returnValue = confirmationMessage; //Gecko + IE return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. + } else { + return; } }); },