Projects Meta Quick Hacks About

SlackMood - Analyse your teams happieness via Slack Emoji use

Jun 27, 2016

We had a hack day at Songkick a few weeks back, and I decided I wanted to build something with Slack. Like any sensible company, we use Slack to help us colaborate and improve communication, but we also use it to share catgifs (we have an entire channel) and a whole host of default, aliased and custom emojis. Based on this, I wondered if I could use our emoji use to gague the average mood of the whole company. And so SlackMood was born.

My first step was figuring out how to get a feed of messages across our whole Slack. I’d already decided to build it in Golang, fortunatey some clever person had already built a Golang library for Slack, saving me a huge amount of work. I registered a new bot on the Slack developer site and started hacking.

Unfortunately I quickly ran into an issue. I wanted to get the RTM (real-time message) feed of every channel, but it turns out bot accounts can’t join channels unless their invited. I could see 3 solutions to this;

  1. Create a real Slack user with an API key (I decided Finance wouldn’t be happy with this)
  2. Add my own API key alongside the bot, use the API to have me join all the channels, invite the bot and leave - annoying everyone in the company
  3. Use the message history APIs to periodically scrape the channels.

I decided to go with 3, as it seemed the simplest to implement.

The actual code for this was relatively simple

for _,c := range channels{
  if c.IsArchived{
    continue
  }
  hp := api.NewHistoryParameters()
  hp.Count = 1000
  h, err := s.Api.GetChannelHistory(c.ID, hp)

  if err != nil {
    log.WithFields(log.Fields{
      "error": err,
      "channelId": c.ID,
      "channel": c,
    }).Warning("Could not fetch channel history")
  } else {
    models.ParseEmoji(h.Messages)

    log.WithFields(log.Fields{
      "channel": c.Name,
      "channelId": c.ID,
      "messages": len(h.Messages),
    }).Debug("Got channel history")
  }
}

It then passes the message object into a function that extracts the emoji counts

func ParseEmoji(messages []api.Message){
  r := regexp.MustCompile(`:([a-z0-9_\+\-]+):`)

  for _,m := range messages{
    msgId := fmt.Sprintf("%s-%s-%s", m.Timestamp, m.Channel, m.User)
    for _,r := range m.Reactions{
      emojiList.AddEmoji(r.Name, m, fmt.Sprintf("%s-%s-%s", msgId, m.User, m.Name))
    }

    foundEmoji := r.FindAllStringSubmatch(m.Text, -1)
    for _,em := range foundEmoji{
      emojiList.AddEmoji(em[1], m, msgId)
    }
  }
}

It uses both a regular expression on the message, and interating over the reactions.

I’d decided to use BoltDB for the backend storage, maybe not the best idea as I think a relational datastore like Sqlite would have been much better suited, but Bolt was a technology I’d never used before so it seemed interesting. We generate a message ID from the base message, then the reactions all have their own IDs based on the user who posted them. These are all stored in BoltDB as message ID -> details, where details is a struct describing the emoji;

type Emoji struct{
  Name      string
  SeenAt    time.Time
  Channel   string
  User      string
}

Now we’ve got a list of emojis and their timestamps, we can go through and assign each one a rating, of ether positive, negative or neutral. Fortunately, some of our team had already built a spreadsheet of emoji sentiment analysis for a previous hack project (turns out, we love emojis).

With our emoji ranks loaded into a struct array, we can go through and analyse the score of our current listed emoji

func GetMood(emoji []*Emoji) Mood{
  m := Mood{}

  for _, e := range emoji{
    for _,r := range ranks.EmojiRanks{
      if r.Name == e.Name{
        switch r.Rank {
        case 1:
          m.PositiveCount += 1
        case 0:
          m.NeutralCount += 1
        case -1:
          m.NegativeCount += 1
        }
        m.TotalCount += 1
        break
      }
    }
  }

  m.Positive = percentage(m.PositiveCount, m.TotalCount)
  m.Negative = percentage(m.NegativeCount, m.TotalCount)
  m.Neutral = percentage(m.NeutralCount, m.TotalCount)

  return m
}

(N.B. looking back at this now, I realize a map of emojiname -> mood would have been much better rather than a double-loop, but this was like 6 hours in and I’d been drinking a little).

Now we know the mood of all the emojis, calculating the graph just involves iterating through all the seen emojis and storing them in a map of date->mood. The GetMood function above works on a list of emojis, so we just bucket the emojis by the selected time period.

Due to storing all the emoji in Bolt and not being able to do proper filtering, we first filter by the time period we care about, then divide this up.

type Mood struct{
  Positive      float32
  Negative      float32
  Neutral       float32
  PositiveCount int32
  NegativeCount int32
  NeutralCount  int32
  TotalCount    int32
  Time          time.Time
  TimeString    string
}

func FilterEmoji(from time.Time, to time.Time, emoji []*Emoji) []*Emoji{
  var emj []*Emoji
  for _, e := range emoji{
    if e.SeenAt.After(from) && e.SeenAt.Before(to){
      emj = append(emj, e)
    }
  }

  return emj
}

func GraphMood(over time.Duration, interval time.Duration) []Mood{
  var points []Mood

  now := time.Now().UTC()
  dataPointCount := int(over.Seconds()/interval.Seconds())
  endTime := time.Unix(int64(interval.Seconds())*int64(now.Unix()/int64(interval.Seconds())), 0)
  periodEmoji := FilterEmoji(endTime.Add(over*-1), endTime, AllEmoji())
  for i:=0;i<dataPointCount;i++{
    offset := int(interval.Seconds())*(dataPointCount-i)
    startTime := endTime.Add(time.Second*time.Duration(offset)*-1)

    m := GetMood(FilterEmoji(startTime, startTime.Add(interval), periodEmoji))
    m.Time = startTime
    m.TimeString = startTime.Format("Jan _2")
    points = append(points, m)
  }

  return points
}

GraphMood returns a struct array which we can just JSON encode and feed into Chart.JS to get the nice visualization above.

All in all, it was pretty fun but the whole project contains a lot of terrible code. If you want, check it out on Github here.

Other stuff I would have liked to add:

  • Most positive/negative person (I was told this might not have been HR approved though)
  • Most used emoji
  • Biggest winker 😜

Maybe next hack day.


P.S. if you fancy working somewhere with regular hack days, in a team which has a pre-prepared spreadsheet with emoji sentiment analysis, Songkick are hiring a variety of technology roles at the moment. So come work with us, we have a 60% SlackMood happyness rating(tm)