Home

Hackathon V2 - Hello Twitter !

In this month hackathon, we are going to create a Twitter Slack bot to use the Twitter account of theTribe.

Project presentation and motivation

  • Hey man, have you seen this tweet? We should answer with the company’s Twitter!
  • You’re right, give me just a sec and I’ll do it.
  • You’ve got the access?
  • No, but I’ll ask the boss…

A few moments later: Beer!

That’s the risk when you don’t have a communication department and you rely on people with a special sense of humor. Two problems emerged from this real and not fake at all story:

1) Everyone needs to have the credentials for the Twitter account. This is a bad practice and if someone leaves the company, the password must be changed.

2) We don’t want our beer lover developers to post anything when they have been drinking another good craft beer (social medias and alcohol don’t mix very well).

With this in mind, we decided to create a bot for our Slack team. This way no one needs to have the Twitter account credentials and we can decide all together if we want to publish a message or not. Another advantage of creating a bot is that we can capitalize on the bot we created last time.

Conception

We split up into two groups to work on the main features: the voting system and the tweet post.

Vote system

We agreed on a simple set of rules:

  • People will vote by adding a 👍 or 👎 reaction on the suggested message
  • The bot will automatically add both reactions to the message to gain time (we don’t want to open the emoji modal everytime)
  • The message is posted if there are 5 more 👍 than 👎

The bot needs to be aware of the reactions posted on the messages so we are using the two following events.

start() {
  super.start()
  ...
  this.rtm.on(RTM_EVENTS.REACTION_ADDED, this.handleReaction)
  this.rtm.on(RTM_EVENTS.REACTION_REMOVED, this.handleReaction)
}

This way we can retrieve the message on which the reaction has been added. Unfortunately, the Slack API only provides a light object as the message, without the attached list of reactions (how strange is this?), so we need to make another API call to retrieve this list.

handleReaction({ item }) {
  this.checkNeedToPostMessage(item).then(isValid => {
    if (isValid) {
      // Post the message on Twitter
    }
  })
}

checkNeedToPostMessage({ channel, ts }) {
  return this.api.reactions.get({
      channel,
      timestamp: ts,
      full: true
  }).then(({ message: { reactions } }) => {
    return this.vote(reactions) && this.checkAlreadyPosted(reactions);
  })
}

Now, we can send a list of reactions to the vote function and implement the rules described above. The use of lodash makes it simple to read and write the code.

vote(reactions) {
  const plusReactions = _.find(reactions, { name: '+1' })
  const minusReactions = _.find(reactions, { name: '-1' })

  if (!plusReactions || !minusReactions) {
    return false
  }

  return plusReactions.count - minusReactions.count >= MIN_SCORE
}

Now that we have a voting system, all we have to do next is to add 👍 and 👎 reactions to the new messages.

start() {
  super.start()

  this.rtm.on(RTM_EVENTS.MESSAGE, ({ subtype, thread_ts, channel, ts, text, message }) => {
    if (subtype || thread_ts) {
      return
    }

    this.api.reactions.add('+1', { channel, timestamp: ts })
      .then(() => this.api.reactions.add('-1', { channel, timestamp: ts }))
  })
  ...
})

Checking the subtype and the thread_ts variables allow us to differentiate messages in the original channel from edited messages or messages posted in a thread of the channel. We chained our promises to ensure that the reactions are always added in the same order.

Sending a tweet

Posting on Twitter is not really difficult once you installed the Twitter library:

const Twitter = require('twitter');

class TweetBot extends Bot {
  constructor(...args) {
    super(...args)

    this.twitter = new Twitter({
      consumer_key: process.env.TWITTER_CONSUMER_KEY,
      consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
      access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY,
      access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET,
    });
  }
}

In the constructor of our bot, we initialize the Twitter library so that it can use the team’s account to post the messages. Then we implement the sendTweet method:

sendTweet({ text }) {
  const status = text.replace(/<([^>]*)>/g, '$1')
  return new Promise((resolve, reject) => {
    this.twitter.post('statuses/update', { status }, (error, tweets, response) => {
      if (error) {
        reject(error)
      } else {
        resolve(response)
      }
    })
  })
}

