How to build a Spotify Playlist Slackbot with Serverless Cloud

Feb 1, 2022

Have you ever wanted to keep track of music shared in a company Slack channel? We are here to do just that! In this tutorial, we will use Serverless Cloud as the bridge between a Slack app and a Spotify app, adding any shared tracks to a centralized playlist!

Getting Started

Create a new empty directory called `slack-playlister`, then open this new directory in your preferred code editor. This directory will contain all of your app files. Using your terminal, run the following command within the slack-playlister directory to initialize a new Serverless Cloud project.

npm init cloud


You may be prompted to login if you haven’t already, then the CLI will ask you to name your new application and select a template. Enter “slack-playlister” as the name, and choose the “JavaScript API” template. 

Select the JavaScript API starter template


Within just a few seconds, your new application will be generated in the directory and deployed to your developer sandbox in the cloud. The CLI will now enter development mode, streaming live logs and errors to your terminal as you iterate through the project. Development mode does not prevent commands, so you can continue to interact with the CLI as you are working on the project.

Your new project has been created and the cloud shell is connected


Awesome. Now that we have our Cloud app set and ready, we need to set up the Slack and Spotify apps. 

Making a Slack App

First off, we need to get to the app portal for your Slack workspace. Go to the top left corner of your team Slack window, and click “Administration” and then “Manage Apps”. This should open your browser to a list of installed Slack apps currently in your workspace. Go to the top right of this page, and click “Build”.



Select the Slack workspace where you'd like to run the app


Click “Create New App” and “From Scratch” in the modal that appears. Enter in whatever name you would like, but keep it friendly as this will be visible to your entire Slack workspace. Select your desired workspace from the dropdown.

Now, jump back to your Cloud app code. We need to create an endpoint that Slack will use as a webhook whenever messages are posted. For this, let’s use a POST to “/slack”. Within the endpoint, for now, we will need to add some Slack mandated requirements so we can approve our endpoint with the Slack backend. All we need to do is send back a 200 with a “challenge” key in the body when a request with a “challenge” key is sent. 

import { api } from '@serverless/cloud'
api.post("/slack", async (req, res) => {
  // For setting up new event listeners for Slack bots
  if (req.body.challenge) {
    return res.status(200).json({ challenge: req.body.challenge });
  }
  return res.sendStatus(200)
})


Once you save, this will immediately be synced to your developer sandbox. Now, copy the URL for your developer sandbox in your terminal, and go back to your Slack app configuration. Click “Event Subscriptions”, and then click the toggle to “On”. Paste your developer sandbox URL into the Request URL input, but be sure to add a “/slack” to the end of it. Slack will immediately try to hit your instance to verify the URL. You should see a green Verified message shortly after. 

Add you developer sandbox URL to to enable events in Slack


Now we want to subscribe to some Slack events. Click “Subscribe to events on behalf of users” and then “Add Workspace Event”. Add “message.channels” under “channels:history”. This will send us any message sent in a public channel on the workspace. Hit “Save Changes”. Immediately, Slack will start hitting your new API with any message sent in any channel. 

This is a bit hectic, though, as it receives every public message in the Slack workspace. Let’s give users in our workspace a bit more control by allowing them to manage which channels the playlister pulls from. 

To do this, we need to go back to our Slack app configuration. Go to “Basic Information”, “Slash Commands”, and then “Create New Command”. Call it whatever you want, I chose “/subscribe”. Paste in your developer sandbox URL and add “/subscribe” to the end. Enter a description if you like, and then hit save. 

Let’s use Serverless Data to track what channels the app should listen to. Make a new POST endpoint with the route “/subscribe”. The Slack bodies are pretty large, but we only need a few things from them as the user does not need to send any arguments. 

For this endpoint, we will need an HTTP library in our backend. I chose axios, but feel free to choose anything you are comfortable with. Slack gives our API a “response URL” to send any messages back to the user who called it, so axios will let us do this. To add axios, simply type `install axios` in your Cloud Shell. 

import { api, data } from "@serverless/cloud";

const postToResponseURL = async (
  url,
  channelName,
  msg
) => {
  await axios.post(url, {
    channel: channelName,
    blocks: [
      {
        type: "section",
        text: { type: "mrkdwn", text: msg },
      },
    ],
  });
};


