Spotify Mixmax Integration

A Spotify Mixmax slash command that allows you to search for a track, artist, or album to embed in an email.

Three types of rich links

Background

Mixmax is an awesome startup that provides powerful analytics, automation, and enhancements for Gmail. I thought this service was awesome and wanted to see if I could create a Mixmax integration that I could see myself using since I use the service a lot myself.

The API that Mixmax provides allows you to make any of three integrations:

  1. Slash Commands
  2. Enhancements
  3. Link Resolvers

I decided that I was going to make a slash command that creates a rich media link to Spotify for a specific song, artist, or album.

Requirements

In order for a simple Mixmax slash command to work, two components are needed:

  1. Typeahead
  2. Resolver

Once those two things are implemented in your slash command, it will be able to resolve the URL you provide and generate a rich preview of a template you can set.

Since email templating is years behind normal web templating, and not all email providers render emails the same, we will need to make sure our templates use backward-compatible HTML elements and CSS properties.

Flexbox my friend, we shall reunite soon!

Implementation

After laying out the requirements, the first thing that I got to work on was the typeahead.

What is a typeahead?

Slash commands are triggered when a user types in "/" into a draft email. You can think of the typeahead as an API - a resource the Mixmax menu will reference to load your specific slash command data. By default, that menu provides all of the slash commands that Mixmax currently has.

Creating the typeahead

In order to get the slash command to show up locally, I needed to add the slash command to the Mixmax Developer Dashboard.

Once that was done, I got started on the actual logic for the typeahead, since it would be the hardest component to do. The first thing we did was define all of our constants:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var key = require('../utils/key');
var request = require('request');
var _ = require('underscore');
var createTemplate = require('../utils/template.js').typeahead;

// get client keys
const keys = require('./clientKeys');

// define options a user can choose
var options = {
	Album: "album",
	Track: "track",
	Artist: "artist"
}

// set a variable for our access token that'll be refreshed automatically by Spotify every 6 minutes
let access_token;

The first thing we need to do is get the users option and find the matching hash key for that input. We can do that using two of Lodash's helper functions: _.keys, and _.find

