UNPKG

mailchimp-app

Version:

A Mailchimp express sub-app to mount on any project, for easy integration

614 lines (426 loc) 15.9 kB
var request = require('request'); var Promise = require('bluebird'); var fs = require('fs'); var _ = require('lodash'); var MailchimpLogger = require('./mc-logger'); var term = require('terminal-kit').terminal // require('request-debug')(request); Promise.promisifyAll( fs ); // Interface to interact with the Mailchimp api 3.0 // http://developer.mailchimp.com // Constructor base function, instantiate one per list var Mailchimp = function( opts ){ this.api_version = this.api_version || '3.0'; this.api_key = opts.api_key; this.dc = opts.dc; this.list_id = opts.list_id this.interests = opts.interests; this.log = opts.logger || new MailchimpLogger(); // Generate HTTP helper functions to make api calls this._generateHTTPHelper({ method: 'get' }); this._generateHTTPHelper({ method: 'post', json_body: true }); this._generateHTTPHelper({ method: 'patch', json_body: true }); this._generateHTTPHelper({ method: 'delete' }); // Generate the full suite of REST operations for the 'members' resource this._generateRestInterface({ // For information purposes only _mandatory_fields : [ "email_address", "status" ], resource : 'member', read_only_fields : [ "email_address" ], get_default_fields : [ "id", "email_address", "status", "merge_fields", "language", "last_changed", "location" ], write_default_fields : [ "id", "email_address", "merge_fields", "interests" ], getRes: function( res ){ return res.members; }, formatFields: function( fields ){ return _.map( fields, function( field ){ return 'members.' + field; }); }, setCreateDefaults: function( data ){ data.status = "subscribed"; } }); // Generate the full suite of REST operations for the 'merge-fields' resource this._generateRestInterface({ // For information purposes only _mandatory_fields : [ "tag", "type" ], resource : 'merge-field', get_default_fields : [ "merge_id", "type", "name", "default_value" ], write_default_fields : [ "merge_id","type", "name", "options" ], getRes: function( res ){ return res.merge_fields; }, formatFields: function( fields ){ return _.map( fields, function( field ){ return 'merge_fields.' + field; }); }, }); // Generate the full suite of REST operations for the 'interest-category' resource this._generateRestInterface({ // For information purposes only _mandatory_fields : [ "title", "type" ], resource : 'interest-categorie', methodName : 'InterestCat', get_default_fields : [ "id", "type", "title", "list_id" ], write_default_fields : [ "id", "type", "title", "list_id" ], getRes: function( res ){ return res.categories; }, formatFields: function( fields ){ return _.map( fields, function( field ){ return 'categories.' + field; }); }, setCreateDefaults : function( data ){ data.type = "hidden"; } }); // Turn every callback-version of each function into a promise-based version this._promisifyAll(); // Specific version because code doesnt work for subresources this._promisify( "getInterestsAsync" ); } // Turn each specific api call into a promisified version Mailchimp.prototype._promisifyAll = function(){ var self = this; [ "Member", "MergeField", "InterestCat" ].forEach(function( methodName ){ self._promisify( "get" + methodName + "Async" ); self._promisify( "get" + methodName + "sAsync" ); self._promisify( "create" + methodName + "Async" ); self._promisify( "update" + methodName + "Async" ); self._promisify( "delete" + methodName + "Async" ); }); } // Turn any function into a promise, provided that this function // accept a callback as its last parameter Mailchimp.prototype._promisify = function( method ){ var self = this; var methodName; self[ method.replace("Async", "") ] = function(){ var args = arguments; return new Promise(function( resolve, reject ){ [].push.call( args, function( err, res ){ if( err ){ reject( err ); } else { resolve( res ); } }); self[ method ].apply( self, args ); }); } } // Getter on the interest_id when given the name of the list Mailchimp.prototype.getInterestId = function( interest_name ){ var self = this; var id = _.find( self.interests, function( int ){ return int.name == interest_name; }).id; self.log.info('Id : ' + id + ' found for list name : ' + interest_name ); return id; }; // @As found in the node-validator module // seems to cover allmost all usecases Mailchimp.prototype.isEmail = function( email ){ return /^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/.test( email ); } // Test purposes, make sure the interface is accessible Mailchimp.prototype.printConfig = function(){ return JSON.stringify({ api_key : this.api_key, api_version : this.api_version, dc : this.dc, list_id : this.list_id }, null, 4 ); } Mailchimp.prototype._parseResponse = function( res ){ var parsed; try { parsed = JSON.parse( res ) } catch( e ){ parsed = res; } return parsed; } Mailchimp.prototype._handleRequestDone = function( callback ){ var self = this; return function( err, body, response ){ if( err ){ callback( err, null ); } else { callback.apply( self, [ null, self._parseResponse( response ) ] ); } } } Mailchimp.prototype._makeAuthorizationHeader = function(){ return "Basic " + new Buffer( "anystring:" + this.api_key, "utf-8" ).toString("base64"); } Mailchimp.prototype._capitalizeFirstLetter = function( word ){ return word[0].toUpperCase() + word.slice(1); } Mailchimp.prototype._makeMethodName = function( resource ){ var self = this; var parts = resource.split('-'); return _.map( parts, function( part ){ return self._capitalizeFirstLetter( part ); }).join(''); } Mailchimp.prototype._generateHTTPHelper = function( http_verb ){ var self = this; var methodName = "_http" + self._capitalizeFirstLetter( http_verb.method ); if( http_verb.json_body ){ self[ methodName ] = function( url, data ){ var callback = arguments[ arguments.length - 1 ]; request({ headers: { "Authorization": self._makeAuthorizationHeader() }, method : http_verb.method, url : url, json : data }, self._handleRequestDone( callback ) ); } } else { self[ methodName ] = function( url ){ var callback = arguments[ arguments.length - 1 ]; request({ headers: { "Authorization": self._makeAuthorizationHeader() }, method : http_verb.method, url : url }, self._handleRequestDone( callback ) ); } } } // Helper | Promisified version of the fs write method, adapted for json only Mailchimp.prototype._writeJson = function( path, content ){ content = typeof content == 'string' ? content : JSON.stringify( content, null, 4 ); return fs.writeFileAsync( path + '.json', content, 'utf-8' ); } // @private, construct the url according to Mailchimp api 3.0 Mailchimp.prototype._makeResourceURL = function( resource ){ if( resource[0] != '/' ){ resource = '/' + resource; } var url = 'https://{dc}.api.mailchimp.com/{api_version}/lists/{list_id}{resource}' .replace('{dc}', this.dc ) .replace('{api_version}', this.api_version ) .replace('{list_id}', this.list_id ) .replace('{resource}', resource ) return url; } // "Patchify" | Apply the b patch on the a object // --------------------------------------------------- // Update all object a's keys with the ones in b // only if they are of the same type. Any other key contained in b // that is not present in a will be ignored. This is typically the // algorithm that is applied when doing a PATCH on a resource. Mailchimp.prototype.patchify = function( a, b ){ var self = this; var o = {}; var distinct_keys = []; Object.keys( a ).forEach(function( key ){ distinct_keys.push( key ); }); Object.keys( b ).forEach(function( key ){ if( distinct_keys.indexOf( key ) == -1 ){ distinct_keys.push( key ); } }); distinct_keys.forEach(function( key ){ if( !(key in a) ){ return; } if( (key in a) && !(key in b) ){ return o[ key ] = a[ key ]; } if( (key in a) && (key in b) && (typeof a[ key ] == typeof b[ key ]) ){ if( typeof a[ key ] == "object" ){ return o[ key ] = self.patchify( a[ key ], b[ key ] ); } else { return o[ key ] = b[ key ]; } } else { o[ key ] = a[ key ]; } }); return o; } Mailchimp.prototype._makeUrlGetter = function( opts ){ var self = this; var methodName = opts.methodName || this._makeMethodName( opts.resource ); // Generate the single-version of the resource self[ "_get" + methodName + "URL" ] = function( id, fields ){ var url; if( !fields || typeof fields == "function" ){ url = self._makeResourceURL( '/'+ opts.resource + 's/' + id ); } if( fields == "filtered" ){ fields = opts.get_default_fields; } if( Array.isArray( fields ) ){ url = self._makeResourceURL( '/' + opts.resource + 's/' + id + '?fields=' + fields.join(',') ); } if( url ){ // Strip the domain part for maximum readability in the console // self.log.info("Generated url (1) : ..." + url.slice( 34, url.length ) ); return url; } self.log.error( "Error, unable to make url for resource " + opts.resource ); } // Generate the multiple-version of the resource self[ "_get" + methodName + "sURL" ] = function( fields ){ var url; if( !fields || typeof fields == "function" ){ url = self._makeResourceURL( '/' + opts.resource + 's' ); } if( fields == "filtered" ){ fields = opts.get_default_fields; } if( Array.isArray( fields ) ){ fields = opts.formatFields( fields ); url = self._makeResourceURL( '/' + opts.resource + 's?fields=' + fields.join(',') ); } if( url ){ // Strip the domain part for maximum readability in the console // self.log.info("Generated url (2) : ..." + url.slice( 34, url.length ) ); return url; } self.log.error( "Error, unable to make url for resource " + opts.resource ); } } Mailchimp.prototype._isPatchPossible = function( read_only_fields, patch ){ var has_unpatchable_keys = false; for( var key in patch ){ if( read_only_fields.indexOf( key ) != -1 ){ has_unpatchable_keys = true; } } return !has_unpatchable_keys; } Mailchimp.prototype._generateRestInterface = function( opts ){ var self = this; var methodName = opts.methodName || this._makeMethodName( opts.resource ); self._makeUrlGetter( opts ); // Get a single ressource based on its id self[ "get" + methodName + "Async" ] = function( id ){ var callback = arguments[ arguments.length - 1 ]; var url = self[ "_get" + methodName + "URL" ]( id, "filtered" ); self._httpGet( url, callback ); } // Get multiple ressources, by appending a discrete 's' to the method name self[ "get" + methodName + "sAsync" ] = function( fields ){ var callback = arguments[ arguments.length - 1 ]; var url = self[ "_get" + methodName + "sURL" ]( fields ); self._httpGet( url, function( err, res ){ var parsed = opts.getRes( res ); if( parsed ){ callback( null, parsed ); } else { callback( err, null ); } }); } // Create one ressource. The resource is returned by Mailchimp upon creation self[ "create" + methodName + "Async" ] = function( data ){ var callback = arguments[ arguments.length - 1 ]; var url = self[ "_get" + methodName + "sURL" ](); if( typeof opts.setCreateDefaults == "function" ){ opts.setCreateDefaults( data ); } self._httpPost( url, data, callback ); } // Delete a resource self[ "delete" + methodName + "Async" ] = function( id ){ var callback = arguments[ arguments.length - 1 ]; var url = self[ "_get" + methodName + "URL" ]( id ); self._httpDelete( url, callback ); } // Update a resource self[ "update" + methodName + "Async" ] = function( id, patch ){ var callback = arguments[ arguments.length - 1 ]; var url = self[ "_get" + methodName + "URL" ]( id ); // self.log.debug( id ); // self.log.debug( patch ); if( self._isPatchPossible( opts.read_only_fields, patch ) ){ // self.log.info("Patch is possible (same email address)"); self._httpPatch( url, patch, callback ); } else { var resource_ref; // self.log.info("Patch is not possible (new email address)"); self[ "get" + methodName + "Async" ]( id, function( err, resource ){ if( err ) return callback( err, null ); resource_ref = self.patchify( resource, patch ); self[ "delete" + methodName + "Async" ]( id, function( err ){ if( err ) return callback( err, null ); self[ "create" + methodName + "Async" ]( resource_ref, function( err, resource ){ if( err ) return callback( err, null ); callback( null, resource ); }); }) }); } } // Write the state of the base in a local json file self[ "write" + methodName + "sToFile" ] = function( path ){ var self = this; var ref = []; var path = path || process.cwd(); return this[ "get" + methodName + "s"]( opts.write_default_fields ) .then(function( res ){ if( res ){ ref = res; return self._writeJson( path + '/' + opts.resource + 's', ref ); } else { self.log.warn("There was nothing to be written on file (res=" + res +")"); } }) .then(function(){ return self.log.info( opts.resource + 's.json updated (%n entries)'.replace('%n', ref.length)); }); } } Mailchimp.prototype.getInterestsAsync = function( category_id ){ var self = this; var callback = arguments[ arguments.length - 1 ]; var fields = 'interests.category_id,interests.list_id,interests.id,interests.name,interests.subscriber_count'; var url = self._makeResourceURL( '/interest-categories/' + category_id + '/interests?fields=' + fields ); self._httpGet( url, function( err, res ){ if( res.interests ){ callback( null, res.interests ); } else { callback( err, null ); } }); } Mailchimp.prototype.writeInterestsToFile = function( path ){ var self = this; var L = 0; var path = path || process.cwd(); return this.getInterestCats( "filtered" ) .then(function( interest_cats ){ var promises = []; for( var i=0; i<interest_cats.length; i++ ){ var p = self.getInterests( interest_cats[ i ].id ); promises.push( p ); } return Promise.all( promises ) .then(function(){ var res = []; for( var i=0; i< arguments.length; i++ ){ res = res.concat( arguments[ i ] ); L += arguments[ i ].length; } if( res.length != 0 ){ return self._writeJson( path + '/interests', res ); } else { self.log.warn("There was nothing to be written on file (res=" + res +")"); } }); }) .then(function(){ return self.log.info( 'interests.json updated (%n entries)'.replace('%n', L )); }); } module.exports = Mailchimp