diff --git a/packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts b/packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts new file mode 100644 index 000000000..0301bf2c5 --- /dev/null +++ b/packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts @@ -0,0 +1,46 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HelpScoutOAuth2Api implements ICredentialType { + name = 'helpScoutOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'HelpScout OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://secure.helpscout.net/authentication/authorizeClientApplication', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.helpscout.net/v2/oauth2/token', + required: true, + }, + { + 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/HelpScout/ConversationDescription.ts b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts new file mode 100644 index 000000000..5d882ac80 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts @@ -0,0 +1,598 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const conversationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new conversation', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a conversation', + }, + { + name: 'Get', + value: 'get', + description: 'Get a conversation', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all conversations', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const conversationFields = [ +/* -------------------------------------------------------------------------- */ +/* conversation:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Mailbox', + name: 'mailboxId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getMailboxes', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: 'ID of a mailbox where the conversation is being created', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Pending', + value: 'pending', + }, + ], + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: 'Conversation status', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: `Conversation’s subject`, + }, + { + displayName: 'Type', + name: 'type', + required: true, + type: 'options', + options: [ + { + name: 'Chat', + value: 'chat', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Phone', + value: 'phone', + }, + ], + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: 'Conversation type', + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'By default the response only contain the ID to resource
. If this option gets activated it
will resolve the data automatically.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + options: [ + { + displayName: 'Assign To', + name: 'assignTo', + type: 'number', + default: 0, + description: 'The Help Scout user assigned to the conversation.', + }, + { + displayName: 'Auto Reply', + name: 'autoReply', + type: 'boolean', + default: false, + description: `When autoReply is set to true, an auto reply will be sent
+ as long as there is at least one customer thread in the conversation.`, + }, + { + displayName: 'Closed At', + name: 'closedAt', + type: 'dateTime', + default: '', + description: `When the conversation was closed, only applicable for imported conversations`, + }, + { + displayName: 'Created At', + name: 'createdAt', + type: 'dateTime', + default: '', + description: `When this conversation was created - ISO 8601 date time`, + }, + { + displayName: 'Customer Email', + name: 'customerEmail', + type: 'string', + default: '', + }, + { + displayName: 'Customer ID', + name: 'customerId', + type: 'number', + default: 0, + }, + { + displayName: 'Imported', + name: 'imported', + type: 'boolean', + default: false, + description: `When imported is set to true, no outgoing emails or notifications will be generated.`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'List of tags to be be added to the conversation', + }, + { + displayName: 'User ID', + name: 'user', + type: 'number', + default: 0, + description: 'ID of the user who is adding the conversation and threads.', + }, + ] + }, + { + displayName: 'Threads', + name: 'threadsUi', + placeholder: 'Add Thread', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Thread', + name: 'threadsValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Chat', + value: 'chat' + }, + { + name: 'Customer', + value: 'customer' + }, + { + name: 'Note', + value: 'note' + }, + { + name: 'Phone', + value: 'phone' + }, + { + name: 'Reply', + value: 'reply' + }, + ], + default: '', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true + }, + default: '', + description: 'The message text.' + }, + { + displayName: 'Bcc', + name: 'bcc', + displayOptions: { + show: { + type: [ + 'customer' + ], + }, + }, + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Email', + }, + default: [], + description: 'Email addresses.' + }, + { + displayName: 'Cc', + name: 'cc', + displayOptions: { + show: { + type: [ + 'customer' + ], + }, + }, + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Email', + }, + default: [], + description: 'Email addresses.' + }, + { + displayName: 'Draft', + name: 'draft', + displayOptions: { + show: { + type: [ + 'reply' + ], + }, + }, + type: 'boolean', + default: false, + description: 'If set to true, a draft reply is created', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* conversation:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'get', + ], + }, + }, + description: 'conversation ID', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'conversation ID', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Assign To', + name: 'assignTo', + type: 'number', + default: 0, + description: 'Filters conversations by assignee id', + }, + { + displayName: 'Embed', + name: 'embed', + type: 'options', + options: [ + { + name: 'Threads', + value: 'threads', + }, + ], + default: '', + description: 'Allows embedding/loading of sub-entities', + }, + { + displayName: 'Folder ID', + name: 'folder', + type: 'string', + default: '', + description: 'Filters conversations from a specific folder id', + }, + { + displayName: 'Mailbox ID', + name: 'mailbox', + type: 'string', + default: '', + description: 'Filters conversations from a specific mailbox', + }, + { + displayName: 'Modified Since', + name: 'modifiedSince', + type: 'dateTime', + default: '', + description: 'Returns only conversations that were modified after this date', + }, + { + displayName: 'Number', + name: 'number', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Looks up conversation by conversation number', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Advanced search Examples' + }, + { + displayName: 'Sort Field', + name: 'sortField', + type: 'options', + options: [ + { + name: 'Created At', + value: 'createdAt', + }, + { + name: 'customer Email', + value: 'customerEmail', + }, + { + name: 'customer Name', + value: 'customerName', + }, + { + name: 'Mailbox ID', + value: 'mailboxid', + }, + { + name: 'Modified At', + value: 'modifiedAt', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Score', + value: 'score', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Subject', + value: 'subject', + }, + ], + default: '', + description: 'Sorts the result by specified field', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'asc', + }, + { + name: 'Desc', + value: 'desc', + }, + ], + default: 'desc', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'All', + value: 'all', + }, + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Open', + value: 'open', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Spam', + value: 'spam', + }, + ], + default: 'active', + description: 'Filter conversation by status', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'Filter conversation by tags', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts b/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts new file mode 100644 index 000000000..ea617f5aa --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts @@ -0,0 +1,18 @@ +import { IDataObject } from 'n8n-workflow'; + +export interface IConversation { + assignTo?: number; + autoReply?: boolean; + closedAt?: string; + createdAt?: string; + customer?: IDataObject; + fields?: IDataObject[]; + imported?: boolean; + mailboxId?: number; + status?: string; + subject?: string; + tags?: IDataObject[]; + threads?: IDataObject[]; + type?: string; + user?: number; +} diff --git a/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts b/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts new file mode 100644 index 000000000..3c41a76c7 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts @@ -0,0 +1,1579 @@ +export const countriesCodes = [ + { + 'name': 'Afghanistan', + 'alpha2': 'AF', + 'alpha3': 'AFG', + 'numeric': '004' + }, + { + 'name': 'Åland Islands', + 'alpha2': 'AX', + 'alpha3': 'ALA', + 'numeric': '248', + 'altName': 'Aland Islands' + }, + { + 'name': 'Albania', + 'alpha2': 'AL', + 'alpha3': 'ALB', + 'numeric': '008' + }, + { + 'name': 'Algeria', + 'alpha2': 'DZ', + 'alpha3': 'DZA', + 'numeric': '012' + }, + { + 'name': 'American Samoa', + 'alpha2': 'AS', + 'alpha3': 'ASM', + 'numeric': '016' + }, + { + 'name': 'Andorra', + 'alpha2': 'AD', + 'alpha3': 'AND', + 'numeric': '020' + }, + { + 'name': 'Angola', + 'alpha2': 'AO', + 'alpha3': 'AGO', + 'numeric': '024' + }, + { + 'name': 'Anguilla', + 'alpha2': 'AI', + 'alpha3': 'AIA', + 'numeric': '660' + }, + { + 'name': 'Antarctica', + 'alpha2': 'AQ', + 'alpha3': 'ATA', + 'numeric': '010' + }, + { + 'name': 'Antigua and Barbuda', + 'alpha2': 'AG', + 'alpha3': 'ATG', + 'numeric': '028' + }, + { + 'name': 'Argentina', + 'alpha2': 'AR', + 'alpha3': 'ARG', + 'numeric': '032' + }, + { + 'name': 'Armenia', + 'alpha2': 'AM', + 'alpha3': 'ARM', + 'numeric': '051' + }, + { + 'name': 'Aruba', + 'alpha2': 'AW', + 'alpha3': 'ABW', + 'numeric': '533' + }, + { + 'name': 'Australia', + 'alpha2': 'AU', + 'alpha3': 'AUS', + 'numeric': '036' + }, + { + 'name': 'Austria', + 'alpha2': 'AT', + 'alpha3': 'AUT', + 'numeric': '040' + }, + { + 'name': 'Azerbaijan', + 'alpha2': 'AZ', + 'alpha3': 'AZE', + 'numeric': '031' + }, + { + 'name': 'Bahamas (the)', + 'alpha2': 'BS', + 'alpha3': 'BHS', + 'numeric': '044', + 'altName': 'Bahamas' + }, + { + 'name': 'Bahrain', + 'alpha2': 'BH', + 'alpha3': 'BHR', + 'numeric': '048' + }, + { + 'name': 'Bangladesh', + 'alpha2': 'BD', + 'alpha3': 'BGD', + 'numeric': '050' + }, + { + 'name': 'Barbados', + 'alpha2': 'BB', + 'alpha3': 'BRB', + 'numeric': '052' + }, + { + 'name': 'Belarus', + 'alpha2': 'BY', + 'alpha3': 'BLR', + 'numeric': '112' + }, + { + 'name': 'Belgium', + 'alpha2': 'BE', + 'alpha3': 'BEL', + 'numeric': '056' + }, + { + 'name': 'Belize', + 'alpha2': 'BZ', + 'alpha3': 'BLZ', + 'numeric': '084' + }, + { + 'name': 'Benin', + 'alpha2': 'BJ', + 'alpha3': 'BEN', + 'numeric': '204' + }, + { + 'name': 'Bermuda', + 'alpha2': 'BM', + 'alpha3': 'BMU', + 'numeric': '060' + }, + { + 'name': 'Bhutan', + 'alpha2': 'BT', + 'alpha3': 'BTN', + 'numeric': '064' + }, + { + 'name': 'Bolivia (Plurinational State of)', + 'alpha2': 'BO', + 'alpha3': 'BOL', + 'numeric': '068', + 'altName': 'Bolivia' + }, + { + 'name': 'Bonaire, Sint Eustatius and Saba', + 'alpha2': 'BQ', + 'alpha3': 'BES', + 'numeric': '535' + }, + { + 'name': 'Bosnia and Herzegovina', + 'alpha2': 'BA', + 'alpha3': 'BIH', + 'numeric': '070' + }, + { + 'name': 'Botswana', + 'alpha2': 'BW', + 'alpha3': 'BWA', + 'numeric': '072' + }, + { + 'name': 'Bouvet Island', + 'alpha2': 'BV', + 'alpha3': 'BVT', + 'numeric': '074' + }, + { + 'name': 'Brazil', + 'alpha2': 'BR', + 'alpha3': 'BRA', + 'numeric': '076' + }, + { + 'name': 'British Indian Ocean Territory (the)', + 'alpha2': 'IO', + 'alpha3': 'IOT', + 'numeric': '086', + 'altName': 'British Indian Ocean Territory' + }, + { + 'name': 'Brunei Darussalam', + 'alpha2': 'BN', + 'alpha3': 'BRN', + 'numeric': '096', + 'shortName': 'Brunei' + }, + { + 'name': 'Bulgaria', + 'alpha2': 'BG', + 'alpha3': 'BGR', + 'numeric': '100' + }, + { + 'name': 'Burkina Faso', + 'alpha2': 'BF', + 'alpha3': 'BFA', + 'numeric': '854' + }, + { + 'name': 'Burundi', + 'alpha2': 'BI', + 'alpha3': 'BDI', + 'numeric': '108' + }, + { + 'name': 'Cabo Verde', + 'alpha2': 'CV', + 'alpha3': 'CPV', + 'numeric': '132', + 'altName': 'Cape Verde' + }, + { + 'name': 'Cambodia', + 'alpha2': 'KH', + 'alpha3': 'KHM', + 'numeric': '116' + }, + { + 'name': 'Cameroon', + 'alpha2': 'CM', + 'alpha3': 'CMR', + 'numeric': '120' + }, + { + 'name': 'Canada', + 'alpha2': 'CA', + 'alpha3': 'CAN', + 'numeric': '124' + }, + { + 'name': 'Cayman Islands (the)', + 'alpha2': 'KY', + 'alpha3': 'CYM', + 'numeric': '136', + 'altName': 'Cayman Islands' + }, + { + 'name': 'Central African Republic (the)', + 'alpha2': 'CF', + 'alpha3': 'CAF', + 'numeric': '140', + 'altName': 'Central African Republic' + }, + { + 'name': 'Chad', + 'alpha2': 'TD', + 'alpha3': 'TCD', + 'numeric': '148' + }, + { + 'name': 'Chile', + 'alpha2': 'CL', + 'alpha3': 'CHL', + 'numeric': '152' + }, + { + 'name': 'China', + 'alpha2': 'CN', + 'alpha3': 'CHN', + 'numeric': '156' + }, + { + 'name': 'Christmas Island', + 'alpha2': 'CX', + 'alpha3': 'CXR', + 'numeric': '162' + }, + { + 'name': 'Cocos (Keeling) Islands (the)', + 'alpha2': 'CC', + 'alpha3': 'CCK', + 'numeric': '166', + 'altName': 'Cocos (Keeling) Islands', + 'shortName': 'Cocos Islands' + }, + { + 'name': 'Colombia', + 'alpha2': 'CO', + 'alpha3': 'COL', + 'numeric': '170' + }, + { + 'name': 'Comoros (the)', + 'alpha2': 'KM', + 'alpha3': 'COM', + 'numeric': '174', + 'altName': 'Comoros' + }, + { + 'name': 'Congo (the Democratic Republic of the)', + 'alpha2': 'CD', + 'alpha3': 'COD', + 'numeric': '180', + 'altName': 'Congo, (Kinshasa)', + 'shortName': 'Democratic Republic of the Congo' + }, + { + 'name': 'Congo (the)', + 'alpha2': 'CG', + 'alpha3': 'COG', + 'numeric': '178', + 'altName': 'Congo (Brazzaville)', + 'shortName': 'Republic of the Congo' + }, + { + 'name': 'Cook Islands (the)', + 'alpha2': 'CK', + 'alpha3': 'COK', + 'numeric': '184', + 'altName': 'Cook Islands' + }, + { + 'name': 'Costa Rica', + 'alpha2': 'CR', + 'alpha3': 'CRI', + 'numeric': '188' + }, + { + 'name': 'Côte d\'Ivoire', + 'alpha2': 'CI', + 'alpha3': 'CIV', + 'numeric': '384', + 'shortName': 'Ivory Coast' + }, + { + 'name': 'Croatia', + 'alpha2': 'HR', + 'alpha3': 'HRV', + 'numeric': '191' + }, + { + 'name': 'Cuba', + 'alpha2': 'CU', + 'alpha3': 'CUB', + 'numeric': '192' + }, + { + 'name': 'Curaçao', + 'alpha2': 'CW', + 'alpha3': 'CUW', + 'numeric': '531', + 'shortName': 'Curacao' + }, + { + 'name': 'Cyprus', + 'alpha2': 'CY', + 'alpha3': 'CYP', + 'numeric': '196' + }, + { + 'name': 'Czechia', + 'alpha2': 'CZ', + 'alpha3': 'CZE', + 'numeric': '203', + 'altName': 'Czech Republic' + }, + { + 'name': 'Denmark', + 'alpha2': 'DK', + 'alpha3': 'DNK', + 'numeric': '208' + }, + { + 'name': 'Djibouti', + 'alpha2': 'DJ', + 'alpha3': 'DJI', + 'numeric': '262' + }, + { + 'name': 'Dominica', + 'alpha2': 'DM', + 'alpha3': 'DMA', + 'numeric': '212' + }, + { + 'name': 'Dominican Republic (the)', + 'alpha2': 'DO', + 'alpha3': 'DOM', + 'numeric': '214', + 'altName': 'Dominican Republic' + }, + { + 'name': 'Ecuador', + 'alpha2': 'EC', + 'alpha3': 'ECU', + 'numeric': '218' + }, + { + 'name': 'Egypt', + 'alpha2': 'EG', + 'alpha3': 'EGY', + 'numeric': '818' + }, + { + 'name': 'El Salvador', + 'alpha2': 'SV', + 'alpha3': 'SLV', + 'numeric': '222' + }, + { + 'name': 'Equatorial Guinea', + 'alpha2': 'GQ', + 'alpha3': 'GNQ', + 'numeric': '226' + }, + { + 'name': 'Eritrea', + 'alpha2': 'ER', + 'alpha3': 'ERI', + 'numeric': '232' + }, + { + 'name': 'Estonia', + 'alpha2': 'EE', + 'alpha3': 'EST', + 'numeric': '233' + }, + { + 'name': 'Ethiopia', + 'alpha2': 'ET', + 'alpha3': 'ETH', + 'numeric': '231' + }, + { + 'name': 'Falkland Islands (the) [Malvinas]', + 'alpha2': 'FK', + 'alpha3': 'FLK', + 'numeric': '238', + 'altName': 'Falkland Islands (Malvinas)', + 'shortName': 'Falkland Islands' + }, + { + 'name': 'Faroe Islands (the)', + 'alpha2': 'FO', + 'alpha3': 'FRO', + 'numeric': '234', + 'altName': 'Faroe Islands' + }, + { + 'name': 'Fiji', + 'alpha2': 'FJ', + 'alpha3': 'FJI', + 'numeric': '242' + }, + { + 'name': 'Finland', + 'alpha2': 'FI', + 'alpha3': 'FIN', + 'numeric': '246' + }, + { + 'name': 'France', + 'alpha2': 'FR', + 'alpha3': 'FRA', + 'numeric': '250' + }, + { + 'name': 'French Guiana', + 'alpha2': 'GF', + 'alpha3': 'GUF', + 'numeric': '254' + }, + { + 'name': 'French Polynesia', + 'alpha2': 'PF', + 'alpha3': 'PYF', + 'numeric': '258' + }, + { + 'name': 'French Southern Territories (the)', + 'alpha2': 'TF', + 'alpha3': 'ATF', + 'numeric': '260', + 'altName': 'French Southern Territories' + }, + { + 'name': 'Gabon', + 'alpha2': 'GA', + 'alpha3': 'GAB', + 'numeric': '266' + }, + { + 'name': 'Gambia (the)', + 'alpha2': 'GM', + 'alpha3': 'GMB', + 'numeric': '270', + 'altName': 'Gambia' + }, + { + 'name': 'Georgia', + 'alpha2': 'GE', + 'alpha3': 'GEO', + 'numeric': '268' + }, + { + 'name': 'Germany', + 'alpha2': 'DE', + 'alpha3': 'DEU', + 'numeric': '276' + }, + { + 'name': 'Ghana', + 'alpha2': 'GH', + 'alpha3': 'GHA', + 'numeric': '288' + }, + { + 'name': 'Gibraltar', + 'alpha2': 'GI', + 'alpha3': 'GIB', + 'numeric': '292' + }, + { + 'name': 'Greece', + 'alpha2': 'GR', + 'alpha3': 'GRC', + 'numeric': '300' + }, + { + 'name': 'Greenland', + 'alpha2': 'GL', + 'alpha3': 'GRL', + 'numeric': '304' + }, + { + 'name': 'Grenada', + 'alpha2': 'GD', + 'alpha3': 'GRD', + 'numeric': '308' + }, + { + 'name': 'Guadeloupe', + 'alpha2': 'GP', + 'alpha3': 'GLP', + 'numeric': '312' + }, + { + 'name': 'Guam', + 'alpha2': 'GU', + 'alpha3': 'GUM', + 'numeric': '316' + }, + { + 'name': 'Guatemala', + 'alpha2': 'GT', + 'alpha3': 'GTM', + 'numeric': '320' + }, + { + 'name': 'Guernsey', + 'alpha2': 'GG', + 'alpha3': 'GGY', + 'numeric': '831' + }, + { + 'name': 'Guinea', + 'alpha2': 'GN', + 'alpha3': 'GIN', + 'numeric': '324' + }, + { + 'name': 'Guinea-Bissau', + 'alpha2': 'GW', + 'alpha3': 'GNB', + 'numeric': '624' + }, + { + 'name': 'Guyana', + 'alpha2': 'GY', + 'alpha3': 'GUY', + 'numeric': '328' + }, + { + 'name': 'Haiti', + 'alpha2': 'HT', + 'alpha3': 'HTI', + 'numeric': '332' + }, + { + 'name': 'Heard Island and McDonald Islands', + 'alpha2': 'HM', + 'alpha3': 'HMD', + 'numeric': '334', + 'altName': 'Heard and Mcdonald Islands' + }, + { + 'name': 'Holy See (the)', + 'alpha2': 'VA', + 'alpha3': 'VAT', + 'numeric': '336', + 'altName': 'Holy See (Vatican City State)', + 'shortName': 'Vatican' + }, + { + 'name': 'Honduras', + 'alpha2': 'HN', + 'alpha3': 'HND', + 'numeric': '340' + }, + { + 'name': 'Hong Kong', + 'alpha2': 'HK', + 'alpha3': 'HKG', + 'numeric': '344', + 'altName': 'Hong Kong, SAR China' + }, + { + 'name': 'Hungary', + 'alpha2': 'HU', + 'alpha3': 'HUN', + 'numeric': '348' + }, + { + 'name': 'Iceland', + 'alpha2': 'IS', + 'alpha3': 'ISL', + 'numeric': '352' + }, + { + 'name': 'India', + 'alpha2': 'IN', + 'alpha3': 'IND', + 'numeric': '356' + }, + { + 'name': 'Indonesia', + 'alpha2': 'ID', + 'alpha3': 'IDN', + 'numeric': '360' + }, + { + 'name': 'Iran (Islamic Republic of)', + 'alpha2': 'IR', + 'alpha3': 'IRN', + 'numeric': '364', + 'altName': 'Iran, Islamic Republic of', + 'shortName': 'Iran' + }, + { + 'name': 'Iraq', + 'alpha2': 'IQ', + 'alpha3': 'IRQ', + 'numeric': '368' + }, + { + 'name': 'Ireland', + 'alpha2': 'IE', + 'alpha3': 'IRL', + 'numeric': '372' + }, + { + 'name': 'Isle of Man', + 'alpha2': 'IM', + 'alpha3': 'IMN', + 'numeric': '833' + }, + { + 'name': 'Israel', + 'alpha2': 'IL', + 'alpha3': 'ISR', + 'numeric': '376' + }, + { + 'name': 'Italy', + 'alpha2': 'IT', + 'alpha3': 'ITA', + 'numeric': '380' + }, + { + 'name': 'Jamaica', + 'alpha2': 'JM', + 'alpha3': 'JAM', + 'numeric': '388' + }, + { + 'name': 'Japan', + 'alpha2': 'JP', + 'alpha3': 'JPN', + 'numeric': '392' + }, + { + 'name': 'Jersey', + 'alpha2': 'JE', + 'alpha3': 'JEY', + 'numeric': '832' + }, + { + 'name': 'Jordan', + 'alpha2': 'JO', + 'alpha3': 'JOR', + 'numeric': '400' + }, + { + 'name': 'Kazakhstan', + 'alpha2': 'KZ', + 'alpha3': 'KAZ', + 'numeric': '398' + }, + { + 'name': 'Kenya', + 'alpha2': 'KE', + 'alpha3': 'KEN', + 'numeric': '404' + }, + { + 'name': 'Kiribati', + 'alpha2': 'KI', + 'alpha3': 'KIR', + 'numeric': '296' + }, + { + 'name': 'Korea (the Democratic People\'s Republic of)', + 'alpha2': 'KP', + 'alpha3': 'PRK', + 'numeric': '408', + 'altName': 'Korea (North)', + 'shortName': 'North Korea' + }, + { + 'name': 'Korea (the Republic of)', + 'alpha2': 'KR', + 'alpha3': 'KOR', + 'numeric': '410', + 'altName': 'Korea (South)', + 'shortName': 'South Korea' + }, + { + 'name': 'Kuwait', + 'alpha2': 'KW', + 'alpha3': 'KWT', + 'numeric': '414' + }, + { + 'name': 'Kyrgyzstan', + 'alpha2': 'KG', + 'alpha3': 'KGZ', + 'numeric': '417' + }, + { + 'name': 'Lao People\'s Democratic Republic (the)', + 'alpha2': 'LA', + 'alpha3': 'LAO', + 'numeric': '418', + 'altName': 'Lao PDR', + 'shortName': 'Laos' + }, + { + 'name': 'Latvia', + 'alpha2': 'LV', + 'alpha3': 'LVA', + 'numeric': '428' + }, + { + 'name': 'Lebanon', + 'alpha2': 'LB', + 'alpha3': 'LBN', + 'numeric': '422' + }, + { + 'name': 'Lesotho', + 'alpha2': 'LS', + 'alpha3': 'LSO', + 'numeric': '426' + }, + { + 'name': 'Liberia', + 'alpha2': 'LR', + 'alpha3': 'LBR', + 'numeric': '430' + }, + { + 'name': 'Libya', + 'alpha2': 'LY', + 'alpha3': 'LBY', + 'numeric': '434' + }, + { + 'name': 'Liechtenstein', + 'alpha2': 'LI', + 'alpha3': 'LIE', + 'numeric': '438' + }, + { + 'name': 'Lithuania', + 'alpha2': 'LT', + 'alpha3': 'LTU', + 'numeric': '440' + }, + { + 'name': 'Luxembourg', + 'alpha2': 'LU', + 'alpha3': 'LUX', + 'numeric': '442' + }, + { + 'name': 'Macao', + 'alpha2': 'MO', + 'alpha3': 'MAC', + 'numeric': '446', + 'altName': 'Macao, SAR China', + 'shortName': 'Macau' + }, + { + 'name': 'Macedonia (the former Yugoslav Republic of)', + 'alpha2': 'MK', + 'alpha3': 'MKD', + 'numeric': '807', + 'altName': 'Macedonia, Republic of', + 'shortName': 'Macedonia' + }, + { + 'name': 'Madagascar', + 'alpha2': 'MG', + 'alpha3': 'MDG', + 'numeric': '450' + }, + { + 'name': 'Malawi', + 'alpha2': 'MW', + 'alpha3': 'MWI', + 'numeric': '454' + }, + { + 'name': 'Malaysia', + 'alpha2': 'MY', + 'alpha3': 'MYS', + 'numeric': '458' + }, + { + 'name': 'Maldives', + 'alpha2': 'MV', + 'alpha3': 'MDV', + 'numeric': '462' + }, + { + 'name': 'Mali', + 'alpha2': 'ML', + 'alpha3': 'MLI', + 'numeric': '466' + }, + { + 'name': 'Malta', + 'alpha2': 'MT', + 'alpha3': 'MLT', + 'numeric': '470' + }, + { + 'name': 'Marshall Islands (the)', + 'alpha2': 'MH', + 'alpha3': 'MHL', + 'numeric': '584', + 'altName': 'Marshall Islands' + }, + { + 'name': 'Martinique', + 'alpha2': 'MQ', + 'alpha3': 'MTQ', + 'numeric': '474' + }, + { + 'name': 'Mauritania', + 'alpha2': 'MR', + 'alpha3': 'MRT', + 'numeric': '478' + }, + { + 'name': 'Mauritius', + 'alpha2': 'MU', + 'alpha3': 'MUS', + 'numeric': '480' + }, + { + 'name': 'Mayotte', + 'alpha2': 'YT', + 'alpha3': 'MYT', + 'numeric': '175' + }, + { + 'name': 'Mexico', + 'alpha2': 'MX', + 'alpha3': 'MEX', + 'numeric': '484' + }, + { + 'name': 'Micronesia (Federated States of)', + 'alpha2': 'FM', + 'alpha3': 'FSM', + 'numeric': '583', + 'altName': 'Micronesia, Federated States of', + 'shortName': 'Micronesia' + }, + { + 'name': 'Moldova (the Republic of)', + 'alpha2': 'MD', + 'alpha3': 'MDA', + 'numeric': '498', + 'altName': 'Moldova' + }, + { + 'name': 'Monaco', + 'alpha2': 'MC', + 'alpha3': 'MCO', + 'numeric': '492' + }, + { + 'name': 'Mongolia', + 'alpha2': 'MN', + 'alpha3': 'MNG', + 'numeric': '496' + }, + { + 'name': 'Montenegro', + 'alpha2': 'ME', + 'alpha3': 'MNE', + 'numeric': '499' + }, + { + 'name': 'Montserrat', + 'alpha2': 'MS', + 'alpha3': 'MSR', + 'numeric': '500' + }, + { + 'name': 'Morocco', + 'alpha2': 'MA', + 'alpha3': 'MAR', + 'numeric': '504' + }, + { + 'name': 'Mozambique', + 'alpha2': 'MZ', + 'alpha3': 'MOZ', + 'numeric': '508' + }, + { + 'name': 'Myanmar', + 'alpha2': 'MM', + 'alpha3': 'MMR', + 'numeric': '104' + }, + { + 'name': 'Namibia', + 'alpha2': 'NA', + 'alpha3': 'NAM', + 'numeric': '516' + }, + { + 'name': 'Nauru', + 'alpha2': 'NR', + 'alpha3': 'NRU', + 'numeric': '520' + }, + { + 'name': 'Nepal', + 'alpha2': 'NP', + 'alpha3': 'NPL', + 'numeric': '524' + }, + { + 'name': 'Netherlands (the)', + 'alpha2': 'NL', + 'alpha3': 'NLD', + 'numeric': '528', + 'altName': 'Netherlands' + }, + { + 'name': 'New Caledonia', + 'alpha2': 'NC', + 'alpha3': 'NCL', + 'numeric': '540' + }, + { + 'name': 'New Zealand', + 'alpha2': 'NZ', + 'alpha3': 'NZL', + 'numeric': '554' + }, + { + 'name': 'Nicaragua', + 'alpha2': 'NI', + 'alpha3': 'NIC', + 'numeric': '558' + }, + { + 'name': 'Niger (the)', + 'alpha2': 'NE', + 'alpha3': 'NER', + 'numeric': '562', + 'altName': 'Niger' + }, + { + 'name': 'Nigeria', + 'alpha2': 'NG', + 'alpha3': 'NGA', + 'numeric': '566' + }, + { + 'name': 'Niue', + 'alpha2': 'NU', + 'alpha3': 'NIU', + 'numeric': '570' + }, + { + 'name': 'Norfolk Island', + 'alpha2': 'NF', + 'alpha3': 'NFK', + 'numeric': '574' + }, + { + 'name': 'Northern Mariana Islands (the)', + 'alpha2': 'MP', + 'alpha3': 'MNP', + 'numeric': '580', + 'altName': 'Northern Mariana Islands' + }, + { + 'name': 'Norway', + 'alpha2': 'NO', + 'alpha3': 'NOR', + 'numeric': '578' + }, + { + 'name': 'Oman', + 'alpha2': 'OM', + 'alpha3': 'OMN', + 'numeric': '512' + }, + { + 'name': 'Pakistan', + 'alpha2': 'PK', + 'alpha3': 'PAK', + 'numeric': '586' + }, + { + 'name': 'Palau', + 'alpha2': 'PW', + 'alpha3': 'PLW', + 'numeric': '585' + }, + { + 'name': 'Palestine, State of', + 'alpha2': 'PS', + 'alpha3': 'PSE', + 'numeric': '275', + 'altName': 'Palestinian Territory', + 'shortName': 'Palestine' + }, + { + 'name': 'Panama', + 'alpha2': 'PA', + 'alpha3': 'PAN', + 'numeric': '591' + }, + { + 'name': 'Papua New Guinea', + 'alpha2': 'PG', + 'alpha3': 'PNG', + 'numeric': '598' + }, + { + 'name': 'Paraguay', + 'alpha2': 'PY', + 'alpha3': 'PRY', + 'numeric': '600' + }, + { + 'name': 'Peru', + 'alpha2': 'PE', + 'alpha3': 'PER', + 'numeric': '604' + }, + { + 'name': 'Philippines (the)', + 'alpha2': 'PH', + 'alpha3': 'PHL', + 'numeric': '608', + 'altName': 'Philippines' + }, + { + 'name': 'Pitcairn', + 'alpha2': 'PN', + 'alpha3': 'PCN', + 'numeric': '612' + }, + { + 'name': 'Poland', + 'alpha2': 'PL', + 'alpha3': 'POL', + 'numeric': '616' + }, + { + 'name': 'Portugal', + 'alpha2': 'PT', + 'alpha3': 'PRT', + 'numeric': '620' + }, + { + 'name': 'Puerto Rico', + 'alpha2': 'PR', + 'alpha3': 'PRI', + 'numeric': '630' + }, + { + 'name': 'Qatar', + 'alpha2': 'QA', + 'alpha3': 'QAT', + 'numeric': '634' + }, + { + 'name': 'Réunion', + 'alpha2': 'RE', + 'alpha3': 'REU', + 'numeric': '638', + 'shortName': 'Reunion' + }, + { + 'name': 'Romania', + 'alpha2': 'RO', + 'alpha3': 'ROU', + 'numeric': '642' + }, + { + 'name': 'Russian Federation (the)', + 'alpha2': 'RU', + 'alpha3': 'RUS', + 'numeric': '643', + 'altName': 'Russian Federation', + 'shortName': 'Russia' + }, + { + 'name': 'Rwanda', + 'alpha2': 'RW', + 'alpha3': 'RWA', + 'numeric': '646' + }, + { + 'name': 'Saint Barthélemy', + 'alpha2': 'BL', + 'alpha3': 'BLM', + 'numeric': '652', + 'altName': 'Saint-Barthélemy', + 'shortName': 'Saint Barthelemy' + }, + { + 'name': 'Saint Helena, Ascension and Tristan da Cunha', + 'alpha2': 'SH', + 'alpha3': 'SHN', + 'numeric': '654', + 'altName': 'Saint Helena' + }, + { + 'name': 'Saint Kitts and Nevis', + 'alpha2': 'KN', + 'alpha3': 'KNA', + 'numeric': '659' + }, + { + 'name': 'Saint Lucia', + 'alpha2': 'LC', + 'alpha3': 'LCA', + 'numeric': '662' + }, + { + 'name': 'Saint Martin (French part)', + 'alpha2': 'MF', + 'alpha3': 'MAF', + 'numeric': '663', + 'altName': 'Saint-Martin (French part)', + 'shortName': 'Saint Martin' + }, + { + 'name': 'Saint Pierre and Miquelon', + 'alpha2': 'PM', + 'alpha3': 'SPM', + 'numeric': '666' + }, + { + 'name': 'Saint Vincent and the Grenadines', + 'alpha2': 'VC', + 'alpha3': 'VCT', + 'numeric': '670', + 'altName': 'Saint Vincent and Grenadines' + }, + { + 'name': 'Samoa', + 'alpha2': 'WS', + 'alpha3': 'WSM', + 'numeric': '882' + }, + { + 'name': 'San Marino', + 'alpha2': 'SM', + 'alpha3': 'SMR', + 'numeric': '674' + }, + { + 'name': 'Sao Tome and Principe', + 'alpha2': 'ST', + 'alpha3': 'STP', + 'numeric': '678' + }, + { + 'name': 'Saudi Arabia', + 'alpha2': 'SA', + 'alpha3': 'SAU', + 'numeric': '682' + }, + { + 'name': 'Senegal', + 'alpha2': 'SN', + 'alpha3': 'SEN', + 'numeric': '686' + }, + { + 'name': 'Serbia', + 'alpha2': 'RS', + 'alpha3': 'SRB', + 'numeric': '688' + }, + { + 'name': 'Seychelles', + 'alpha2': 'SC', + 'alpha3': 'SYC', + 'numeric': '690' + }, + { + 'name': 'Sierra Leone', + 'alpha2': 'SL', + 'alpha3': 'SLE', + 'numeric': '694' + }, + { + 'name': 'Singapore', + 'alpha2': 'SG', + 'alpha3': 'SGP', + 'numeric': '702' + }, + { + 'name': 'Sint Maarten (Dutch part)', + 'alpha2': 'SX', + 'alpha3': 'SXM', + 'numeric': '534', + 'shortName': 'Sint Maarten' + }, + { + 'name': 'Slovakia', + 'alpha2': 'SK', + 'alpha3': 'SVK', + 'numeric': '703' + }, + { + 'name': 'Slovenia', + 'alpha2': 'SI', + 'alpha3': 'SVN', + 'numeric': '705' + }, + { + 'name': 'Solomon Islands', + 'alpha2': 'SB', + 'alpha3': 'SLB', + 'numeric': '090' + }, + { + 'name': 'Somalia', + 'alpha2': 'SO', + 'alpha3': 'SOM', + 'numeric': '706' + }, + { + 'name': 'South Africa', + 'alpha2': 'ZA', + 'alpha3': 'ZAF', + 'numeric': '710' + }, + { + 'name': 'South Georgia and the South Sandwich Islands', + 'alpha2': 'GS', + 'alpha3': 'SGS', + 'numeric': '239' + }, + { + 'name': 'South Sudan', + 'alpha2': 'SS', + 'alpha3': 'SSD', + 'numeric': '728' + }, + { + 'name': 'Spain', + 'alpha2': 'ES', + 'alpha3': 'ESP', + 'numeric': '724' + }, + { + 'name': 'Sri Lanka', + 'alpha2': 'LK', + 'alpha3': 'LKA', + 'numeric': '144' + }, + { + 'name': 'Sudan (the)', + 'alpha2': 'SD', + 'alpha3': 'SDN', + 'numeric': '729', + 'altName': 'Sudan' + }, + { + 'name': 'Suriname', + 'alpha2': 'SR', + 'alpha3': 'SUR', + 'numeric': '740' + }, + { + 'name': 'Svalbard and Jan Mayen', + 'alpha2': 'SJ', + 'alpha3': 'SJM', + 'numeric': '744', + 'altName': 'Svalbard and Jan Mayen Islands' + }, + { + 'name': 'Swaziland', + 'alpha2': 'SZ', + 'alpha3': 'SWZ', + 'numeric': '748' + }, + { + 'name': 'Sweden', + 'alpha2': 'SE', + 'alpha3': 'SWE', + 'numeric': '752' + }, + { + 'name': 'Switzerland', + 'alpha2': 'CH', + 'alpha3': 'CHE', + 'numeric': '756' + }, + { + 'name': 'Syrian Arab Republic', + 'alpha2': 'SY', + 'alpha3': 'SYR', + 'numeric': '760', + 'altName': 'Syrian Arab Republic (Syria)', + 'shortName': 'Syria' + }, + { + 'name': 'Taiwan (Province of China)', + 'alpha2': 'TW', + 'alpha3': 'TWN', + 'numeric': '158', + 'altName': 'Taiwan, Republic of China', + 'shortName': 'Taiwan' + }, + { + 'name': 'Tajikistan', + 'alpha2': 'TJ', + 'alpha3': 'TJK', + 'numeric': '762' + }, + { + 'name': 'Tanzania, United Republic of', + 'alpha2': 'TZ', + 'alpha3': 'TZA', + 'numeric': '834', + 'shortName': 'Tanzania' + }, + { + 'name': 'Thailand', + 'alpha2': 'TH', + 'alpha3': 'THA', + 'numeric': '764' + }, + { + 'name': 'Timor-Leste', + 'alpha2': 'TL', + 'alpha3': 'TLS', + 'numeric': '626', + 'shortName': 'East Timor' + }, + { + 'name': 'Togo', + 'alpha2': 'TG', + 'alpha3': 'TGO', + 'numeric': '768' + }, + { + 'name': 'Tokelau', + 'alpha2': 'TK', + 'alpha3': 'TKL', + 'numeric': '772' + }, + { + 'name': 'Tonga', + 'alpha2': 'TO', + 'alpha3': 'TON', + 'numeric': '776' + }, + { + 'name': 'Trinidad and Tobago', + 'alpha2': 'TT', + 'alpha3': 'TTO', + 'numeric': '780' + }, + { + 'name': 'Tunisia', + 'alpha2': 'TN', + 'alpha3': 'TUN', + 'numeric': '788' + }, + { + 'name': 'Turkey', + 'alpha2': 'TR', + 'alpha3': 'TUR', + 'numeric': '792' + }, + { + 'name': 'Turkmenistan', + 'alpha2': 'TM', + 'alpha3': 'TKM', + 'numeric': '795' + }, + { + 'name': 'Turks and Caicos Islands (the)', + 'alpha2': 'TC', + 'alpha3': 'TCA', + 'numeric': '796', + 'altName': 'Turks and Caicos Islands' + }, + { + 'name': 'Tuvalu', + 'alpha2': 'TV', + 'alpha3': 'TUV', + 'numeric': '798' + }, + { + 'name': 'Uganda', + 'alpha2': 'UG', + 'alpha3': 'UGA', + 'numeric': '800' + }, + { + 'name': 'Ukraine', + 'alpha2': 'UA', + 'alpha3': 'UKR', + 'numeric': '804' + }, + { + 'name': 'United Arab Emirates (the)', + 'alpha2': 'AE', + 'alpha3': 'ARE', + 'numeric': '784', + 'altName': 'United Arab Emirates' + }, + { + 'name': 'United Kingdom of Great Britain and Northern Ireland (the)', + 'alpha2': 'GB', + 'alpha3': 'GBR', + 'numeric': '826', + 'altName': 'United Kingdom' + }, + { + 'name': 'United States Minor Outlying Islands (the)', + 'alpha2': 'UM', + 'alpha3': 'UMI', + 'numeric': '581', + 'altName': 'US Minor Outlying Islands' + }, + { + 'name': 'United States of America (the)', + 'alpha2': 'US', + 'alpha3': 'USA', + 'numeric': '840', + 'altName': 'United States of America', + 'shortName': 'United States' + }, + { + 'name': 'Uruguay', + 'alpha2': 'UY', + 'alpha3': 'URY', + 'numeric': '858' + }, + { + 'name': 'Uzbekistan', + 'alpha2': 'UZ', + 'alpha3': 'UZB', + 'numeric': '860' + }, + { + 'name': 'Vanuatu', + 'alpha2': 'VU', + 'alpha3': 'VUT', + 'numeric': '548' + }, + { + 'name': 'Venezuela (Bolivarian Republic of)', + 'alpha2': 'VE', + 'alpha3': 'VEN', + 'numeric': '862', + 'altName': 'Venezuela (Bolivarian Republic)', + 'shortName': 'Venezuela' + }, + { + 'name': 'Viet Nam', + 'alpha2': 'VN', + 'alpha3': 'VNM', + 'numeric': '704', + 'shortName': 'Vietnam' + }, + { + 'name': 'Virgin Islands (British)', + 'alpha2': 'VG', + 'alpha3': 'VGB', + 'numeric': '092', + 'altName': 'British Virgin Islands' + }, + { + 'name': 'Virgin Islands (U.S.)', + 'alpha2': 'VI', + 'alpha3': 'VIR', + 'numeric': '850', + 'altName': 'Virgin Islands, US', + 'shortName': 'U.S. Virgin Islands' + }, + { + 'name': 'Wallis and Futuna', + 'alpha2': 'WF', + 'alpha3': 'WLF', + 'numeric': '876', + 'altName': 'Wallis and Futuna Islands' + }, + { + 'name': 'Western Sahara*', + 'alpha2': 'EH', + 'alpha3': 'ESH', + 'numeric': '732', + 'altName': 'Western Sahara' + }, + { + 'name': 'Yemen', + 'alpha2': 'YE', + 'alpha3': 'YEM', + 'numeric': '887' + }, + { + 'name': 'Zambia', + 'alpha2': 'ZM', + 'alpha3': 'ZMB', + 'numeric': '894' + }, + { + 'name': 'Zimbabwe', + 'alpha2': 'ZW', + 'alpha3': 'ZWE', + 'numeric': '716' + } +]; diff --git a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts new file mode 100644 index 000000000..584108cf7 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts @@ -0,0 +1,811 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const customerOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'customer', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new customer', + }, + { + name: 'Get', + value: 'get', + description: 'Get a customer', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all customers', + }, + { + name: 'Properties', + value: 'properties', + description: 'Get customer property definitions', + }, + { + name: 'Update', + value: 'update', + description: 'Update a customer', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const customerFields = [ +/* -------------------------------------------------------------------------- */ +/* customer:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + description: 'By default the response only contain the ID to resource
. If this option gets activated it
will resolve the data automatically.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + options: [ + { + displayName: 'Age', + name: 'age', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: `Customer’s age`, + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `First name of the customer. When defined it must be between 1 and 40 characters.`, + }, + { + displayName: 'Gender', + name: 'gender', + type: 'options', + options: [ + { + name: 'Female', + value: 'female', + }, + { + name: 'Male', + value: 'male', + }, + { + name: 'Unknown', + value: 'unknown', + }, + ], + default: '', + description: 'Gender of this customer.', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + description: 'Job title. Max length 60 characters.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the customer', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the customer.', + }, + { + displayName: 'Notes', + name: 'background', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Notes`, + }, + { + displayName: 'Organization', + name: 'organization', + type: 'string', + default: '', + description: 'Organization', + }, + { + displayName: 'Photo Url', + name: 'photoUrl', + type: 'string', + default: '', + description: 'URL of the customer’s photo', + }, + ] + }, + { + displayName: 'Address', + name: 'addressUi', + placeholder: 'Add Address', + type: 'fixedCollection', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Address', + name: 'addressValue', + values: [ + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + description: 'line1', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + description: 'line2', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State', + }, + { + displayName: 'Country', + name: 'country', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountriesCodes', + }, + default: '', + description: 'Country', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + description: 'Postal code', + }, + ], + }, + ], + }, + { + displayName: 'Chat Handles', + name: 'chatsUi', + placeholder: 'Add Chat Handle', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Chat Handle', + name: 'chatsValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'AIM', + value: 'aim', + }, + { + name: 'Google Talk', + value: 'gtalk', + }, + { + name: 'ICQ', + value: 'icq', + }, + { + name: 'MSN', + value: 'msn', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'QQ', + value: 'qq', + }, + { + name: 'Skype', + value: 'skype', + }, + { + name: 'XMPP', + value: 'xmpp', + }, + { + name: 'Yahoo', + value: 'yahoo', + }, + ], + description: 'Chat type', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Chat handle', + }, + ], + }, + ], + }, + { + displayName: 'Emails', + name: 'emailsUi', + placeholder: 'Add Email', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Email', + name: 'emailsValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Home', + value: 'home', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Work', + value: 'work', + }, + ], + description: 'Location for this email address', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Email', + }, + ], + }, + ], + }, + { + displayName: 'Phones', + name: 'phonesUi', + placeholder: 'Add Phone', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Email', + name: 'phonesValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Home', + value: 'home', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Pager', + value: 'pager', + }, + { + name: 'Work', + value: 'work', + }, + ], + description: 'Location for this phone', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Phone', + }, + ], + }, + ], + }, + { + displayName: 'Social Profiles', + name: 'socialProfilesUi', + placeholder: 'Add Social Profile', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Social Profile', + name: 'socialProfilesValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'About Me', + value: 'aboutMe', + }, + { + name: 'Facebook', + value: 'facebook', + }, + { + name: 'Flickr', + value: 'flickr', + }, + { + name: 'Forsquare', + value: 'forsquare', + }, + { + name: 'Google', + value: 'google', + }, + { + name: 'Google Plus', + value: 'googleplus', + }, + { + name: 'Linkedin', + value: 'linkedin', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Quora', + value: 'quora', + }, + { + name: 'Tungleme', + value: 'tungleme', + }, + { + name: 'Twitter', + value: 'twitter', + }, + { + name: 'Youtube', + value: 'youtube', + }, + ], + description: 'Type of social profile', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Social Profile handle (url for example)', + }, + ], + }, + ], + }, + { + displayName: 'Websites', + name: 'websitesUi', + placeholder: 'Add Website', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Website', + name: 'websitesValues', + values: [ + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Website URL', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* customer:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'Filters customers by first name', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Filters customers by last name', + }, + { + displayName: 'Mailbox ID', + name: 'mailbox', + type: 'string', + default: '', + description: 'Filters customers from a specific mailbox', + }, + { + displayName: 'Modified Since', + name: 'modifiedSince', + type: 'dateTime', + default: '', + description: 'Returns only customers that were modified after this date', + }, + { + displayName: 'Sort Field', + name: 'sortField', + type: 'options', + options: [ + { + name: 'Score', + value: 'score', + }, + { + name: 'First Name', + value: 'firstName', + }, + { + name: 'Last Name', + value: 'lastName', + }, + { + name: 'Modified At', + value: 'modifiedAt', + }, + ], + default: 'score', + description: 'Sorts the result by specified field', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'asc', + }, + { + name: 'Desc', + value: 'desc', + }, + ], + default: 'desc', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Advanced search Examples' + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* customer:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Customer ID', + }, +/* -------------------------------------------------------------------------- */ +/* customer:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'customer', + ], + }, + }, + description: 'Customer ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'customer', + ], + }, + }, + options: [ + { + displayName: 'Age', + name: 'age', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: `Customer’s age`, + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `First name of the customer. When defined it must be between 1 and 40 characters.`, + }, + { + displayName: 'Gender', + name: 'gender', + type: 'options', + options: [ + { + name: 'Female', + value: 'female', + }, + { + name: 'Male', + value: 'male', + }, + { + name: 'Unknown', + value: 'unknown', + }, + ], + default: '', + description: 'Gender of this customer.', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + description: 'Job title. Max length 60 characters.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the customer', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the customer.', + }, + { + displayName: 'Notes', + name: 'background', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Notes`, + }, + { + displayName: 'Organization', + name: 'organization', + type: 'string', + default: '', + description: 'Organization', + }, + { + displayName: 'Photo Url', + name: 'photoUrl', + type: 'string', + default: '', + description: 'URL of the customer’s photo', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts b/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts new file mode 100644 index 000000000..fe50a7cb3 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts @@ -0,0 +1,20 @@ +import { IDataObject } from 'n8n-workflow'; + +export interface ICustomer { + address?: IDataObject; + age?: string; + background?: string; + chats?: IDataObject[]; + emails?: IDataObject[]; + firstName?: string; + gender?: string; + jobTitle?: string; + lastName?: string; + location?: string; + organization?: string; + phones?: IDataObject[]; + photoUrl?: string; + properties?: IDataObject; + socialProfiles?: IDataObject[]; + websites?: IDataObject[]; +} diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts new file mode 100644 index 000000000..fb04b669e --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -0,0 +1,70 @@ +import { OptionsWithUri } from 'request'; +import { + IHookFunctions, + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject, +} from 'n8n-workflow'; + +import { + get, +} from 'lodash'; + +export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.helpscout.net${resource}`, + json: true + }; + try { + if (Object.keys(option).length !== 0) { + options = Object.assign({}, options, option); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'helpScoutOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body + && error.response.body._embedded + && error.response.body._embedded.errors) { + // Try to return the error prettier + //@ts-ignore + throw new Error(`HelpScout error response [${error.statusCode}]: ${error.response.body.message} - ${error.response.body._embedded.errors.map(error => { + return `${error.path} ${error.message}`; + }).join('-')}`); + } + + throw new Error(`HelpScout error response [${error.statusCode}]: ${error.message}`); + } +} + +export async function helpscoutApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.size = 50; + let uri; + + do { + responseData = await helpscoutApiRequest.call(this, method, endpoint, body, query, uri); + uri = get(responseData, '_links.next.href'); + returnData.push.apply(returnData, get(responseData, propertyName)); + } while ( + responseData['_links'] !== undefined && + responseData['_links'].next !== undefined && + responseData['_links'].next.href !== undefined + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts new file mode 100644 index 000000000..190b276cb --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts @@ -0,0 +1,411 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeTypeDescription, + INodeType, +} from 'n8n-workflow'; + +import { + countriesCodes +} from './CountriesCodes'; + +import { + conversationFields, + conversationOperations, +} from './ConversationDescription'; + +import { + customerFields, + customerOperations, +} from './CustomerDescription'; + +import { + ICustomer, +} from './CustomerInterface'; + +import { + IConversation, +} from './ConversationInterface'; + +import { + helpscoutApiRequest, + helpscoutApiRequestAllItems, +} from './GenericFunctions'; + +import { + mailboxFields, + mailboxOperations, +} from './MailboxDescription'; + +import { + threadFields, + threadOperations, +} from './ThreadDescription'; + +import { + IAttachment, + IThread, +} from './ThreadInterface'; + + +export class HelpScout implements INodeType { + description: INodeTypeDescription = { + displayName: 'HelpScout', + name: 'helpScout', + icon: 'file:helpScout.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Help Scout API.', + defaults: { + name: 'HelpScout', + color: '#1392ee', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'helpScoutOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Conversation', + value: 'conversation', + }, + { + name: 'Customer', + value: 'customer', + }, + { + name: 'Mailbox', + value: 'mailbox', + }, + { + name: 'Thread', + value: 'thread', + }, + ], + default: 'conversation', + description: 'The resource to operate on.', + }, + ...conversationOperations, + ...conversationFields, + ...customerOperations, + ...customerFields, + ...mailboxOperations, + ...mailboxFields, + ...threadOperations, + ...threadFields, + ], + }; + + methods = { + loadOptions: { + // Get all the countries codes to display them to user so that he can + // select them easily + async getCountriesCodes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const countryCode of countriesCodes) { + const countryCodeName = `${countryCode.name} - ${countryCode.alpha2}`; + const countryCodeId = countryCode.alpha2; + returnData.push({ + name: countryCodeName, + value: countryCodeId, + }); + } + return returnData; + }, + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await helpscoutApiRequestAllItems.call(this, '_embedded.tags', 'GET', '/v2/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.id; + returnData.push({ + name: tagName, + value: tagId, + }); + } + return returnData; + }, + // Get all the mailboxes to display them to user so that he can + // select them easily + async getMailboxes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const mailboxes = await helpscoutApiRequestAllItems.call(this, '_embedded.mailboxes', 'GET', '/v2/mailboxes'); + for (const mailbox of mailboxes) { + const mailboxName = mailbox.name; + const mailboxId = mailbox.id; + returnData.push({ + name: mailboxName, + value: mailboxId, + }); + } + 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 === 'conversation') { + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/create + if (operation === 'create') { + const mailboxId = this.getNodeParameter('mailboxId', i) as number; + const status = this.getNodeParameter('status', i) as string; + const subject = this.getNodeParameter('subject', i) as string; + const type = this.getNodeParameter('type', i) as string; + const resolveData = this.getNodeParameter('resolveData', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const threads = (this.getNodeParameter('threadsUi', i) as IDataObject).threadsValues as IDataObject[]; + const body: IConversation = { + mailboxId, + status, + subject, + type, + }; + Object.assign(body, additionalFields); + if (additionalFields.customerId) { + body.customer = { + id: additionalFields.customerId, + }; + //@ts-ignore + delete body.customerId; + } + if (additionalFields.customerEmail) { + body.customer = { + email: additionalFields.customerEmail, + }; + //@ts-ignore + delete body.customerEmail; + } + if (body.customer === undefined) { + throw new Error('Either customer email or customer ID must be set'); + } + if (threads) { + for (let i = 0; i < threads.length; i++) { + if (threads[i].type === '' || threads[i].text === '') { + throw new Error('Chat Threads cannot be empty'); + } + if (threads[i].type !== 'note') { + threads[i].customer = body.customer; + } + } + body.threads = threads; + } + responseData = await helpscoutApiRequest.call(this, 'POST', '/v2/conversations', body, qs, undefined, { resolveWithFullResponse: true }); + const id = responseData.headers['resource-id']; + const uri = responseData.headers.location; + if (resolveData) { + responseData = await helpscoutApiRequest.call(this, 'GET', '', {}, {}, uri); + } else { + responseData = { + id, + uri, + }; + } + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/delete + if (operation === 'delete') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'DELETE', `/v2/conversations/${conversationId}`); + responseData = { success: true }; + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/get + if (operation === 'get') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'GET', `/v2/conversations/${conversationId}`); + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/list + if (operation === 'getAll') { + const options = this.getNodeParameter('options', i) as IDataObject; + Object.assign(qs, options); + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.conversations', 'GET', '/v2/conversations', {}, qs); + } + } + if (resource === 'customer') { + //https://developer.helpscout.com/mailbox-api/endpoints/customers/create + if (operation === 'create') { + const resolveData = this.getNodeParameter('resolveData', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const chats = (this.getNodeParameter('chatsUi', i) as IDataObject).chatsValues as IDataObject[]; + const address = (this.getNodeParameter('addressUi', i) as IDataObject).addressValue as IDataObject; + const emails = (this.getNodeParameter('emailsUi', i) as IDataObject).emailsValues as IDataObject[]; + const phones = (this.getNodeParameter('phonesUi', i) as IDataObject).phonesValues as IDataObject[]; + const socialProfiles = (this.getNodeParameter('socialProfilesUi', i) as IDataObject).socialProfilesValues as IDataObject[]; + const websites = (this.getNodeParameter('websitesUi', i) as IDataObject).websitesValues as IDataObject[]; + let body: ICustomer = {}; + body = Object.assign({}, additionalFields); + if (body.age) { + body.age = body.age.toString(); + } + if (chats) { + body.chats = chats; + } + if (address) { + body.address = address; + body.address.lines = [address.line1, address.line2]; + } + if (emails) { + body.emails = emails; + } + if (phones) { + body.phones = phones; + } + if (socialProfiles) { + body.socialProfiles = socialProfiles; + } + if (websites) { + body.websites = websites; + } + if (Object.keys(body).length === 0) { + throw new Error('You have to set at least one field'); + } + responseData = await helpscoutApiRequest.call(this, 'POST', '/v2/customers', body, qs, undefined, { resolveWithFullResponse: true }); + const id = responseData.headers['resource-id']; + const uri = responseData.headers.location; + if (resolveData) { + responseData = await helpscoutApiRequest.call(this, 'GET', '', {}, {}, uri); + } else { + responseData = { + id, + uri, + }; + } + } + //https://developer.helpscout.com/mailbox-api/endpoints/customer_properties/list + if (operation === 'properties') { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.customer-properties', 'GET', '/v2/customer-properties', {}, qs); + } + //https://developer.helpscout.com/mailbox-api/endpoints/customers/get + if (operation === 'get') { + const customerId = this.getNodeParameter('customerId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'GET', `/v2/customers/${customerId}`); + } + //https://developer.helpscout.com/mailbox-api/endpoints/customers/list + if (operation === 'getAll') { + const options = this.getNodeParameter('options', i) as IDataObject; + Object.assign(qs, options); + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.customers', 'GET', '/v2/customers', {}, qs); + } + //https://developer.helpscout.com/mailbox-api/endpoints/customers/overwrite/ + if (operation === 'update') { + const customerId = this.getNodeParameter('customerId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + let body: ICustomer = {}; + body = Object.assign({}, updateFields); + if (body.age) { + body.age = body.age.toString(); + } + if (Object.keys(body).length === 0) { + throw new Error('You have to set at least one field'); + } + responseData = await helpscoutApiRequest.call(this, 'PUT', `/v2/customers/${customerId}`, body, qs, undefined, { resolveWithFullResponse: true }); + responseData = { success: true }; + } + } + if (resource === 'mailbox') { + //https://developer.helpscout.com/mailbox-api/endpoints/mailboxes/get + if (operation === 'get') { + const mailboxId = this.getNodeParameter('mailboxId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'GET', `/v2/mailboxes/${mailboxId}`, {}, qs); + } + //https://developer.helpscout.com/mailbox-api/endpoints/mailboxes/list + if (operation === 'getAll') { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.mailboxes', 'GET', '/v2/mailboxes', {}, qs); + } + } + if (resource === 'thread') { + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/chat + if (operation === 'create') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + const type = this.getNodeParameter('type', i) as string; + const text = this.getNodeParameter('text', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const attachments = this.getNodeParameter('attachmentsUi', i) as IDataObject; + const body: IThread = { + text, + attachments: [], + }; + Object.assign(body, additionalFields); + if (additionalFields.customerId) { + body.customer = { + id: additionalFields.customerId, + }; + //@ts-ignore + delete body.customerId; + } + if (additionalFields.customerEmail) { + body.customer = { + email: additionalFields.customerEmail, + }; + //@ts-ignore + delete body.customerEmail; + } + if (body.customer === undefined) { + throw new Error('Either customer email or customer ID must be set'); + } + if (attachments) { + if (attachments.attachmentsValues + && (attachments.attachmentsValues as IDataObject[]).length !== 0) { + body.attachments?.push.apply(body.attachments, attachments.attachmentsValues as IAttachment[]); + } + if (attachments.attachmentsBinary + && (attachments.attachmentsBinary as IDataObject[]).length !== 0 + && items[i].binary) { + const mapFunction = (value: IDataObject): IAttachment => { + const binaryProperty = (items[i].binary as IBinaryKeyData)[value.property as string]; + if (binaryProperty) { + return { + fileName: binaryProperty.fileName || 'unknown', + data: binaryProperty.data, + mimeType: binaryProperty.mimeType, + }; + } else { + throw new Error(`Binary property ${value.property} does not exist on input`); + } + }; + body.attachments?.push.apply(body.attachments, (attachments.attachmentsBinary as IDataObject[]).map(mapFunction) as IAttachment[]); + } + } + responseData = await helpscoutApiRequest.call(this, 'POST', `/v2/conversations/${conversationId}/chats`, body); + responseData = { success: true }; + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/list + if (operation === 'getAll') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.threads', 'GET', `/v2/conversations/${conversationId}/threads`); + } + } + 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/HelpScout/HelpScoutTrigger.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts new file mode 100644 index 000000000..63592aa14 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts @@ -0,0 +1,203 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + helpscoutApiRequest, + helpscoutApiRequestAllItems, +} from './GenericFunctions'; + +import { createHmac } from 'crypto'; + +export class HelpScoutTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'HelpScout Trigger', + name: 'helpScoutTrigger', + icon: 'file:helpScout.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when HelpScout events occure.', + defaults: { + name: 'HelpScout Trigger', + color: '#1392ee', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'helpScoutOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + + { + name: 'Conversation - Assigned', + value: 'convo.assigned', + }, + { + name: 'Conversation - Created', + value: 'convo.created', + }, + { + name: 'Conversation - Deleted', + value: 'convo.deleted', + }, + { + name: 'Conversation - Merged', + value: 'convo.merged', + }, + { + name: 'Conversation - Moved', + value: 'convo.moved', + }, + { + name: 'Conversation - Status', + value: 'convo.status', + }, + { + name: 'Conversation - Tags', + value: 'convo.tags', + }, + { + name: 'Conversation Agent Reply - Created', + value: 'convo.agent.reply.created', + }, + { + name: 'Conversation Customer Reply - Created', + value: 'convo.customer.reply.created', + }, + { + name: 'Conversation Note - Created', + value: 'convo.note.created', + }, + { + name: 'Customer - Created', + value: 'customer.created', + }, + { + name: 'Rating - Received', + value: 'satisfaction.ratings', + }, + ], + default: [], + required: true, + }, + ], + + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string; + + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/v2/webhooks'; + const data = await helpscoutApiRequestAllItems.call(this, '_embedded.webhooks', 'GET', endpoint, {}); + + for (const webhook of data) { + if (webhook.url === webhookUrl) { + for (const event of events) { + if (!webhook.events.includes(event) + && webhook.state === 'enabled') { + return false; + } + } + } + // Set webhook-id to be sure that it can be deleted + webhookData.webhookId = webhook.id as string; + return true; + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events') as string; + + const endpoint = '/v2/webhooks'; + + const body = { + url: webhookUrl, + events, + secret: Math.random().toString(36).substring(2, 15), + }; + + const responseData = await helpscoutApiRequest.call(this, 'POST', endpoint, body, {}, undefined, { resolveWithFullResponse: true }); + + if (responseData.headers['resource-id'] === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.headers['resource-id'] as string; + webhookData.secret = body.secret; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/v2/webhooks/${webhookData.webhookId}`; + try { + await helpscoutApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + delete webhookData.secret; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const bodyData = this.getBodyData(); + const headerData = this.getHeaderData() as IDataObject; + const webhookData = this.getWorkflowStaticData('node'); + if (headerData['x-helpscout-signature'] === undefined) { + return {}; + } + //@ts-ignore + const computedSignature = createHmac('sha1', webhookData.secret as string).update(req.rawBody).digest('base64'); + if (headerData['x-helpscout-signature'] !== computedSignature) { + return {}; + } + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts new file mode 100644 index 000000000..27063c05e --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts @@ -0,0 +1,54 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const mailboxOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'mailbox', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a mailbox', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all mailboxes', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const mailboxFields = [ + +/* -------------------------------------------------------------------------- */ +/* mailbox:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Mailbox ID', + name: 'mailboxId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'mailbox', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts new file mode 100644 index 000000000..4374e6845 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts @@ -0,0 +1,257 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const threadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'thread', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new chat thread', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all chat threads', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const threadFields = [ +/* -------------------------------------------------------------------------- */ +/* thread:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + description: 'conversation ID', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'Chat', + value: 'chat' + }, + { + name: 'Customer', + value: 'customer' + }, + { + name: 'Note', + value: 'note' + }, + { + name: 'Phone', + value: 'phone' + }, + { + name: 'Reply', + value: 'reply' + }, + ], + default: '', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The chat text', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + options: [ + { + displayName: 'Created At', + name: 'createdAt', + type: 'dateTime', + default: '', + }, + { + displayName: 'Customer Email', + name: 'customerEmail', + type: 'string', + default: '', + }, + { + displayName: 'Customer ID', + name: 'customerId', + type: 'number', + default: 0, + }, + { + displayName: 'Draft', + name: 'draft', + type: 'boolean', + default: false, + displayOptions: { + show: { + '/type': [ + 'note', + ], + }, + }, + description: 'If set to true, a draft reply is created', + }, + { + displayName: 'Imported', + name: 'imported', + type: 'boolean', + default: false, + description: 'When imported is set to true, no outgoing emails or notifications will be generated.', + }, + ] + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + options: [ + { + name: 'attachmentsValues', + displayName: 'Attachments Values', + values: [ + { + displayName: 'FileName', + name: 'fileName', + type: 'string', + default: '', + description: 'Attachment’s file name', + }, + { + displayName: 'Mime Type', + name: 'mimeType', + type: 'string', + default: '', + description: 'Attachment’s mime type', + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + default: '', + placeholder: 'ZXhhbXBsZSBmaWxl', + description: 'Base64-encoded stream of data.', + }, + ], + }, + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'string', + default: 'data', + description: 'Name of the binary properties which contain data which should be added to email as attachment', + }, + ], + }, + ], + default: '', + description: 'Array of supported attachments to add to the message.', + }, +/* -------------------------------------------------------------------------- */ +/* thread:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'conversation ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts b/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts new file mode 100644 index 000000000..e47a0a674 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts @@ -0,0 +1,15 @@ +import { IDataObject } from 'n8n-workflow'; + +export interface IAttachment { + fileName?: string; + mimeType?: string; + data?: string; +} + +export interface IThread { + createdAt?: string; + customer?: IDataObject; + imported?: boolean; + text?: string; + attachments?: IAttachment[]; +} diff --git a/packages/nodes-base/nodes/HelpScout/helpScout.png b/packages/nodes-base/nodes/HelpScout/helpScout.png new file mode 100644 index 000000000..1ebdaa8e5 Binary files /dev/null and b/packages/nodes-base/nodes/HelpScout/helpScout.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 25f148aa7..67490dd1c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -44,6 +44,7 @@ "dist/credentials/GitlabApi.credentials.js", "dist/credentials/GoogleApi.credentials.js", "dist/credentials/GoogleOAuth2Api.credentials.js", + "dist/credentials/HelpScoutOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -124,6 +125,8 @@ "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", + "dist/nodes/HelpScout/HelpScout.node.js", + "dist/nodes/HelpScout/HelpScoutTrigger.node.js", "dist/nodes/HtmlExtract/HtmlExtract.node.js", "dist/nodes/HttpRequest.node.js", "dist/nodes/Hubspot/Hubspot.node.js", @@ -195,8 +198,8 @@ "@types/gm": "^1.18.2", "@types/imap-simple": "^4.2.0", "@types/jest": "^24.0.18", - "@types/lodash.set": "^4.3.6", - "@types/moment-timezone": "^0.5.12", + "@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", @@ -222,8 +225,8 @@ "imap-simple": "^4.3.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", - "moment-timezone": "0.5.28", + "lodash.unset": "^4.5.2", + "moment-timezone": "0.5.28", "mongodb": "^3.3.2", "mysql2": "^2.0.1", "n8n-core": "~0.20.0",