api.post("/subscribe", async (req, res) => {
  const channelName = req.body.channel_name;
  const responseUrl = req.body.response_url;
  try {
    if (req.body.channel_id) {
      const channelId = req.body.channel_id;
      await data.set(
        `channel:${channelId}`,
        {
          channelId,
          added: new Date().toISOString(),
        },
        {
          overwrite: true,
        }
      );
      await postToResponseURL(
        responseUrl,
        channelId,
        `Subscribed to ${channelName}`
      );
      return res.sendStatus(200);
    }
  } catch (e) {
    if (responseUrl && channelName) {
      await postToResponseURL(
        responseUrl,
        channelName,
        `Error: ${JSON.stringify(e)}`
      );
    }
    return res.status(500).send(e);
  }
});


Let’s walk through the code here: We want to assure that this request was indeed sent by Slack, and we do this by checking for the channel_id field. 

If those are present, we want to call `data.set`, making a new collection for channels using the prefix “channel”. Alongside this, we write the channel ID and the date the channel was added.

Since by this point we know that this is a Slack request, we send back a quick confirmation post using axios to the response_url supplied by Slack. This message will appear in the user’s window almost immediately. To handle any errors, though, we do something very similar. But, we do not know if we have a response url to send anything, so we want to check those fields in the catch block. 


Subscribe your app to a channel


By this point, you should be able to successfully subscribe your app to any desired Slack channels! If you really want to make sure it is working, go into your app’s dashboard at cloud.serverless.com and do a `channels:*` query in the Data tab. This should reveal all of your subscribed channels.

Listening for Spotify Links



With all that done, we can finally start recording shared Spotify links from Slack’s webhook in the “/slack” endpoint.

api.post("/slack", async (req, res) => {
  // For setting up new event listeners for Slack bots
  if (req.body.challenge) {
    return res.status(200).json({ challenge: req.body.challenge });
  }
  const channelId = req.body.event.channel;
  const teamSubscribedChannels = await data.get(
    `channel:*`
  );
  if (!teamSubscribedChannels.items.length) {
    return res.sendStatus(200);
  }
  const channels = teamSubscribedChannels.items.map((i) => i.value.channelId);
  if (channelId && channels.includes(channelId) && req.body.event.text) {
    const msg = req.body.event.text;
    const msgParts = msg.split("\n");
    const spotifyLinks = msgParts.filter((p) =>
      p.includes("open.spotify.com/track")
    );
    await Promise.all(
      spotifyLinks.map((link) => {
        let trackId = link.split("/")[4];
        if (trackId.includes("?")) {
          trackId = trackId.split("?")[0];
        }
        const spotifyTrackId = `spotify:track:${trackId}`;
        return data.set(
          `track:${trackId}`,
          {
            spotifyTrackId,
          },
          {
            overwrite: true,
          }
        );
      })
    );
  }
  return res.sendStatus(200);
});


Let’s walk through this new code. We start off with a query using Serverless Data to see if there are any channels we should listen to. Using the query result, we check to see if the incoming message is from a subscribed channel ID. 

If it is, we want to parse the text out to look for any Spotify links, splitting on the newline character to get multiple lines, and filtering each to get any that contain a track link from Spotify. 

Once these are found, we parse the link to get the track ID in order to convert it to a Spotify URI. Once we have that, we save it with Serverless Data using a “tracks” namespace with the spotifyTrackId in the attributes. 

Setting up a Spotify App

Let’s set up a Spotify App to be able to add these tracks to a playlist. 

Login to your Spotify account, and go to the Spotify Developer Dashboard. Click “Create New App”, then enter a name and description. Once created, pull up your Cloud dashboard on another tab. Click the Gear icon on the left sidebar and then navigate to “Parameters”. Here we are going to drop in the information from our new Spotify App and save it securely.

Put both the Spotify Client ID and Secret in as “SPOTIFY_CLIENT_ID” and “SPOTIFY_SECRET” and save. Since these are at your organization level, they will be available for any instance of the playlister you make in the future.

While we are here, let’s put in the playlist ID we will want to add to. You can either make a new one or use an existing one, as long as it is public. To get the ID, copy the playlist link and grab the ID that comes after “/playlist/”. Save this as SPOTIFY_PLAYLIST_ID in your cloud parameters. 

We have one last thing to do in the Spotify Developer Dashboard. Click “Edit Settings” in your new app, and add your developer sandbox URL + “/redirect” to the Redirect URI’s section. This will allow our app to be properly authenticated. 

Now finally, let’s write some more code. 

Authorizing Spotify

