Most companies that I’ve worked with use Slack for internal communication.

A very common feature request is to receive a Slack messagewhen something happens in the application.

This can be though of as receiving “webhooks” by your Slack app.

Incoming webhooks are a simple way to post messages from external sources into Slack.

Example notifications:

  • “{email} signed up!”
  • “{email} bought {product} for {price}”
  • “daily stats” (cron job)
  • “daily income CSV” (cron job)

To implement this kind of functionality, we can use Slack API.

First, create a Slack channel (obviously 🤷‍♂️).

Next, visit the Slack API website and create a bot:

slack-1-create-app

Easiest way - to create an app via manifest. Here’s mine:

display_information:
  name: message bot
features:
  bot_user:
    display_name: message bot
    always_online: false
oauth_config:
  scopes:
    bot:
      - chat:write
      - chat:write.public
      - files:write
      - im:write
      - links:write
      - links.embed:write
settings:
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false

Alternatively - create an app from scratch.

After creation go to “OAuth & Permissions” tab:

slack-2-oath-and-permissions

These are the permissions that I would usually select to send messages:

slack-3-permission-scopes

Invite the bot to your slack workspace:

slack-4-install-to-workspace

Allow access:

slack-4-allow-access

After that you will granted an API token. Copy it:

slack-5-copy-token

Try to connect to the token via the console:

slack --slack-api-token=[token] auth test
# slack --slack-api-token=xoxb-123432423-rgwrgerge-657567 auth test

Works? Now let’s make it work with Rails

Slack API + Rails #

Add the generated API key to your Rails app credentials:

# credentials.yml
slack:
  slack_api_token: xoxb-123432423-rgwrgerge-657567

To comfortably interact with the bot using Ruby, install gem slack-ruby-client:

bundle add slack-ruby-client

Initialize Slack token:

# echo > config/initializers/slack.rb
# config/initializers/slack.rb
Slack.configure do |config|
  # config.token = xoxb-123432423-rgwrgerge-657567
  config.token = Rails.application.credentials.dig(:slack, :slack_api_token)
end

Connect to Slack API and send your first message on behalf of the “bot”:

client = Slack::Web::Client.new
client.auth_test
client.chat_postMessage(channel: '#general', text: 'Hello World', as_user: true)

Add a service that will allow you to connect to the Slack client in the future:

# mkdir app/services
# echo > app/services/slack_client.rb
# app/services/slack_client.rb
module SlackClient
  def client
    Slack::Web::Client.new.tap(&:auth_test)
  rescue Slack::Web::Api::Errors::NotAuthed
    nil
  end

  module_function :client
end

# now you can use
SlackClient.client.chat_postMessage(channel: '#general', text: 'Hello World', as_user: true)

Send markdown #

The main difference between sending inline text and markdown is the inclusion of line breaks.

This can be accomplished with squiggly heredoc (<<~). This way you can have a string with \n line breaks:

def text
  <<~TEXT
    :alert: *#something happened*
    `code inline`
    and
    ```
    code block
    ```
    that's it!
    a link: https://blog.corsego.com
    a video: https://www.youtube.com/watch?v=dVbDkWbHX6M
  TEXT
end

# => ":alert: *#something happened*\n`code inline`\nand\n```\ncode block\n```\nthat's it!\na link: https://blog.corsego.com\na video: https://www.youtube.com/watch?v=dVbDkWbHX6M\n"

SlackClient.client.chat_postMessage(channel: '#general', text:, as_user: true)

Send a file #

Sending an image from assets:

filename = 'sample-image.png'
file_path = Rails.root.join('app', 'assets', 'images', filename).to_s
file = Faraday::UploadIO.new(file_path, 'image/png')
SlackClient.client.files_upload(
  channels: '#general',
  as_user: true,
  file:,
  title: 'file caption',
  filename:,
  initial_comment: 'normal text above file'
)

Example result:

slack-image-upload-example

To send a text file, you would use text/plain mime type:

file_to_upload = 'test.txt'
Faraday::UploadIO.new(file_to_upload, 'text/plain')

If your app generates a new file, that you want to send, you can:

  • save it to an external storage like S3 and attach form there (best option)
  • save it directly into your app and attach it from there

In the below example I:

# requires gem caxlsx
def export_daily_users_to_slack
  users_for_period = User.where(created_at: Date.today.all_day)
    .order(created_at: :desc)
  xlsx = ActionController::Base.new.render_to_string(
    layout: false,
    handlers: [:axlsx],
    formats: [:xlsx],
    template: 'csv/users',
    locals: { users: users_for_period }
  )
  title = [Time.now.to_s(:dmy), 'Users'].join(' ')
  filename = title.concat('.xlsx')
  folder_path = Rails.root.join('app', 'assets', 'csv', filename).to_s
  FileUtils.mkdir_p 'app/assets/csv'
  IO.binwrite(folder_path, xlsx.to_s)

  file = Faraday::UploadIO.new(folder_path, 'xlsx')

  SlackClient.client.files_upload(
    channels: '#general',
    as_user: true,
    file:,
    title:,
    filename:,
    initial_comment: 'users created in the last 24 hours'
  )
end

Example result:

slack-csv-import-example

Testing with rspec #

Test slack authentication, message delivery (considering you use rspec)

# spec/operations/reports/users_csv_report_spec.rb
require 'rails_helper'

RSpec.describe Reports::UsersCsvReport do
  subject(:service) { described_class.call }

  before do
    stub_request(:post, 'https://slack.com/api/auth.test').to_return(status: 200)
    stub_request(:post, 'https://slack.com/api/files.upload').to_return(status: 200)
    stub_request(:post, "https://slack.com/api/chat.postMessage").to_return(status: 200)
  end

  it 'calls slack api and logs event' do
    service
    expect(WebMock).to have_requested(:post, 'https://slack.com/api/files.upload')
    expect(WebMock).to have_requested(:post, "https://slack.com/api/chat.postMessage").with(body: "abc")
  end
end

That’s it!