You must enable javascript in order to use the Slack API Documentation. You can do this in your browser settings.
Go to Slack

Storing, retrieving, and modifying file uploads

By Sachin Ranchod

Have you ever wished that you could just upload a photo of a receipt to Slack to have it automatically attached to your expense report?

Maybe you want to upload a quote from a supplier, discuss it with your team and have it immediately linked to the vendor's profile in your procurement system?

With the Files API, you can build amazing integrations that allow you to quickly and easily interact with the files your users have on their phone or desktop.

To guide you through the process of using the Files API, we'll be building a simple app that takes images uploaded to Slack and uses a computer vision API to find (and draw red boxes) around every face in the image. We'll upload the marked up image and add reactions to it based on the subjects' emotions which have been determined using image sentiment analysis. Finally, if you mention the bot with one or more emotion emojis ( 😂,😡,😃,😭 ), we'll search the files we previously uploaded to the channel to find one that matches the set of emotions.

To build our Slack App, we'll be using Ruby's Sinatra framework together with Slack's Events API. We chose the Events API (over the RTM API) due to the simplicity of the inbound request model.

Getting started

The first thing we'll need to do is create a Slack App and enable Event subscriptions. In order to receive events from the Events API, you'll need an HTTPS endpoint which is set up to respond to the url verification challenge. We recommend using ngrok or LocalTunnel to tunnel your localhost through HTTPS.

post '/events' do
  request.body.rewind
  data = JSON.parse(request.body.read)

  if data["type"] == "url_verification"
    content_type :json
    return {challenge: data["challenge"]}.to_json
  end
end

Our app needs to respond to two events. First, the bot needs to notice when files are being shared so that it can scan images for faces. The bot also needs to listen to messages in channels so that it can respond when it is mentioned.

In order to get all the information we need, we'll subscribe to the following bot events:

Our app does not need to subscribe to the file_shared event because we'll receive that information as part of the message.* events. You'll want to subscribe to the file_shared if your app will only be responding to files being shared and not messages.

Save your client_id, client_secret and verification_token as environment variables. Finally, set your OAuth redirect URL to an /oauth endpoint on your localhost.

Add to Slack button

We'll be using OAuth and the Add to Slack button to manage the process of granting our app access to a Slack team. In order to receive bot events, we'll need to request the bot scope. To search files, we'll need to request the files:read scope from a user.

Our 'Add to Slack' button HTML looks as follows:

<a href="https://slack.com/oauth/authorize?scope=bot,files:read&client_id=#{ENV['SLACK_CLIENT_ID']">
<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x"/></a>

The /oauth endpoint exchanges the code it receives for a bot and user token. This information is saved in a database keyed on team_id.

get '/oauth' do
  if params['code']
    options = {
      client_id: ENV['SLACK_CLIENT_ID'],
      client_secret: ENV['SLACK_CLIENT_SECRET'],
      code: params['code']
    }

    res = RestClient.post 'https://slack.com/api/oauth.access',
                          options,
                          content_type: :json

    save_team(JSON.parse(res))
    erb :success
  else
    erb :failure
  end
end

Verifying incoming requests & load data

For every request coming in to our event subscription endpoint, we want to load up the saved authentication data (so that we can make requests with the correct tokens) and verify the verification token. When pulling up saved tokens, you'll want to first search by team_id then user so that you can select the most appropriate token to use.

post '/events' do
  request.body.rewind
  data = JSON.parse(request.body.read, object_class: OpenStruct)

  halt 500 if data.token != ENV['VERIFICATION_TOKEN']

  case data.type
  when "url_verification"
    content_type :json
    return {challenge: data.challenge}.to_json

  when "event_callback"
    event = data.event
    @team = find_team(data.team_id, event.user)

[...]

Catching shared files

When a file is shared, you'll either receive an event with a type of message (and a subtype of file_share) or file_shared depending on which events you've subscribed to. When we receive this event, we'll confirm that the image isn't being uploaded by our bot. This will stop us from creating an endless loop of uploaded images.

post '/events' do

  [...]

  when "event_callback"
    event = data.event
    @team = find_team(data.team_id, event.user)

    if event.subtype && event.subtype == "file_share"
      if @team && event.user != @team.bot["bot_user_id"]
        file = event.file
        fetch_and_compose_image(file, event.channel)
      end
    end
  end

  [...]

When sending you an event, Slack expects a 200 OK response within 3 seconds. If you're downloading and processing files, there's a good chance you won't be able to respond in time. This could put you in a position where you're handling the same event multiple times and unintentionally spamming your users. To prevent this from happening, we'd recommend responding to the request immediately and passing the processing of files to a background job.

