diff --git a/packages/nodes-base/credentials/MailchimpApi.credentials.ts b/packages/nodes-base/credentials/MailchimpApi.credentials.ts new file mode 100644 index 0000000000..c6a6462bc1 --- /dev/null +++ b/packages/nodes-base/credentials/MailchimpApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MailchimpApi implements ICredentialType { + name = 'mailchimpApi'; + displayName = 'Mailchimp API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts new file mode 100644 index 0000000000..6f0aeb0e8c --- /dev/null +++ b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts @@ -0,0 +1,58 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions +} from 'n8n-core'; + +export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, headers?: object): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('mailchimpApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const headerWithAuthentication = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` }); + + if (!(credentials.apiKey as string).includes('-')) { + throw new Error('The API key is not valid!'); + } + + const datacenter = (credentials.apiKey as string).split('-').pop(); + + const host = 'api.mailchimp.com/3.0'; + + const options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + uri: `https://${datacenter}.${host}${endpoint}`, + json: true, + }; + + if (Object.keys(body).length !== 0) { + options.body = body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = ''; + } + return result; +} diff --git a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts new file mode 100644 index 0000000000..187512ff29 --- /dev/null +++ b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts @@ -0,0 +1,558 @@ +import * as moment from 'moment'; + +import { + IExecuteSingleFunctions, +} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeTypeDescription, + INodeExecutionData, + INodeType, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + mailchimpApiRequest, + validateJSON, +} from './GenericFunctions'; + +enum Status { + subscribe = 'subscribe', + unsubscribed = 'unsubscribe', + cleaned = 'cleaned', + pending = 'pending', + transactional = 'transactional', +} + +interface ILocation { + latitude?: number; + longitude?: number; +} + +interface ICreateMemberBody { + listId: string; + email_address: string; + email_type?: string; + status?: Status; + language?: string; + vip?: boolean; + location?: ILocation; + ips_signup?: string; + timestamp_signup?: string; + ip_opt?: string; + timestamp_opt?: string; + tags?: string[]; + merge_fields?: IDataObject; +} + +export class Mailchimp implements INodeType { + + description: INodeTypeDescription = { + displayName: 'Mailchimp', + name: 'mailchimp', + icon: 'file:mailchimp.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Mailchimp API', + defaults: { + name: 'Mailchimp', + color: '#c02428', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'mailchimpApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Member', + value: 'member', + description: 'Add member to list', + }, + ], + default: 'member', + required: true, + description: 'Resource to consume.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new member on list', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + { + displayName: 'List', + name: 'list', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + options: [], + required: true, + description: 'List of lists', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Email address for a subscriber.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'Subscribed', + value: 'subscribed', + description: '', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + description: '', + }, + { + name: 'Cleaned', + value: 'cleaned', + description: '', + }, + { + name: 'Pending', + value: 'pending', + description: '', + }, + { + name: 'Transactional', + value: 'transactional', + description: '', + }, + ], + default: '', + description: `Subscriber's current status.`, + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource:[ + 'member' + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Email Type', + name: 'emailType', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + description: '', + }, + { + name: 'Text', + value: 'text', + description: '', + }, + ], + default: '', + description: 'Type of email this member asked to get', + }, + { + displayName: 'Signup IP', + name: 'ipSignup', + type: 'string', + default: '', + description: 'IP address the subscriber signed up from.', + }, + { + displayName: 'Opt-in IP', + name: 'ipOptIn', + type: 'string', + default: '', + description: 'The IP address the subscriber used to confirm their opt-in status.', + }, + { + displayName: 'Signup Timestamp', + name: 'timestampSignup', + type: 'dateTime', + default: '', + description: 'The date and time the subscriber signed up for the list in ISO 8601 format.', + }, + { + displayName: 'Language', + name: 'language', + type: 'string', + default: '', + description: `If set/detected, the subscriber's language.`, + }, + { + displayName: 'Vip', + name: 'vip', + type: 'boolean', + default: false, + description: `Vip status for subscribers`, + }, + { + displayName: 'Opt-in Timestamp', + name: 'timestampOpt', + type: 'dateTime', + default: '', + description: `The date and time the subscribe confirmed their opt-in status in ISO 8601 format.`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: `The tags that are associated with a member separeted by ,.`, + }, + ] + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: `Subscriber location information.n`, + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude.', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude.', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Merge Fields', + name: 'mergeFieldsUi', + placeholder: 'Add Merge Fields', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource:[ + 'member' + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + description: 'An individual merge var and value for a member.', + options: [ + { + name: 'mergeFieldsValues', + displayName: 'Field', + typeOptions: { + multipleValueButtonText: 'Add Field', + }, + values: [ + { + displayName: 'Field Name', + name: 'name', + type: 'string', + required: true, + description: 'Merge Field name', + default: '', + }, + { + displayName: 'Field Value', + name: 'value', + required: true, + type: 'string', + default: '', + description: 'Merge field value.', + }, + ], + }, + ], + }, + { + displayName: 'Merge Fields', + name: 'mergeFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: '', + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + }, + { + displayName: 'Location', + name: 'locationJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: '', + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + }, + ] + }; + + + methods = { + loadOptions: { + + // Get all the available lists to display them to user so that he can + // select them easily + async getLists(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let lists, response; + try { + response = await mailchimpApiRequest.call(this, '/lists', 'GET'); + lists = response.lists; + } catch (err) { + throw new Error(`Mailchimp Error: ${err}`); + } + for (const list of lists) { + const listName = list.name; + const listId = list.id; + + returnData.push({ + name: listName, + value: listId, + }); + } + return returnData; + }, + } + }; + + async executeSingle(this: IExecuteSingleFunctions): Promise { + let response = {}; + const resource = this.getNodeParameter('resource') as string; + const operation = this.getNodeParameter('operation') as string; + + if (resource === 'member') { + if (operation === 'create') { + + const listId = this.getNodeParameter('list') as string; + const email = this.getNodeParameter('email') as string; + const status = this.getNodeParameter('status') as Status; + const options = this.getNodeParameter('options') as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters') as IDataObject; + + const body: ICreateMemberBody = { + listId, + email_address: email, + status + }; + if (options.emailType) { + body.email_type = options.emailType as string; + } + if (options.languaje) { + body.language = options.language as string; + } + if (options.vip) { + body.vip = options.vip as boolean; + } + if (options.ipSignup) { + body.ips_signup = options.ipSignup as string; + } + if (options.ipOptIn) { + body.ip_opt = options.ipOptIn as string; + } + if (options.timestampOpt) { + body.timestamp_opt = moment(options.timestampOpt as string).format('YYYY-MM-DD HH:MM:SS') as string; + } + if (options.timestampSignup) { + body.timestamp_signup = moment(options.timestampSignup as string).format('YYYY-MM-DD HH:MM:SS') as string; + } + if (options.tags) { + // @ts-ignore + body.tags = options.tags.split(',') as string[]; + } + if (!jsonActive) { + const locationValues = (this.getNodeParameter('locationFieldsUi') as IDataObject).locationFieldsValues as IDataObject; + if (locationValues) { + const location: ILocation = {}; + for (const key of Object.keys(locationValues)) { + if (key === 'latitude') { + location.latitude = parseInt(locationValues[key] as string, 10) as number; + } else if (key === 'longitude') { + location.longitude = parseInt(locationValues[key] as string, 10) as number; + } + } + body.location = location; + } + const mergeFieldsValues = (this.getNodeParameter('mergeFieldsUi') as IDataObject).mergeFieldsValues as IDataObject[]; + if (mergeFieldsValues) { + const mergeFields = {}; + for (let i = 0; i < mergeFieldsValues.length; i++) { + // @ts-ignore + mergeFields[mergeFieldsValues[i].name] = mergeFieldsValues[i].value; + } + body.merge_fields = mergeFields; + } + } else { + const locationJson = validateJSON(this.getNodeParameter('locationJson') as string); + const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson') as string); + if (locationJson) { + body.location = locationJson; + } + if (mergeFieldsJson) { + body.merge_fields = mergeFieldsJson; + } + } + try { + response = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body); + } catch (err) { + throw new Error(`Mailchimp Error: ${JSON.stringify(err)}`); + } + } + } + return { + json: response, + }; + } +} diff --git a/packages/nodes-base/nodes/Mailchimp/mailchimp.png b/packages/nodes-base/nodes/Mailchimp/mailchimp.png new file mode 100644 index 0000000000..a4998396c3 Binary files /dev/null and b/packages/nodes-base/nodes/Mailchimp/mailchimp.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1e3e1ceeae..8b87d9e255 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,6 +42,7 @@ "dist/credentials/HttpHeaderAuth.credentials.js", "dist/credentials/Imap.credentials.js", "dist/credentials/LinkFishApi.credentials.js", + "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailgunApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", "dist/credentials/MattermostApi.credentials.js", @@ -97,6 +98,7 @@ "dist/nodes/If.node.js", "dist/nodes/Interval.node.js", "dist/nodes/LinkFish/LinkFish.node.js", + "dist/nodes/Mailchimp/Mailchimp.node.js", "dist/nodes/Mailgun/Mailgun.node.js", "dist/nodes/Mandrill/Mandrill.node.js", "dist/nodes/Mattermost/Mattermost.node.js",