Bots are a useful way to interact with chat services such asSlack. If you have never built a bot before, thispost provides an easy starter tutorial for combining theSlack API with Python to create your first bot.
We will walk through setting up your development environment, obtaining aSlack API bot token and coding our simple bot in Python.
Our bot, which we will name "StarterBot", requires Python and the Slack API.To run our Python code we need:
It is also useful to have theSlack API docs handywhile you're building this tutorial.
All the code for this tutorial is available open source under the MIT licensein theslack-starterbot publicrepository.
We now know what tools we need for our project so let's get our developmentenvironment set up. Go to the terminal (or Command Prompt on Windows) andchange into the directory where you want to store this project. Withinthat directory, create a new virtualenv to isolate our applicationdependencies from other Python projects.
virtualenv starterbot
Activate the virtualenv:
source starterbot/bin/activate
Your prompt should now look like the one in this screenshot.
The officialslackclient
API helper library built by Slack can send andreceive messages from a Slack channel. Install the slackclient library withthepip
command:
pip install slackclient==1.3.2
Whenpip
is finished you should see output like this and you'll beback at the prompt.
We also need tocreate a Slack App to recievean API token for your bot. Use "Starter Bot" as your App name. If you are signedinto more than one workspace, pick a Development Workspace from the dropdown.
After submitting the form, keep the app configuration page open.
We want our Starter Bot to appear like any other user in your team - it willparticipate in conversations inside channels, groups, and DMs. In a SlackApp, this is called abot user, whichwe set up by choosing "Bot Users" under the "Features" section. Afterclicking "Add a Bot User", you should choose a display name, choose adefault username, and save your choices by clicking "Add Bot User". You'llend up with a page that looks like the following:
Theslackclient
library makes it simple to use Slack'sRTM API andWeb API.We'll use both to implement Starter Bot, and they each require authentication.Conveniently, the bot user we created earlier can be used to authenticate forboth APIs.
Click on the "Install App" under the "Settings" section. The button on this pagewill install the App into our Development Workspace. Once the App is installed,it displays abot user oauth access token for authentication as the bot user.
A common practice for Python developers is to export secret tokens asenvironment variables. Back in your terminal, export the Slack token with thenameSLACK_BOT_TOKEN
:
export SLACK_BOT_TOKEN='your bot user access token here'
Nice, now we are authorized to use the Slack RTM and Web APIs as a bot user.
We've got everything we need to write the Starter Bot code. Create a new filenamedstarterbot.py
and include the following code in it.
import osimport timeimport refrom slackclient import SlackClient
With our dependencies imported we can use them to obtain the environmentvariable values and then instantiate the Slack client.
#instantiateSlackclientslack_client=SlackClient(os.environ.get('SLACK_BOT_TOKEN'))#starterbot'suserIDinSlack:valueisassignedafterthebotstartsupstarterbot_id=None#constantsRTM_READ_DELAY=1#1seconddelaybetweenreadingfromRTMEXAMPLE_COMMAND="do"MENTION_REGEX="^<@(|[WU].+?)>(.*)"
The code instantiates theSlackClient
client with ourSLACK_BOT_TOKEN
exported as an environment variable. It also declares a variable we can use tostore the Slack user ID of our Starter Bot. A few constants are also declared,and each of them will be explained as they are used in the code that follows.
if__name__ == "__main__": ifslack_client.rtm_connect(with_team_state=False):print("Starter Bot connected and running!") #Readbot's user ID by calling Web API method `auth.test`starterbot_id =slack_client.api_call("auth.test")["user_id"] whileTrue:command,channel =parse_bot_commands(slack_client.rtm_read()) ifcommand:handle_command(command,channel)time.sleep(RTM_READ_DELAY) else:print("Connection failed. Exception traceback printed above.")
The Slack client connects to the Slack RTM API. Once it's connected, it calls aWeb API method (auth.test
) to findStarter Bot's user ID.
Each bot user has a user ID for each workspace the Slack App is installedwithin. Storing this user ID will help the program understand if someone hasmentioned the bot in a message.
Next, the program enters an infinite loop, where each time the loop runs theclient recieves any events that arrived from Slack's RTM API. Notice thatbefore the loop ends, the program pauses for one second so that it doesn't looptoo fast and waste your CPU time.
For each event that is read, theparse_bot_commands()
function determines ifthe event contains a command for Starter Bot. If it does, thencommand
willcontain a value and thehandle_command()
function determines whatto do with the command.
We've laid the groundwork for processing Slack events and calling Slack methodsin the program. Next, add three new functions above the previous snippet tocomplete handling commands:
defparse_bot_commands(slack_events): """ParsesalistofeventscomingfromtheSlackRTMAPItofindbotcommands. Ifabotcommandisfound,thisfunctionreturnsatupleofcommandandchannel. Ifitsnotfound, thenthisfunctionreturnsNone,None. """ foreventinslack_events: ifevent["type"] == "message"andnot "subtype"inevent:user_id,message =parse_direct_mention(event["text"]) ifuser_id ==starterbot_id: returnmessage,event["channel"] returnNone,Nonedefparse_direct_mention(message_text): """Findsadirectmention(amentionthatisatthebeginning)inmessagetextandreturnstheuserIDwhichwasmentioned. Ifthereisnodirectmention,returnsNone """matches =re.search(MENTION_REGEX,message_text) #thefirstgroupcontainstheusername,thesecondgroupcontainstheremainingmessage return(matches.group(1),matches.group(2).strip()) ifmatches else(None,None)defhandle_command(command,channel): """Executesbotcommand ifthecommandisknown """ #Defaultresponseishelptext fortheuserdefault_response = "Not sure what you mean. Try *{}*.".format(EXAMPLE_COMMAND) #Findsandexecutesthegivencommand,fillinginresponseresponse =None #Thisiswhereyoustarttoimplementmorecommands! ifcommand.startswith(EXAMPLE_COMMAND):response = "Sure...write some more code then I can do that!" #Sendstheresponsebacktothechannelslack_client.api_call( "chat.postMessage",channel=channel,text=responseordefault_response)
Theparse_bot_commands()
function takes events from Slack and determinesif they are commands directed at Starter Bot. There are manyevent types that our bot will encounter, but tofind commands we only want to considermessage events. Message events also havesubtypes, but the commands we want to find won't have any subtype defined. Thefunction filters out uninteresting events by checking these properties. Now weknow the event represents a message with some text, but we want to find outif Starter Bot is being mentioned in the text. Theparse_direct_mention()
function will figure out of the message text starts with a mention, and thenwe compare that to the user ID we stored earlier for Starter Bot. If they arethe same, then we know this is a bot command, and return the command text withthe channel ID.
Theparse_direct_mentions()
function uses a regular expression to determineif a user is being mentionedat the beginning of the message. It returnsthe user ID and the remaining message (andNone, None
if no mention wasfound).
The last function,handle_command()
is where in the future you'll add all theinteresting commands, humor, and personality for Starter Bot. For now, it hasjust one example command:do. If the command starts with a known command, itwill have an appropriate response. If not, a default response is used. Theresponse is sent back to Slack by calling thechat.postMessage
Web APImethod with the channel.
Here is how the entire program should look when it's all put together(you can alsoview the file in GitHub):
import osimport timeimport refrom slackclient import SlackClient# instantiate Slack clientslack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))# starterbot's user ID in Slack: value is assigned after the bot starts upstarterbot_id =None# constantsRTM_READ_DELAY = 1 # 1 second delay between reading from RTMEXAMPLE_COMMAND = "do"MENTION_REGEX ="^<@(|[WU].+?)>(.*)"def parse_bot_commands(slack_events): """ Parses a list of events coming from the Slack RTM API to find bot commands. If a bot command is found, this function returns a tuple of command and channel. If its not found, then this function returns None, None. """ for event in slack_events: if event["type"] == "message" and not "subtype" in event: user_id, message = parse_direct_mention(event["text"]) if user_id == starterbot_id: return message, event["channel"] returnNone,Nonedef parse_direct_mention(message_text): """ Finds a direct mention (a mention that is at the beginning) in message text and returns the user ID which was mentioned. If there is no direct mention, returns None """ matches = re.search(MENTION_REGEX, message_text) # the first group contains the username, the second group contains the remaining message return (matches.group(1), matches.group(2).strip()) if matches else (None,None)def handle_command(command, channel): """ Executes bot command if the command is known """ # Default response is help text for the user default_response = "Not sure what you mean. Try *{}*.".format(EXAMPLE_COMMAND) # Finds and executes the given command, filling in response response =None # This is where you start to implement more commands! if command.startswith(EXAMPLE_COMMAND): response = "Sure...write some more code then I can do that!" # Sends the response back to the channel slack_client.api_call( "chat.postMessage", channel=channel, text=response or default_response )if __name__ == "__main__": if slack_client.rtm_connect(with_team_state=False): print("Starter Bot connected and running!") # Read bot's user ID by calling Web API method `auth.test` starterbot_id = slack_client.api_call("auth.test")["user_id"] whileTrue: command, channel = parse_bot_commands(slack_client.rtm_read()) if command: handle_command(command, channel) time.sleep(RTM_READ_DELAY) else: print("Connection failed. Exception traceback printed above.")
Now that all of our code is in place we can run our Starter Bot on thecommand line with thepython starterbot.py
command.
In Slack, create a new channel and invite Starter Bot or invite it to anexisting channel.
Now start giving Starter Bot commands in your channel.
Additional Note: Currently there's anissue with thewebsocket
package and the CA certificate it uses, so if you encounter an error like:
...ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1045)...slackclient.server.SlackConnectionError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1045)Connection failed. Exception traceback printed above.
There are a couple of things that can be done:1. Downgrading the websocket-client library to0.47.0
2. Or, download the certificate (wget https://www.tbs-certificats.com/issuerdata/DigiCertGlobalRootCA.crt
), then set the environment variableexport WEBSOCKET_CLIENT_CA_BUNDLE=DigiCertGlobalRootCA.crt
Alright, now you've got a simple Starter Bot with a bunch of places in thecode you can add whatever features you want to build.
There is a whole lot more that could be done using the Slack RTM API and Python.Check out these posts to learn what you could do:
Questions? Contact me via Twitter@fullstackpythonor@mattmakai. I'm also on GitHub withthe usernamemattmakai.
See something wrong in this post? Forkthis page's source on GitHuband submit a pull request.