1
2
3
4
5
6
7
module.exports = (req, res) => {
	var input = req.query.text.slice();

	// if user has selected option then we will prefix the option to the search string
	var selectedOption = _.find(_.keys(options), key => {
		return input.indexOf(key + ':') === 0;
	})

Now that we have gotten their input we can now figure out which options to return. The options are filtered based on the user's input, as long as they haven't submitted an option. We can then set the first parameter for our Spotify query.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// if user doesn't have a valid option selected, they're still deciding between track, artist or album
if (!selectedOption) {
  var matchingOptions = _.filter(_.keys(options), option => {
    return input.trim() === '' ? true : option.toLowerCase().startsWith(input.toLowerCase());
  });

  if (matchingOptions.length === 0) {
    res.json([{
      title: 'You can only choose album, track, or artist.',
      text: ''
    }])
  }

  else {
    res.json(matchingOptions.map(option => {
      return {
        title: option,
        text: option + ':',
        resolve: false // don't automatically resolve and remove the text
      }
    }));
  }
  return;
}

// the search term is the remaining string after the option and the colon
var valueToSearch = input.slice((selectedOption + ': ').length)

At this point, the user has either selected an option and typed in a value, or they're thinking about what to search for. We can ask the user what they're looking for until there is data to be shown:

1
2
3
4
5
6
7
8
// if they haven't started entering a value, ask them what they're searching for
if (valueToSearch === "") {
  // user hasn't typed in an option yet
  res.json([{
    title: "What " + options[selectedOption] + " are you looking for?",
    text: ''
  }])
}

The Spotify API requires that we pass in our client key and secret as a base64 encoded string. Since we needed it in that format I created a small function that given a string, will utilize the Buffer instance provided by NodeJS, and return the base64 encoding of that string:

1
2
3
4
// simple function using the buffer module to return base64 for the authorization header.
function base64(str) {
  return new Buffer(str).toString('base64');
}

Using the extremely popular request module, we can now query Spotify's API by first passing in our base64 encoded clientKey, and then searching:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
else {
// Spotify requires that we get an access token before a request. 
// Since we're providing a clientID and secret, we don't need an OAuth Token.
request({
  url: 'https://accounts.spotify.com/api/token',
  method: 'POST',
  headers: {
    Authorization: 'Basic ' + base64(keys.clientID + ":" + keys.clientSecret)
  },
  form: {
    grant_type: 'client_credentials'
  },
  json: true
}, (err, authResponse) => {
  if (err || authResponse.statusCode !== 200 || !authResponse.body) {
    res.status(500).send('Error');
    return;
  }

  let access_token = authResponse.body.access_token;

  // add wildcards to actual search so the outputted array better matches what a user searches
  request({
    url: 'https://api.spotify.com/v1/search',
    headers: {
      Authorization: "Bearer " + access_token
    },
    qs: {
      q: "*" + valueToSearch + "*",
      type: options[selectedOption]
    },
    json: true
  }, (err, response) => {
    if (err || response.statusCode !== 200 || !response.body) {
      res.status(500).send('Error');
      return;
    }

    // The first property return by the api is the plural version of the option we choose.
    let selection = options[selectedOption] + 's'

    // go through array of items returned from response and return data for the templates to use for each item
    var results = _.chain(response.body[selection].items)
    .map(data => {
      return {
        title: createTemplate(data),
        text: data.href
      };
    })
    .value();

    // nothing returned from spotify api
    if (results.length === 0) {
      res.json([{
        title: "No results found for <i>" + valueToSearch + "</i>.",
        text: ''
      }]);
    } else {
      res.json(results);
    }
  });
});
}

At this point the typeahead should be fully functional! Here's an example of what it looks like:

Mixmax typeahead
What is the resolver?

When a user selects a result from the list, Mixmax needs the slash command to provide content to put in the email. THe resolver will take our search string, and resolve it against Spotify's API.

The resolver will then take the data that Spotify returns and create a template for our rich media link.

Creating the resolver

For the resolver, the first thing we will need to do once again is define all of our constants for the module:

1
2
3
4
5
6
7
var key = require('../utils/key');
var request = require('request');
var _ = require('underscore');
var createTemplate = require('../utils/template.js').resolver

// get client keys
const keys = require('./clientKeys');

The second thing we need to do is handle the search string that the typeahead module gave us. One of the easiest ways to do this would be to create a function that takes in a search string, a request object, and a response, that way we can directly plug in our request and response into our application.

We once again pass in our base64 encoded string, and make a request to Spotify:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function handleSearchString(term, req, res) {
// Spotify requires that we get an access token before a request. Since we're providing a clientID and secret, we don't need an OAuth Token.
request({
  url: 'https://accounts.spotify.com/api/token',
  method: 'POST',
  headers: {
    Authorization: 'Basic ' + base64(keys.clientID + ":" + keys.clientSecret)
  },
  form: {
    grant_type: 'client_credentials'
  },
  json: true
}, (err, authResponse) => {
  if (err || authResponse.statusCode !== 200 || !authResponse.body) {
    res.status(500).send('Error');
    return;
  }

  let access_token = authResponse.body.access_token;

Next, we pass in our access token, and set the resulting object to be provided to the templates each containing different information depending on the result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
request({
  url: term,
  headers: {
    Authorization: "Bearer "+ access_token
  },
  json: true
}, (err, response) => {
  if (err || response.statusCode !== 200 || !response.body) {
    res.status(500).send('Error');
    return;
  }
  let results;
  let data = response.body;
  const dateOptions = {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }

  // data for album
  if (response.body.album_type) {
    results = {
      type: 'album',
      album_url: data.external_urls.spotify,
      artist_url: data.artists[0].external_urls.spotify,
      image_url: data.images[2].url,
      name: data.name,
      artist: data.artists[0].name
    }
  }

  // data for track
  else if (response.body.album) {
    results = {
      type: 'track',
      album_url: data.album.external_urls.spotify,
      image_url: data.album.images[2].url,
      name: data.name,
      song_url: data.external_urls.spotify,
      artist_url: data.artists[0].external_urls.spotify,
      artist: data.artists[0].name
    }
  }

  // data for artist
  else {
    results = {
      type: 'artist',
      artist_url: data.external_urls.spotify,
      image_url: data.images[2].url,
      name: data.name,
      followers: data.followers.total.toLocaleString()
    }
  }

  res.json({
    body: createTemplate(results)
  })
});
});
}

This will allow us to just create the term, and pass it in along with the request and response in our module.exports:

1
2
3
4
5
module.exports = function(req, res) {
  var input = req.query.text.trim();

  handleSearchString(input, req, res);
};

And, voilà! Once we create the templates are created with the data, we are done! Here's what the end result of my application looks like in my example:

Mixmax Resolver

Project Challenges

This was one of the more technically challenging projects I've worked on. There was a lot that I didn't understand at first and realized I needed to find some other implementations.

Some of the things that I learned a lot about from this project include:

  • Lodash
  • Using a response to resolve another response
  • Creating a web application with only NodeJS and HTML
  • Algorithmic thinking: It challenged me to think in order of logical steps rather than how do I get from point A to point B
  • base64 encoding

Technologies Used

  • NodeJS
  • HTML5/CSS3

Available Links

© Malik Browne 2018. All rights reserved.