We are going to use two endpoints for authorization with Spotify, and since we need access to a specific playlist, and the ability to add to it, we need to manually grant access. This will only need to happen once every time we make a new stage, or you can simply copy your refresh token over to future instances.

For Spotify interactions, we will be using the awesome Spotify Web API for Node. To add this to your project, just type “install spotify-web-api-node” in the cloud shell. We will also need to bring in the “params” interface from the SDK for accessing our saved Spotify information.

import { api, params, data } from "@serverless/cloud";
import SpotifyWebApi from "spotify-web-api-node";

const spotify = new SpotifyWebApi({
  clientId: params.SPOTIFY_CLIENT_ID,
  clientSecret: params.SPOTIFY_SECRET,
  redirectUri: params.CLOUD_URL + "/redirect",
});

api.get("/redirect", async (req, res) => {
  if (req.query.code) {
    const authData = await spotify.authorizationCodeGrant(req.query.code);
    await data.set(
      "spotify:auth",
      {
        refreshToken: authData.body["refresh_token"],
      },
      { overwrite: true }
    );
  }
  return res.sendStatus(200);
});

api.get("/spotify", (req, res) => {
  const scopes = [
    "playlist-modify-public"
  ];
  const authUrl = spotify.createAuthorizeURL(scopes);
  return res.redirect(authUrl);
});


We need to grant Spotify access manually once for this instance and save the refresh token for future use. After adding this code to your project, go to your personal instance URL + “/spotify” in a browser. This should pull up your Spotify account (if logged in) asking permission to use your account (this is our app). Click allow, and it should redirect back to the app and save the refresh token using data. If this does not work, or you get an INVALID_REDIRECT_URI, be sure you added the URL to your Spotify app!

Adding saved songs to the playlist

With authentication ready, we can start making things happen for shared songs using Serverless Data. For this, we will be using a data listener with data.on.

data.on(["created:track:*"], async (event) => {
  const { item } = event;
  const { key } = item;
  const authData = await data.get("spotify:auth");
  if (!authData) {
    console.log("No Spotify Auth Saved");
    return;
  }
  spotify.setRefreshToken(authData.refreshToken);
  const refreshData = await spotify.refreshAccessToken();
  spotify.setAccessToken(refreshData.body["access_token"]);
  const spotifyTrackId = item.value.spotifyTrackId;
  const currentSongsOnPlaylist = await spotify.getPlaylistTracks(params.PLAYLIST_ID);
  const playlistTrackIds = new Set(
    currentSongsOnPlaylist.body.items.map((i) => `spotify:track:${i.track.id}`)
  );
  if (playlistTrackIds.has(spotifyTrackId)) {
    console.log(`${spotifyTrackId} already on playlist`);
    return;
  }
  await spotify.addTracksToPlaylist(params.PLAYLIST_ID, [spotifyTrackId]);
});


And with that, you are now live listening for Spotify links and adding them to your chosen Spotify Playlist. 

To dive into the code a bit more, we start by only firing this listener for newly saved tracks added by our Slack listener. We pull the saved refresh token from our data store in order to authenticate our Spotify API Instance. If we don’t have any authentication data saved, we return, as we cannot process the request.

With the refresh token in hand, we refresh the access token and add it to the Spotify instance. Our app is serverless, so we do this every time the listener fires since we can’t guarantee that the Spotify instance always has an access token ready. 

Next, we check for duplicates. Either you or someone else could have manually added the song to the playlist already. We get the current tracks, convert them to a set using Spotify’s URI format, and make sure the incoming track isn’t present. If the song isn’t currently on the playlist, we call addTracksToPlaylist, and it should show up shortly on your Spotify app. 

Summary

If you’ve made it through all of this, congratulations! You can now make some nice playlists for you or your teammates to revisit and listen to. While this only works with Spotify Track URLs, it can be easily expanded to cover other formats.

When you are all done developing, be sure to deploy the app to a stage, using deploy “stageName”. This will make a smaller bundle of your code and allow your app to run even faster. You’ll also have to update your Slack and Spotify apps to use this new URL and re-authenticate.

So what are you waiting for? Go ahead and create your company’s playlist with Serverless Cloud or check out the source code.

Try Serverless Console

Monitor, observe, and trace your serverless architectures.
Real-time dev mode provides streaming logs from your AWS Lambda Functions.

Subscribe to our newsletter to get the latest product updates, tips, and best practices!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.