Gustav Ehrenborg fullstack developer

Email symbol LinkedIn logo GitHub logo Instagram logo

Building a simple Slack bot Easily send Slack notifications from Rails

Tuesday, June 1, 2021
Part of the Slack API

I thought a custom Slack app sending notifications would be quite advanced to setup, but it was not. Slack has a walkthrough on how to setup an app and a webhook: https://api.slack.com/messaging/webhooks

Following the above guide will result in an URI, like this: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXX, to which messages can be POSTed.

Split the URI into two parts, and add it to the .env file.

SLACK_API_BASE_URI=https://hooks.slack.com
SLACK_NOTIFY_WEBHOOK=Txxxxx/Bxxxxxx/xxxxxx

To incorporate this in Rails, an API client which makes the POST is needed. Using HTTParty, the following lines are all that is needed. The notify method takes the text to be posted, puts it in a JSON object and sends it to Slack.

class SlackApi
  include HTTParty
  base_uri ENV['SLACK_API_BASE_URI']

  def notify(text)
    body = {text: text}.to_json
    self.class.post("/services/#{ENV['SLACK_NOTIFY_WEBHOOK']}", body: body).parsed_response
  end
end

The API client can be used right away, and the following code shows how we use it. A scheduled job will fire the following every morning.

class NotifySlack
  def initialize(api = SlackApi.new)
    @api = api
  end

  def call
    bad_statuses = Status.find.the.bad.ones

    if bad_statuses.count.positive?
      @api.notify('Ouch! There are some problems. :face_with_head_bandage: :boom:')
      bad_statuses.each { |status| @api.notify("#{status.text} :#{random_emoji}:") }
    else
      @api.notify('No problems today, well done! :superhero: :tophat:')
    end
  end

  private

  def random_emoji
    %w[fire firecracker exploding_head scream see_no_evil fearful speak_no_evil neutral_face
       disappointed nauseated_face poop angry face_with_symbols_on_mouth].sample
  end
end

Testing the API client using WebMock to prevent a real HTTP call and instead return the same HTTP response that Slack would. If you have Slack channel for testing purposes, the following test can be duplicated and turned into a integration test by removing the WebMock stuff.

require 'rails_helper'
require 'webmock/rspec'

RSpec.describe SlackApi do
  it { should respond_to :notify }

  subject { SlackApi.new }

  context 'notify' do
    before do
      WebMock.stub_request(:post, /hooks.slack.com/).to_return(status: 200, body: 'ok')
    end

    it 'sends a request to Slack' do
      response = subject.notify(Faker::Movies::StarWars.quote)
      expect(response).to eq('ok')
    end
  end
end

Tests for the scheduled job, the SlackApi client is mocked and then it is checked what messages that was supposed to be sent.

require 'rails_helper'

RSpec.describe NotifySlack do
  let(:api) { double }
  subject { NotifySlack.new(api) }

  context 'when bad status exist' do
    let!(:status) { Fabricate(:status, status: :red) }

    it 'sends messages to Slack' do
      expect(api).to receive(:notify).with(/Ouch! There are some problems/)
      expect(api).to receive(:notify).with(/#{status.text}/)
      subject.call
    end
  end

  context 'when no bad status exist' do
    it 'sends a message to Slack' do
      expect(api).to receive(:notify).with(/No problems today, well done!/)
      subject.call
    end
  end
end

As easy as pie!