From eb1640c67ecfc7e4ae12af952ed4d7d634371213 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 13 Mar 2020 21:22:36 -0400 Subject: [PATCH 1/2] :sparkles: Google Calendar Integration done :sparkles: Google Calendar Integration --- packages/cli/src/Server.ts | 4 +- .../GoogleOAuth2Api.credentials.ts | 49 + .../nodes/Google/EventDescription.ts | 1106 +++++++++++++++++ .../nodes-base/nodes/Google/EventInterface.ts | 26 + .../nodes/Google/GenericFunctions.ts | 57 + .../nodes/Google/GoogleCalendar.node.ts | 412 ++++++ .../nodes/Google/googleCalendar.png | Bin 0 -> 6937 bytes packages/nodes-base/package.json | 12 +- 8 files changed, 1661 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/EventDescription.ts create mode 100644 packages/nodes-base/nodes/Google/EventInterface.ts create mode 100644 packages/nodes-base/nodes/Google/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Google/GoogleCalendar.node.ts create mode 100644 packages/nodes-base/nodes/Google/googleCalendar.png diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4fe1ef70d7..5a1c0703bf 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,7 +203,9 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error, decoded: string) => { + jwt.verify(token, getKey, {}, (err: Error, + //decoded: string + ) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts new file mode 100644 index 0000000000..facd5e8fac --- /dev/null +++ b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts @@ -0,0 +1,49 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/calendar.events', +]; + +export class GoogleOAuth2Api implements ICredentialType { + name = 'googleOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Google OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://accounts.google.com/o/oauth2/v2/auth', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://oauth2.googleapis.com/token', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'access_type=offline', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/EventDescription.ts b/packages/nodes-base/nodes/Google/EventDescription.ts new file mode 100644 index 0000000000..fb1ea6a90f --- /dev/null +++ b/packages/nodes-base/nodes/Google/EventDescription.ts @@ -0,0 +1,1106 @@ +import { INodeProperties } from "n8n-workflow"; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add a event to calendar', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an event', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an event', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all events from a calendar', + }, + { + name: 'Update', + value: 'update', + description: 'Update an event', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + +/* -------------------------------------------------------------------------- */ +/* event:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + description: 'Start time of the event.', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + description: 'End time of the event.', + }, + { + displayName: 'Use Default Reminders', + name: 'useDefaultReminders', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'All Day', + name: 'allday', + type: 'boolean', + options: [ + { + name: 'Yes', + value: 'yes', + }, + { + name: 'No', + value: 'no', + }, + ], + default: 'no', + description: 'Wheater the event is all day or not', + }, + { + displayName: 'Attendees', + name: 'attendees', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Attendee', + }, + default: '', + description: 'The attendees of the event', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColors', + }, + default: '', + description: 'The color of the event.', + }, + { + displayName: 'Guests Can Invite Others', + name: 'guestsCanInviteOthers', + type: 'boolean', + default: true, + description: 'Whether attendees other than the organizer can invite others to the event', + }, + { + displayName: 'Guests Can Modify', + name: 'guestsCanModify', + type: 'boolean', + default: false, + description: 'Whether attendees other than the organizer can modify the event', + }, + { + displayName: 'Guests Can See Other Guests', + name: 'guestsCanSeeOtherGuests', + type: 'boolean', + default: true, + description: `Whether attendees other than the organizer can see who the event's attendees are.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: 'Opaque identifier of the event', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Geographic location of the event as free-form text.', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + displayName: 'Repeat Frecuency', + name: 'repeatFrecuency', + type: 'options', + options: [ + { + name: 'Daily', + value: 'Daily', + }, + { + name: 'Weekly', + value: 'weekly', + }, + { + name: 'Monthly', + value: 'monthly', + }, + { + name: 'Yearly', + value: 'yearly', + }, + ], + default: '', + }, + { + displayName: 'Repeat Until', + name: 'repeatUntil', + type: 'dateTime', + default: '', + }, + { + displayName: 'Repeat How Many Times?', + name: 'repeatHowManyTimes', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Title of the event.', + }, + { + displayName: 'Show Me As', + name: 'showMeAs', + type: 'options', + options: [ + { + name: 'Available', + value: 'transparent', + description: 'The event does not block time on the calendar', + }, + { + name: 'Busy', + value: 'opaque', + description: ' The event does block time on the calendar.', + }, + ], + default: 'opaque', + description: 'Whether the event blocks time on the calendar', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + }, + { + displayName: 'Visibility', + name: 'visibility', + type: 'options', + options: [ + { + name: 'Confidential', + value: 'confidential', + description: 'The event is private. This value is provided for compatibility reasons.', + }, + { + name: 'Default', + value: 'default', + description: ' Uses the default visibility for events on the calendar.', + }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, + { + name: 'Private', + value: 'private', + description: 'The event is private and only event attendees may view event details.', + }, + ], + default: 'default', + description: 'Visibility of the event.', + }, + ], + }, + { + displayName: 'Reminders', + name: 'remindersUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Reminder', + typeOptions: { + multipleValues: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + useDefaultReminders: [ + false, + ], + }, + }, + options: [ + { + name: 'remindersValues', + displayName: 'Reminder', + values: [ + { + displayName: 'Method', + name: 'method', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Popup', + value: 'popup', + }, + ], + default: '', + }, + { + displayName: 'Minutes Before', + name: 'minutes', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 40320, + }, + default: 0, + }, + ], + } + ], + description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, + }, +/* -------------------------------------------------------------------------- */ +/* event:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + 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.`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + 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: [ + 'event', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'iCalUID', + name: 'iCalUID', + type: 'string', + default: '', + description: 'Specifies event ID in the iCalendar format to be included in the response', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + displayName: 'Order By', + name: 'orderBy', + type: 'options', + options: [ + { + name: 'Start Time', + value: 'startTime', + description: 'Order by the start date/time (ascending). This is only available when querying single events (i.e. the parameter singleEvents is True)', + }, + { + name: 'Updated', + value: 'updated', + description: 'Order by last modification time (ascending).', + }, + ], + default: '', + description: 'The order of the events returned in the result.', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: 'Free text search terms to find events that match these terms in any field, except for extended properties.', + }, + { + displayName: 'Show Deleted', + name: 'showDeleted', + type: 'boolean', + default: false, + description: 'Whether to include deleted events (with status equals "cancelled") in the result.', + }, + { + displayName: 'Show Hidden Invitations', + name: 'showHiddenInvitations', + type: 'boolean', + default: false, + description: 'Whether to include hidden invitations in the result.', + }, + { + displayName: 'Single Events', + name: 'singleEvents', + type: 'boolean', + default: false, + description: `Whether to expand recurring events into instances and only return single one-off
+ events and instances of recurring events, but not the underlying recurring events themselves.`, + }, + { + displayName: 'Time Max', + name: 'timeMax', + type: 'dateTime', + default: '', + description: `Upper bound (exclusive) for an event's start time to filter by`, + }, + { + displayName: 'Time Min', + name: 'timeMin', + type: 'dateTime', + default: '', + description: `Lower bound (exclusive) for an event's end time to filter by`, + }, + { + 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: 'Updated Min', + name: 'updatedMin', + type: 'dateTime', + default: '', + description: `Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by. + When specified, entries deleted since this time will always be included regardless of showDeleted`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Use Default Reminders', + name: 'useDefaultReminders', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: true, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'All Day', + name: 'allday', + type: 'boolean', + options: [ + { + name: 'Yes', + value: 'yes', + }, + { + name: 'No', + value: 'no', + }, + ], + default: 'no', + description: 'Wheater the event is all day or not', + }, + { + displayName: 'Attendees', + name: 'attendees', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Attendee', + }, + default: '', + description: 'The attendees of the event', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColors', + }, + default: '', + description: 'The color of the event.', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + description: 'End time of the event.', + }, + { + displayName: 'Guests Can Invite Others', + name: 'guestsCanInviteOthers', + type: 'boolean', + default: true, + description: 'Whether attendees other than the organizer can invite others to the event', + }, + { + displayName: 'Guests Can Modify', + name: 'guestsCanModify', + type: 'boolean', + default: false, + description: 'Whether attendees other than the organizer can modify the event', + }, + { + displayName: 'Guests Can See Other Guests', + name: 'guestsCanSeeOtherGuests', + type: 'boolean', + default: true, + description: `Whether attendees other than the organizer can see who the event's attendees are.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: 'Opaque identifier of the event', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Geographic location of the event as free-form text.', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + displayName: 'Repeat Frecuency', + name: 'repeatFrecuency', + type: 'options', + options: [ + { + name: 'Daily', + value: 'Daily', + }, + { + name: 'Weekly', + value: 'weekly', + }, + { + name: 'Monthly', + value: 'monthly', + }, + { + name: 'Yearly', + value: 'yearly', + }, + ], + default: '', + }, + { + displayName: 'Repeat Until', + name: 'repeatUntil', + type: 'dateTime', + default: '', + }, + { + displayName: 'Repeat How Many Times?', + name: 'repeatHowManyTimes', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + description: 'Start time of the event.', + }, + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Title of the event.', + }, + { + displayName: 'Show Me As', + name: 'showMeAs', + type: 'options', + options: [ + { + name: 'Available', + value: 'transparent', + description: 'The event does not block time on the calendar', + }, + { + name: 'Busy', + value: 'opaque', + description: ' The event does block time on the calendar.', + }, + ], + default: 'opaque', + description: 'Whether the event blocks time on the calendar', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + }, + { + displayName: 'Visibility', + name: 'visibility', + type: 'options', + options: [ + { + name: 'Confidential', + value: 'confidential', + description: 'The event is private. This value is provided for compatibility reasons.', + }, + { + name: 'Default', + value: 'default', + description: ' Uses the default visibility for events on the calendar.', + }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, + { + name: 'Private', + value: 'private', + description: 'The event is private and only event attendees may view event details.', + }, + ], + default: 'default', + description: 'Visibility of the event.', + }, + ], + }, + { + displayName: 'Reminders', + name: 'remindersUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Reminder', + typeOptions: { + multipleValues: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'update', + ], + useDefaultReminders: [ + false, + ], + }, + }, + options: [ + { + name: 'remindersValues', + displayName: 'Reminder', + values: [ + { + displayName: 'Method', + name: 'method', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Popup', + value: 'popup', + }, + ], + default: '', + }, + { + displayName: 'Minutes Before', + name: 'minutes', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 40320, + }, + default: 0, + }, + ], + } + ], + description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/EventInterface.ts b/packages/nodes-base/nodes/Google/EventInterface.ts new file mode 100644 index 0000000000..72bf96cc80 --- /dev/null +++ b/packages/nodes-base/nodes/Google/EventInterface.ts @@ -0,0 +1,26 @@ +import { IDataObject } from "n8n-workflow"; + +export interface IReminder { + useDefault?: boolean; + overrides?: IDataObject[]; +} + +export interface IEvent { + attendees?: IDataObject[]; + colorId?: string; + description?: string; + end?: IDataObject; + guestsCanInviteOthers?: boolean; + guestsCanModify?: boolean; + guestsCanSeeOtherGuests?: boolean; + id?: string; + location?: string; + maxAttendees?: number; + recurrence?: string[]; + reminders?: IReminder; + sendUpdates?: string; + start?: IDataObject; + summary?: string; + transparency?: string; + visibility?: string; +} diff --git a/packages/nodes-base/nodes/Google/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GenericFunctions.ts new file mode 100644 index 0000000000..80edb44370 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GenericFunctions.ts @@ -0,0 +1,57 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject +} from 'n8n-workflow'; + +export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com${resource}`, + json: true + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'googleOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Google Calendar error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts new file mode 100644 index 0000000000..fa6a8b28c7 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts @@ -0,0 +1,412 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import { + eventOperations, + eventFields, +} from './EventDescription'; + +import { + IEvent, +} from './EventInterface'; + +import * as moment from 'moment-timezone'; + +export class GoogleCalendar implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Calendar', + name: 'googleCalendar', + icon: 'file:googleCalendar.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Google Calendar API.', + defaults: { + name: 'Google Calendar', + color: '#3E87E4', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Event', + value: 'event', + }, + ], + default: 'event', + description: 'The resource to operate on.', + }, + ...eventOperations, + ...eventFields, + ], + }; + + methods = { + loadOptions: { + // Get all the calendars to display them to user so that he can + // select them easily + async getCalendars(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const calendars = await googleApiRequestAllItems.call(this, 'items', 'GET', '/calendar/v3/users/me/calendarList'); + for (const calendar of calendars) { + const calendarName = calendar.summary; + const calendarId = calendar.id; + returnData.push({ + name: calendarName, + value: calendarId, + }); + } + return returnData; + }, + // Get all the colors to display them to user so that he can + // select them easily + async getColors(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors'); + for (let key of Object.keys(calendar)) { + const colorName = calendar[key].background; + const colorId = key; + returnData.push({ + name: `${colorName} - ${colorId}`, + value: colorId, + }); + } + return returnData; + }, + // 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(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'event') { + //https://developers.google.com/calendar/v3/reference/events/insert + if (operation === 'create') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const start = this.getNodeParameter('start', i) as string; + const end = this.getNodeParameter('end', i) as string; + const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.maxAttendees) { + qs.maxAttendees = additionalFields.maxAttendees as number; + } + if (additionalFields.sendNotifications) { + qs.sendNotifications = additionalFields.sendNotifications as boolean; + } + if (additionalFields.sendUpdates) { + qs.sendUpdates = additionalFields.sendUpdates as string; + } + const body: IEvent = { + start: { + dateTime: start, + timeZone: additionalFields.timeZone || this.getTimezone(), + }, + end: { + dateTime: end, + timeZone: additionalFields.timeZone || this.getTimezone(), + } + }; + if (additionalFields.attendees) { + body.attendees = (additionalFields.attendees as string[]).map(attendee => { + return { email: attendee }; + }); + } + if (additionalFields.color) { + body.colorId = additionalFields.color as string; + } + if (additionalFields.description) { + body.description = additionalFields.description as string; + } + if (additionalFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean; + } + if (additionalFields.guestsCanModify) { + body.guestsCanModify = additionalFields.guestsCanModify as boolean; + } + if (additionalFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean; + } + if (additionalFields.id) { + body.id = additionalFields.id as string; + } + if (additionalFields.location) { + body.location = additionalFields.location as string; + } + if (additionalFields.summary) { + body.summary = additionalFields.summary as string; + } + if (additionalFields.showMeAs) { + body.transparency = additionalFields.showMeAs as string; + } + if (additionalFields.visibility) { + body.visibility = additionalFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (additionalFields.allday) { + body.start = { + date: moment(start).utc().format('YYYY-MM-DD'), + }; + body.end = { + date: moment(end).utc().format('YYYY-MM-DD'), + }; + } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (additionalFields.repeatHowManyTimes + && additionalFields.repeatUntil) { + throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + } + if (additionalFields.repeatFrecuency) { + body.recurrence?.push(`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`); + } + if (additionalFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${additionalFields.repeatHowManyTimes};`); + } + if (additionalFields.repeatUntil) { + body.recurrence?.push(`UNTIL=${moment(additionalFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } + responseData = await googleApiRequest.call(this, 'POST', `/calendar/v3/calendars/${calendarId}/events`, body, qs); + } + //https://developers.google.com/calendar/v3/reference/events/delete + if (operation === 'delete') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.sendUpdates) { + qs.sendUpdates = options.sendUpdates as number; + } + responseData = await googleApiRequest.call(this, 'DELETE', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}); + responseData = { success: true }; + } + //https://developers.google.com/calendar/v3/reference/events/get + if (operation === 'get') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } + responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs); + } + //https://developers.google.com/calendar/v3/reference/events/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const calendarId = this.getNodeParameter('calendar', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.iCalUID) { + qs.iCalUID = options.iCalUID as string; + } + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.orderBy) { + qs.orderBy = options.orderBy as number; + } + if (options.query) { + qs.q = options.query as number; + } + if (options.showDeleted) { + qs.showDeleted = options.showDeleted as boolean; + } + if (options.showHiddenInvitations) { + qs.showHiddenInvitations = options.showHiddenInvitations as boolean; + } + if (options.singleEvents) { + qs.singleEvents = options.singleEvents as boolean; + } + if (options.timeMax) { + qs.timeMax = options.timeMax as string; + } + if (options.timeMin) { + qs.timeMin = options.timeMin as string; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } + if (options.updatedMin) { + qs.updatedMin = options.updatedMin as string; + } + if (returnAll) { + responseData = await googleApiRequestAllItems.call(this, 'items', 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + responseData = responseData.items; + } + } + //https://developers.google.com/calendar/v3/reference/events/patch + if (operation === 'update') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + if (updateFields.maxAttendees) { + qs.maxAttendees = updateFields.maxAttendees as number; + } + if (updateFields.sendNotifications) { + qs.sendNotifications = updateFields.sendNotifications as boolean; + } + if (updateFields.sendUpdates) { + qs.sendUpdates = updateFields.sendUpdates as string; + } + const body: IEvent = {}; + if (updateFields.start) { + body.start = { + dateTime: updateFields.start, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.end) { + body.end = { + dateTime: updateFields.end, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.attendees) { + body.attendees = (updateFields.attendees as string[]).map(attendee => { + return { email: attendee }; + }); + } + if (updateFields.color) { + body.colorId = updateFields.color as string; + } + if (updateFields.description) { + body.description = updateFields.description as string; + } + if (updateFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean; + } + if (updateFields.guestsCanModify) { + body.guestsCanModify = updateFields.guestsCanModify as boolean; + } + if (updateFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean; + } + if (updateFields.id) { + body.id = updateFields.id as string; + } + if (updateFields.location) { + body.location = updateFields.location as string; + } + if (updateFields.summary) { + body.summary = updateFields.summary as string; + } + if (updateFields.showMeAs) { + body.transparency = updateFields.showMeAs as string; + } + if (updateFields.visibility) { + body.visibility = updateFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (updateFields.allday + && updateFields.start + && updateFields.end) { + body.start = { + date: moment(updateFields.start as string).utc().format('YYYY-MM-DD'), + }; + body.end = { + date: moment(updateFields.end as string).utc().format('YYYY-MM-DD'), + }; + } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (updateFields.repeatHowManyTimes + && updateFields.repeatUntil) { + throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + } + if (updateFields.repeatFrecuency) { + body.recurrence?.push(`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`); + } + if (updateFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); + } + if (updateFields.repeatUntil) { + body.recurrence?.push(`UNTIL=${moment(updateFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } else { + delete body.recurrence; + } + responseData = await googleApiRequest.call(this, 'PATCH', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, body, qs); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/googleCalendar.png b/packages/nodes-base/nodes/Google/googleCalendar.png new file mode 100644 index 0000000000000000000000000000000000000000..2a2bfa1c7b4754b9d4695b0beab270a62ab0a4da GIT binary patch literal 6937 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 IoMq(y0lY1>SO5S3 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 746f91f723..b8152aed6c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,7 +42,8 @@ "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -115,7 +116,8 @@ "dist/nodes/Github/Github.node.js", "dist/nodes/Github/GithubTrigger.node.js", "dist/nodes/Gitlab/Gitlab.node.js", - "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Google/GoogleCalendar.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", @@ -189,7 +191,8 @@ "@types/gm": "^1.18.2", "@types/imap-simple": "^4.2.0", "@types/jest": "^24.0.18", - "@types/lodash.set": "^4.3.6", + "@types/lodash.set": "^4.3.6", + "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.3.6", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", @@ -215,7 +218,8 @@ "imap-simple": "^4.3.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", + "lodash.unset": "^4.5.2", + "moment-timezone": "0.5.28", "mongodb": "^3.3.2", "mysql2": "^2.0.1", "n8n-core": "~0.20.0", From f45a7cb031009bf0ba6d1ccd57cac9118b57f66b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 16 Mar 2020 14:34:04 +0100 Subject: [PATCH 2/2] :zap: Minor change to Google Calendar-Node --- packages/nodes-base/nodes/Google/EventDescription.ts | 12 ++++++------ .../nodes-base/nodes/Google/GoogleCalendar.node.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/nodes/Google/EventDescription.ts b/packages/nodes-base/nodes/Google/EventDescription.ts index fb1ea6a90f..e92582a847 100644 --- a/packages/nodes-base/nodes/Google/EventDescription.ts +++ b/packages/nodes-base/nodes/Google/EventDescription.ts @@ -316,7 +316,7 @@ export const eventFields = [ loadOptionsMethod: 'getTimezones', }, default: '', - description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.' }, { displayName: 'Visibility', @@ -333,16 +333,16 @@ export const eventFields = [ value: 'default', description: ' Uses the default visibility for events on the calendar.', }, - { - name: 'Public', - value: 'public', - description: 'The event is public and event details are visible to all readers of the calendar.', - }, { name: 'Private', value: 'private', description: 'The event is private and only event attendees may view event details.', }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, ], default: 'default', description: 'Visibility of the event.', diff --git a/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts index fa6a8b28c7..c7eb35794e 100644 --- a/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts +++ b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts @@ -89,7 +89,7 @@ export class GoogleCalendar implements INodeType { async getColors(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors'); - for (let key of Object.keys(calendar)) { + for (const key of Object.keys(calendar)) { const colorName = calendar[key].background; const colorId = key; returnData.push({