Downloading files from Slack

When you receive a file shared event, you'll also get all the information we have about file object, including url_private which points to a URL to the file contents.

def fetch_and_compose_image(file, channel)

  filename = file.timestamp

  if file.filetype == "jpg"
    File.open("./tmp/#{filename}", 'wb') do |f|
      f << fetch_image(file.url_private)
    end

    fd = FaceDetection.new

    if fd.process_image
      file_id = upload(file, channel)
      add_reactions(file_id, fd)
    end
  end
end

To retrieve the file contents, you'll need to make a GET request to url_private and include an Authorization header ("Authorization": "Bearer xoxp-123456-abcdef"). The token used in the Authorization header needs to belong to a user who has access to the file.

def fetch_image(url)
  res = RestClient.get(url, { "Authorization" => "Bearer #{@team.access_token}" })

  if res.code == 200
    return res.body
  else
    raise 'Download failed'
  end
end

Upload file

Once we've downloaded the file and processed it, we'll upload the updated file to the channel, group or direct message where it was initially shared. To do this, we send a POST request to https://slack.com/api/files.upload which includes the file contents, the bot_access_token and the channels to share the file in.

def upload(file, channel)
  options = {
    token: @team.bot["bot_access_token"],
    file: File.new("./tmp/composed/#{file.timestamp}", 'rb'),
    filename: "composed_" + file.name,
    title: "Composed " + file.title,
    channels: channel
  }

  res = RestClient.post 'https://slack.com/api/files.upload', options
  # Return the uploaded file's ID
  JSON.parse(res.body)["file"]["id"]
end

Once the file has been successfully uploaded, we'll parse the response to get the ID of the newly uploaded file. This ID will let us interact with the uploaded file by adding reactions or comments.

Comment or react to the file

To add a reaction to the newly uploaded file, we cycle through the detected emotions (which have already been mapped to emojis) and send a request to https://slack.com/api/reactions.add for each. The payload includes the bot_access_token for the team, the file_id of our uploaded file and the emoji name (without colons).

def add_reactions(file_id, face_detection)
  face_detection.emotions.uniq.each do |emotion|
    options = {
  token: @team.bot["bot_access_token"],
  file: file_id,
  name: emotion
    }

    res = RestClient.post 'https://slack.com/api/reactions.add',
            options, content_type: :json
  end
end

Search files

Besides being able to mark up images, our bot also needs to be able to search uploaded images within a channel by emotion and share a random result. To do this, we'll listen for mentions of our bot's name and see if the message sent to us includes any of the emotion emojis we understand.

post '/events' do

  [...]

  when "event_callback"
    if event.text && event.text.match(/^<@#{@team.bot["bot_user_id"]}>/)
      arr = FaceDetection::Emotions.values.select do |emotion|
        event.text.include?(":#{emotion}:")
      end
      unless arr.empty? || already_processing?(event.ts) do
        find_image_with_emotion(arr, event.channel)
      end
    end

  [...]

Once we've parsed the message and extracted the emotion emojis, we'll search the files within the channel by sending a request to https://slack.com/api/files.list. This endpoint expects a user's access token and will not work with a bot access token. We can limit the search to just our bot's uploaded files by passing the bot_user_id to the user parameter. Finally, we limit the files we're searching for to images in the current channel (by passing the channel id to the channel parameter and images to type).

def find_image_with_emotion(array_of_emoji, channel)
  options = {
    token: @team.access_token,
    channel: channel,
    user: @team.bot["bot_user_id"],
    types: "images"
  }

  res = RestClient.post 'https://slack.com/api/files.list', options, content_type: :json

  body = JSON.parse(res.body, object_class: OpenStruct)
  files = body.files

  found_files = []

  files.each do |file|
    found_files << file if bot_reacted(file, array_of_emoji)
  end

  rand_file = found_files.sample
  share_file(rand_file) if rand_file
end

Share an uploaded file

To share the uploaded file in the channel, we use the https://slack.com/api/chat.postMessage method. The text of the method is the file's url_private value. The message should by posted using the bot_access_token and as_user set to true so that the image unfurls correctly.

def share_file(file, channel)
  options = {
    token: @team.bot["bot_access_token"],
    channel: channel,
    text: file.url_private,
    as_user: true
  }

  res = RestClient.post 'https://slack.com/api/chat.postMessage', 
  options, content_type: :json
end

Pulling it all together

Finding faces in images

Related documentation