For transparency purposes I wrote a script that informs me of any changes to the user list of your Slack team. The whole thing is a fairly compact concept, with 2 different integration points to Slack and some funny Coffeescript usage.

Application flow

The application is something really simple that goes like this:

graph TD;

config[Retrieve configuration] --> Slack_users
config --> Saved_users
Slack_users[Retrieve users from Slack] --> Compare[Compare lists]
Saved_users[Retrieve saved users] --> Compare
Compare --> Send_message[Send message to configured users]
Compare --> Save_users[Save users]

Configuration

Configuration is stored in environment variables, then loaded into a configuration object that is passed around to whatever needs it. For something small and functional like this, I really like overusing the method factory pattern.

Loading configuration is as simple as this:

config =
  sendTo: process.env.SLACK_SENDTO.split ","
  slack:
    hostname: process.env.SLACK_HOSTNAME
    slackbotToken: process.env.SLACK_SLACKBOTTOKEN
    token: process.env.SLACK_APITOKEN
  archiveFile: "users.json"

Loading things into an object has the advantage of making it a bit cleaner and organized. It’s also easier to pass the configuration to whatever needs it, and helps you respect the dependency inversion principle.

In my “main” file, I load the configuration

return unless (config = require "./config")
getUsers = (require "./getUsers") config
sendMessage = (require "./sendMessage") config
archiveUsers = (require "./archiveUsers") config

Note the unless, a neat syntactic sugar from coffee.

All the dependencies are built using the method factory pattern:

module.exports = (config) -> (callback) ->
  https.get "https://slack.com/api/users.list?token=#{config.slack.token}&pretty=1", [...]

Integrating to Slack

Integration to Slack here is done in 2 ways:

  • API integration, which gives you to a full list of resources for integration with Slack. Here, we use it to retrieve the list of users.
  • Slackbot integration, the simplest way to send Slack messages from the outer world. We use it here to send messages to users.

Both integrations can be accessed from the “manage my team” interface.

Retrieving the list is done through a GET on https://slack.com/api/users.list?token=#{config.slack.token}&pretty=1"

Posting to slackbot is done through a POST to /services/hooks/slackbot?token=#{config.slack.slackbotToken}&channel=#{to}

Comparing the lists

Few funny things around there:

  • Everything is value, so this is a legit way of collating a list of users into a string (although arguably it can be done using map):
(for user in users
    (user.profile?.real_name + "(" + user.name + ")")
  ).join ","

Finding delta is done through the following logic:

  • Find users in new list that don’t exist in archive (new users)
(updatedUser, savedUser) -> not savedUser
  • Find users in archive that aren’t in new list, or are marked deleted in new list and not in archive (deleted users)
(savedUser, updatedUser) -> (updatedUser.deleted and not savedUser.deleted) or not updatedUser

which we feed to a function that filters entries in lists:

getListDelta = (listA, listB, comparer) ->
  listA.filter ((elementFromA) =>
    elementFromB = listB.find (elementFromB) => elementFromA.id is elementFromB.id
    return comparer elementFromA, elementFromB
  )

Result

The rest is fairly simple, saving stuff and loading stuff. The project can be found here.