In this method several things are done. First, we edit the Slack message, to remove < and > that surround links. Then as the post method is only accepting callbacks, we wrap the call in a promise, allowing us to catch errors and response more easily.

Now that our post method is ready, we can add it to the handleReaction method:

handleReaction({ item }) {
  this.checkNeedToPostMessage(item).then(isValid => {
    if (isValid) {
      this.getMessage(item).then(message => {
        this.sendTweet(message)
      })
    }
  })
}

getMessage({ channel, ts }) {
  return this.api.channels.history(channel, {
    latest: ts,
    count: 1,
    inclusive: true
  }).then(({ messages }) => messages[0])
}

Here we need another method to get the message, as the Slack API doesn’t send the content of the message on the Reaction events. To retrieve a message, we need to call the history method that will send us back messages that correspond to criterias. To get a particular message, you need to pass the timestamp of the message as the latest timestamp, add the inclusive: true, and of course take the first element of the array.

One last thing to do before posting a message is checking that the message hasn’t already been posted. Let’s assume for now that the bot adds the :white_check_mark: or the :x: emoji when the message is posted. With this in mind, we can easily know if the message has been posted.

checkAlreadyPosted(reactions) {
  return _.some(reactions, ({ name, users }) => (name === 'white_check_mark' || name === 'x') && users.includes(this.rtm.activeUserId))
}

Here thanks to lodash, we check in the message reactions if the aforementioned emoji has been added by the bot.

Errors and message length

There are several errors to check and informations to display. We decided to managed only three cases: the success, the unknown error and the message length error. As we wrapped the post method in a promise, we can now manage success and errors in a simple fashion.

handleReaction({ item }) {
  ...
  this.sendTweet(message).then(() => this.addSuccessReaction(item), () => this.addErrorReaction(item))
  ...
}

addSuccessReaction({ channel, ts }) {
  return this.api.reactions.add('white_check_mark', { channel, timestamp: ts })
}

addErrorReaction({ channel, ts }) {
  return this.api.reactions.add('x', { channel, timestamp: ts })
}

This way, we add a check mark icon when the message is posted, or a red cross if a problem occurred while posting.

We don’t need to post the tweet to know the message is too long, so we created a method to check the message length and updated the method linked to the messages creation or update.

start() {
  super.start()

  this.rtm.on(RTM_EVENTS.MESSAGE, ({ subtype, thread_ts, channel, ts, text, message }) => {
    if (subtype === 'message_changed') {
      if (this.sizeOf(message.text) > MAX_SIZE) {
        this.api.reactions.add('no_entry_sign', { channel, timestamp: message.ts })
      } else {
        this.api.reactions.remove('no_entry_sign', { channel, timestamp: message.ts })
      }
    }
    if (subtype || thread_ts) {
      return
    }

    this.api.reactions.add('+1', { channel, timestamp: ts })
      .then(() => this.api.reactions.add('-1', { channel, timestamp: ts }))
      .then(() => {
        if (this.sizeOf(text) > MAX_SIZE) {
          this.api.reactions.add('no_entry_sign', { channel, timestamp: ts })
        }
      })
  })
  ...
})

sizeOf(message) {
  const urlRegex = /<https?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?>/g;
  return message.replace(urlRegex, ' '.repeat(23)).length
}

With this we can display an emoji showing that the text is too long. To understand the sizeOf method, you must know that Twitter automatically minifies the urls to a 23-character-long url.

Conclusion

Well, a picture is worth a thousand words: The Slack message ! Our first tweet !

During this hackathon, we managed to create a simple Slack bot that posts messages on Twitter. This first version is capable of tweeting text, can count votes and publish the message, and warns us if the message is too long or if there has been an error.

This bot is really simple and could be upgraded to send images, retweet or even post on other social medias. Theses features will likely be the subject of another hackathon, so stay tuned for more!

Alexis Dumas

Code, beer, sleep

Nantes, France thetribe.io