What I Learned at Work this Week: Python Slack API

Mike Diaz
5 min readNov 13, 2021

--

Last week, I mentioned working on a Python script that integrates with Slack. Our users run the script by entering commands in a Slack channel and they get responses in that channel. I appreciate working on something that already has an integration set up because I can see how it’s done without having to blaze a path myself. This week, however, I’m being asked to add new functionality to the script.

We want our program to not just send text to the Slack channel, but to also send a CSV file to that channel when prompted. To attempt this, I had to learn how we’re integrating with Slack in the first place. That’s how I ended up in the python-slackclient docs.

Basic Usage

I’ll admit that before I actually read the docs, I tried to pattern-match and hoped that I’d get lucky with a functional implementation. Needless to say, that didn’t work, so I figured I’d have to better understand this API before trying to do something complicated with it. One benefit to blogging on the weekends is that I don’t have to feel like I’m wasting precious time if I have to start reading the docs slowly from the very beginning (though actually we shouldn’t feel bad about doing this on the clock either — it’s absolutely part of the job).

The docs begin with a basic example that uses Python to send a message through Slack:

import logging
logging.basicConfig(level=logging.DEBUG)

import os
from slack import WebClient
from slack.errors import SlackApiError

slack_token = os.environ["SLACK_API_TOKEN"]
client = WebClient(token=slack_token)

try:
response = client.chat_postMessage(
channel="C0XXXXXX",
text="Hello from your app! :tada:"
)
except SlackApiError as e:
# You will get a SlackApiError if "ok" is False
assert e.response["error"] # str like 'invalid_auth', 'channel_not_found'

The example imports the logging module and sets the level as logging.DEBUG. This would allow us to produce logs at different points in our script, but the example doesn’t actually take advantage of that. It adds three other imports:

  • os: The operating system module will give us access to details about where our script is being run. We’ll use it to pull our OAuth token, which we’ll need to get our API working.
  • slack.WebClient: WebClient is a method within the slack module that accepts an OAuth token and returns an object that can call Slack API methods. One method, which we’ll see shortly, sends a message to a Slack channel.
  • slack.SlackApiError: A method used for error handling, which of course is a best practice.

We see the imports used as predicted: os pulls our OAuth token from the local environment, WebClient uses the token to create an API client, and that client calls the chat_postMessage method. That method accepts two arguments: a Slack channel name, and the text we want to send.

We use except to handle the potential error that we’ve attempted to send a message to a channel that does not exist. This seems really straightforward, so hopefully uploading a file is just as easy.

Files Upload

Once again, our documentation provides a helpful sample:

# The name of the file you're going to upload
file_name = "./myFileName.gif"
# ID of channel that you want to upload file to
channel_id = "C12345"
try:
# Call the files.upload method using the WebClient
# Uploading files requires the `files:write` scope
result = client.files_upload(
channels=channel_id,
initial_comment="Here's my file :smile:",
file=file_name,
)
# Log the result
logger.info(result)
except SlackApiError as e:
logger.error("Error uploading file: {}".format(e))

This seems just as straightforward. Inside of a try, we run the files_upload method, providing arguments for channels, initial_comment, and file (the full list of possible arguments can be found here. One thing that was made clearer in this example was that we’re not looking for a channel name, but a channel ID. We also get to see logger at work here, creating a response for a success or for an error. Remember that the file name is a path, which is why it starts with ./.

So this should be no sweat to implement, right? If I were building an app from scratch, yes. But I’m iterating on an existing script; a script that was written years ago. I wonder what version of the Slack API we’re currently using?

Okay.

And what version is the documentation reflecting?

Oh no.

That’s not to say that the syntax I saw definitely won’t work, but it’s a long shot. One way to check is to see how the API is implemented in other parts of the code. Here’s an example where we send a message:

sc.api_call(
"chat.postMessage",
channel=channel,
text=text,
)

sc is an instantiation of the Slack client, so that’s the same. But instead of using a specific method, we call api_call and pass the action type as an argument. We’re going to have to figure out how this used to be done.

Changelog

The screenshot I took above reflects the changelog published by Slack for this API. That log documents all of the updates contained in each new version of the product, from 1.2.1 to today. I started browsing it and saw that the switch from 1.3.1 to 2.0.0 was responsible for the addition of the new methods (a big change, hence the update to the major version number). This section of the doc included migration instructions that showed both old and new syntax:

# Before:
# from slackclient import SlackClient
#
# client = SlackClient(os.environ["SLACK_API_TOKEN"])
# client.api_call('chat.postMessage',
# timeout=30,
# channel='C0123456',
# text="Hi!")
# After:import slackclient = slack.WebClient(os.environ["SLACK_API_TOKEN"], timeout=30)
client.api_call('chat.postMessage', json={
'channel': 'C0123456',
'text': 'Hi!'})
# Note: That while the above is allowed, the more efficient way to call that API is like this:
client.chat_postMessage(
channel='C0123456',
text='Hi!')

This leaves me with two options: I can try to migrate this whole app to a new version (more work, but probably something we’ll want to do eventually), or I can keep digging to find an example that uses files_upload. I’d like to focus on the problem at hand, so I’ll save the migration for the future. In order to find old examples for this API, I ran a Google search for slack “api_call” file upload and set the date range from 1/1/2018–3/31/2019, since version 2.0.0 was released in April 2019. I found this Stack Overflow question, which had sample code that was successfully uploading a file:

def post_image_to_channel(self, channel_name, filepath, tile='Test Upload'):
sc = SlackClient(TOKEN)
with open(filepath, 'rb') as file_content:
sc.api_call(
"files.upload",
channels=channel_id,
file=file_content,
title=tile,
username='mybot',
)

This was a little different than the syntax I had used, so I have a lead to go back and use for testing. I decided to stop there because the weekends are for learning (and relaxing) and the weekdays, from 9–6, are for work-related coding. I’ve already got a head start on my work, as well as more confidence for using a new module.

Sources

--

--

Mike Diaz
Mike Diaz

No responses yet