A Guide to Building a Multi-featured Slackbot with Python

Chatbots are being used almost everywhere today from social messaging platforms to integration into websites for booking tickets, finding nearby restaurants, generating leads, buying and selling products. Some chatbots, like Ruuh by Microsoft have been able to deliver human-like conversations using artificial intelligence and deep learning.

Do you remember Natasha from Hike? I used it 4 years ago and was amazed to see how she handled our conversations which were so much better than a bot could possibly handle. I hadn’t heard the concept of machine learning back then.

Now chatbots have made us so dependent on them that it has become a part of our lives today.

“Ok Google, remind me on 20th to publish my chatbot article”

“What time?”

“Midnight”

“Sure, I’ll remind you on Wednesday at midnight.”

Chatbots are not only making our lives easier by managing our tasks but they are also becoming quite interesting to have conversations with.

“Ok Google, tell me a joke.”

“Here’s a joke just in time for Valentine’s Day, I forgot to tell you I have a date for a Valentine’s Day. It’s February 14th.”

But all these come at a cost of our data being stored and used for the company’s benefits. Recently Google India tweeted asking ‘why do Indian users keep asking Google Assistant to marry them?

Can we do anything about this? Most Probably No.

What if we could build our own chatbot?

We could add all the features we need and tweak them as per our likings.

So let’s build a chatbot which will help you get more productive at work as it runs on Slack. The chatbot we are about to build is in no way near to the Google Assistant. It is not even voice-enabled.

Slack is a messaging platform built for teams to collaborate and work with each other. It is the most common tool used in companies today for communicating with their employees.


**You might also enjoy: **How to build a SlackBot with Node.js and SlackBots.js


Getting Started

Let’s build a chatbot together on Slack.

DISCLAIMER: This project was created by a team of 2 for a competition but unfortunately we couldn’t make it to the finals.

This is our Architecture for our Slackbot.

And this is our Entity Relationship Diagram which will help you create your own database.

Clone the repository from GitHub.

Create a .env file in the /src directory of your project.

Install the requirements:

pip install -r requirements.txt

This is your main file: slackbot.py.

import time
from slackclient import SlackClient
from lyrics_extractor import Song_Lyrics
from get_music import music
from task import parse_tasks
from reminder import parse_reminders
from football import Football_res
from health import health_tips
from help_info import user_help
from news import get_news
import settings
import requests
import json

# instantiate Slack client
slack_client = SlackClient(settings.SECRET_KEY)

# delay between reading from RTM
RTM_READ_DELAY = 1
COMMANDS = ['help', 'drink water', "quotes", "facts", "news", "live cricket", "live football", "health tips", "no quotes", "no facts", "no water", "no health tips", "no news"]
COMP_SUBS = ['premier league', 'championship', 'serie a', 'primeira liga', 'la liga']
foot_res = Football_res(None, None)

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:
            message = event["text"].lower()

            # Splitting user's message for comp_names
            check = set(map(lambda x: x.strip(), message.split(',')))
            # checking if message/s are in comp_names 
            result =  all(elem in COMP_SUBS for elem in check)
            if result:
                foot_res.football_response(message, check, event["channel"])
            else:
                return event["channel"], message.strip()
    return None, None

def handle_command(channel, message):
    """
        Executes bot command if the command is known
    """

    response = None
    # This is where you start to implement more commands!
    if message in COMMANDS or message.startswith("listen ") or message.startswith("lyrics for ") or message.startswith("remind me on "):
        if message == 'drink water':
            response = "Yay... You are going in the right way. Now you will be reminded to drink a glass of water in every 2 hours.\n\nTo deactivate from drink water notifications, type *No water*."

        elif message == 'no water':
            response = "Eh! You are leaving a good habit to stay fit or may be you don't need reminders any more. But please make sure to drink at least 8-10 glasses of water per day.\n\nTo reactivate drink water notifications, type *Drink water*."

        elif message == 'help':
            response = user_help()

        elif message == "quotes":
            quote = requests.get("http://quotes.rest/qod.json?category=inspire")
            data = quote.json()
            quote = data["contents"]["quotes"][0]["quote"]
            author = data["contents"]["quotes"][0]["author"]
            response = "Cool! You are now subscribed for receiving Daily Inspiring Quotes.\n\nTo deactivate from Daily Inspiring Quotes, type *No quotes*.\n\n" 
            response += "*Quote of the day.*\n\n" + quote + "\n - _*" + author + "*_"

        elif message == 'no quotes':
            response = "Eh! You are unsubscribed from receiving Daily Inspiring Quotes. But always try to stay motivated to reach your goals as this tool can make you achieve anything you aspire in your life.\n\nTo reactivate Daily Inspiring Quotes, type *quotes*."

        elif message == 'facts':
            fact = requests.get("http://randomuselessfact.appspot.com/random.json?language=en")
            data = fact.json()
            fact = data["text"]
            response = "Cool! You are now subscribed to Random Facts.\n\nTo deactivate from random facts, type *No facts*.\n\n" + "*Did you know?*\n\n" + fact

        elif message == 'no facts':
            response = 'Eh! You are unsubscribed from Random Facts. But make sure to keep learning and exploring things around you and never let your curiousity die in you.\n\nTo reactivate Random Facts, type *facts*.'

        elif message == "news":
            response = "Cool! You are now subscribed to latest news.\n\nTo deactivate from latest national news, type *No news*.\n\n"
            response += '*Current Affairs*\n\n' + get_news()

        elif message == "no news":
            response = "Eh! You are unsubscribed from latest National news. But make sure to keep yourself updated with the latest current affairs.\n\nTo reactivate latest news, type *news*."

        elif message == "health tips":
            response = "Cool! You are now subscribed to Health Tips.\n\nTo deactivate from health tips, type *No health tips*.\n\n"
            health = health_tips()
            response += "*Today's health tips is about " + health[0] + "*\n\n*_" + health[1] + "_*\n" + health[2]

        elif message == "no health tips":
            response = "Eh! You are unsubscribed from Health Tips. But make sure to stay fit and try to follow all the tips you have received till now.\n\nTo reactivate Health Tips, type *health tips*."

        elif message == "live football":
            response = "We provide live scores for *Premier League*, *Championship*, *Serie A*, *Primeira Liga*, *La Liga*. Which of these would you like to subscribe to receive match updates?\n\nYou can select mutiple competitions by seperating each of them with commas(,)."
            # f_res = threading.Thread(target=football_response, args=(channel, slack_client))
            # f_res.start()
            global foot_res
            foot_res = Football_res(channel, slack_client)

        elif message == "live cricket":
            response = "Subscribed for Cricket live scores."

        elif message.startswith("listen "):
            get_music_name = message[7:]
            store_music = music(get_music_name)
            response = "*" + store_music[0] + "*\n\nAudio Link: " + store_music[1] + "\n\nVideo Link: " + store_music[2]

        elif message.startswith("lyrics for "):
            get_song_name = message[11:]
            lyrics_gen = Song_Lyrics(settings.GCS_API_KEY, settings.GCS_ENGINE_ID)
            song = lyrics_gen.get_lyrics(get_song_name)
            response = '*' + song[0] + '*' + '\n\n' + song[1].replace('<br>','\n')

        # This will execute for tasks which will be scheduled for only once.
        elif message.startswith('remind me on ') and 'at ' in message:
            store_task_vals = parse_tasks(message)
            if len(store_task_vals) == 1:
                # if error, pass the error response to the user.
                response = store_task_vals[0]
            else:
                # Save store_tasks_vals to our db and notifies the user.
                response = 'All set. Your task is scheduled on ' + store_task_vals[0] + ' at ' + store_task_vals[2] + ' hrs.'

        # This will execute for reminders which will be scheduled for every year.
        elif message.startswith('remind me on '):
            store_reminder_vals = parse_reminders(message)
            if len(store_reminder_vals) == 1:
                # if error, pass the error response to the user.
                response = store_reminder_vals[0]
            else:
                # Save store_tasks_vals to our db and notifies the user.
                response = 'Reminder set. Now you will be reminded on ' + store_reminder_vals[0] + ' every year for ' + store_reminder_vals[1] + '\'s ' + store_reminder_vals[2]

        # Sends the response back to the channel
        slack_client.api_call(
            "chat.postMessage",
            channel = channel,
            text = 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"]
        while True:
            try:
                channel, message = parse_bot_commands(slack_client.rtm_read())
                if message:
                    handle_command(channel, message)
                    # h_cmd = threading.Thread(target=handle_command, args=(channel, message))
                    # h_cmd.start()
                time.sleep(RTM_READ_DELAY)
            except Exception as e:
                print("Reconnecting..." + str(e))
                slack_client.rtm_connect(with_team_state=False)
    else:
        print("Connection failed. Exception traceback printed above.")

slackbot.py firstimports all the packages required to run the Slackbot. It then initiates the slack client using your Slack API key stored in your .env file like this:

API_KEY=”Your Slack API Key”

It initializes the constants and tries to connect with the Slack’s RTM API and if it fails to establish a connection then it returns Connection failed with the error message printed above.

If the connection is successful, our slack client runs in an infinite loop and tries to read every second if any user’s message is received and if it receives any message, it extracts the channel id and the message text received from the Slack’s RTM API and further checks if the message received has any assigned command which can be processed to generate a response.

Features with code and Explanation

Song Lyrics

Our users can get lyrics for songs by passing in spelled or misspelled song names right from the Slackbot. This code snippet has already been defined in your slackbot.py file.

elif message.startswith("lyrics for "):
get_song_name = message[11:]
lyrics_gen = Song_Lyrics(settings.GCS_API_KEY, settings.GCS_ENGINE_ID)
song = lyrics_gen.get_lyrics(get_song_name)
response = '*' + song[0] + '*' + '\n\n' + song[1].replace('<br>','\n')

You need to create a Custom Search Engine ID by adding any or all of the following websites as per your choice:

Note: For more information, you may look at the Lyrics Extractor Python Library.

After you get your Custom Search Engine ID, get a Google Custom Search JSONAPI key and you are good to go.

Get Audio & Video for a song

Our users can get audio and video version for songs by passing in spelled or misspelled song names right on their Slackbot.

This is your get_music.py.

import pafy
import requests
from bs4 import BeautifulSoup
try:
    import bitly_api
except ModuleNotFoundError:
    raise ModuleNotFoundError("Please read this article to install Bitly_api: https://www.geeksforgeeks.org/python-how-to-shorten-long-urls-using-bitly-api/")
import settings

def music(song_name):
    """
        Takes song_name as an argument.
        Formats the song name and passes it to Youtube Data API.
        It then extracts teh video id and title from the first Youtube result.
        The video is converted into an Audio with Medium Bitrate settings.
        Audio and Video is formatted and shortened using Bitly API.
        Returns title, audio link and video link as a tuple.
    """

    API_USER = settings.BITLY_API_USER
    API_KEY = settings.BITLY_API_KEY

    b = bitly_api.Connection(API_USER, API_KEY)

    song_name = song_name + " song"

    url = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=" + song_name + \
        "&key=" + settings.YTDATA_API_KEY + "&maxResults=1&type=video"
    page = requests.get(url)
    data = page.json()
    sear = data["items"][0]["id"]["videoId"]
    title = data["items"][0]["snippet"]["title"]

    myaud = pafy.new(sear)
    genlink = myaud.audiostreams[2].url
    vlink = "https://www.youtube.com/watch?v=" + sear

    flink = b.shorten(uri=genlink)
    flink = flink["url"]
    vlink = b.shorten(uri=vlink)
    vlink = vlink["url"]

    return (title, flink, vlink)

# print(music("tere sang yaara"))

After importing all the dependencies, It requires a YouTube Data API to fetch songs and extracts the first link from the search results received for spelled or misspelled song names.

Note: We have assumed our first YouTube search result to be the most accurate one for our users requesting for songs.

It then makes use of Pafy Python Library to extract the audio from the video link of the song. It requires a Bitly username and Bitly API key to shorten long URLs generated for streaming audio which expires within a few hours as well as shortens the YouTube video link for providing the video version of the song.

Note: You may find this article useful to install the Bitly Package from GitHub.

Live Scores for Football

Our users get notified about the latest scores for live football matches after every set time intervals. I have only selected top football leagues which fetch live matches for Premier League, Championship, Serie A, Primeira Liga, La Liga.

I selected only a few leagues as there are numerous matches live at the moment and sending scores for all the live matches would make no sense to the users.

Note: This is a subscription-based service so you need to set up a Database as per my shared schema at the beginning. You can then use a Schedule Python Library to schedule your live scores to be sent to the subscribed users after every set time interval.

You need to get the API key of Football Data API.

You can choose your favorite football leagues from the leagues offered in the Football Data API.

Here is your football.py file.

import http.client
import json
import time
import settings

def live_football(comp_set):
    """
        Takes arg as set containing competition names.
        Converts names into id using dict.
        Fetches live football scores of all the live matches for arg provided.
        It only fetches live matches for Premier League, Championship, Serie A, Primeira Liga, La Liga.
        Extracts required information and stores in a tuple.
        Stores each tuple as separate matches in a list.
        Returns list of tuples if API contains any ongoing matches, containing the desired info of filtered matches.
        Else returns None.
    """

    comp_id = {'premier league': '2021', 
                'championship': '2016', 
                'serie a': '2019', 
                'primeira liga': '2017', 
                'la liga': '2140'}

    id = ''
    for name in comp_set:
        id += comp_id[name] + ','

    connection = http.client.HTTPConnection('api.football-data.org')
    headers = { 'X-Auth-Token': settings.FTLIVE_AUTH_TOKEN }
    connection.request('GET', '/v2/matches?status=LIVE&competitions=' + id[:-1], None, headers )
    response = json.loads(connection.getresponse().read().decode())

    count = response["count"]
    if count > 0:
        capture_football_live = []
        for c in range(count):
            competition = response["matches"][c]["competition"]["name"]
            home_team = response["matches"][c]["homeTeam"]["name"]
            away_team = response["matches"][c]["awayTeam"]["name"]

            fulltime_score_hometeam = str(response["matches"][c]["score"]["fullTime"]["homeTeam"] or 'NA')
            fulltime_score_awayteam = str(response["matches"][c]["score"]["fullTime"]["awayTeam"] or 'NA')

            halftime_score_hometeam = str(response["matches"][c]["score"]["halfTime"]["homeTeam"] or 'NA')
            halftime_score_awayteam = str(response["matches"][c]["score"]["halfTime"]["awayTeam"] or 'NA')

            extratime_score_hometeam = str(response["matches"][c]["score"]["extraTime"]["homeTeam"] or 'NA')
            extratime_score_awayteam = str(response["matches"][c]["score"]["extraTime"]["awayTeam"] or 'NA')

            penalties_hometeam = str(response["matches"][c]["score"]["penalties"]["homeTeam"] or 'NA')
            penalties_awayteam = str(response["matches"][c]["score"]["penalties"]["awayTeam"] or 'NA')

            winning_team = response["matches"][c]["score"]["winner"] or 'NA'
            if winning_team == 'HOME_TEAM':
                winning_team = home_team
            elif winning_team == 'AWAY_TEAM':
                winning_team = away_team

            capture_football_live.append((competition, home_team, away_team, halftime_score_hometeam, halftime_score_awayteam, fulltime_score_hometeam, fulltime_score_awayteam, extratime_score_hometeam, extratime_score_awayteam, penalties_hometeam, penalties_awayteam, winning_team))

        return capture_football_live
    return None

class Football_res():
    """
        Stores sub_channel and slack_client passed in from live football cmd.

        When the user types in further response selecting his competition then the function in it gets executed taking in set and channel as its args.

        It then fetches live scores calling another function passing in set of competition names required to be fetched for live football matches.

        Sends the message back to the user confirming about his subscription, if the channel the message came from is same as channel who subscribed for live football scores.

        Returns None.
    """

    def __init__(self, sub_channel, slack_client):
        self.sub_channel = sub_channel
        self.slack_client = slack_client

    def football_response(self, message, comp_set, channel):
        # we will save in our db list of comp of message as a list
        football_live = live_football(comp_set)
        # comp_subs = ['premier league', 'championship', 'serie a', 'primeira liga', 'la liga']

        if self.sub_channel == channel:
            if football_live != None:
                response = "You are now subscribed to live scores for football and you will now get notified when any football matches goes live for " + message.title() + ".\n\n*Live Football Scores*"
                for match in football_live:
                    response += "\n\n______________________________\n\n*" + match[0] + "*\n\n_*"\
                                + match[1] + ' V/s ' + match[2] + '*_\n\n*Half Time:* '\
                                + match[3] + ' | ' + match[4] + '\n\n*Full Time:* '\
                                + match[5] + ' | ' + match[6] + '\n\n*Extra Time:* '\
                                + match[7] + ' | ' + match[8] + '\n\n*Penalties:* '\
                                + match[9] + ' | ' + match[10] + '\n\n*Winner:* '\
                                + match[11]

                self.slack_client.api_call(
                "chat.postMessage",
                channel = channel,
                text = response,
                )

            else:
                response = 'No matches are live right now for ' + message.title() + '. But you will now get notified when any football matches goes live for ' + message.title() + '.'
                self.slack_client.api_call(
                "chat.postMessage",
                channel = channel,
                text = response,
                )

            self.sub_channel = None
        return None

The live_football function fetches and extracts the live scores for the live football matches of the selected leagues stored in comp_id dictionary and returns a list of tuples for the live matches with the required information of both the teams.

When the user subscribes for live football scores from the Slackbot, our football_res class object stores the user’s channel id and our slack client API key which further verifies the user’s response and stores the selected leagues by the user in our Database and sends the follow-up confirmation response to the subscribed user with the latest scores for the live matches.

News

Our users will be updated about current affairs and breaking news daily so that they can be up-to-date about the current happenings.

This is your news.py file.

try:
    import bitly_api
except ModuleNotFoundError:
    raise ModuleNotFoundError("Please read this article to install Bitly_api: https://www.geeksforgeeks.org/python-how-to-shorten-long-urls-using-bitly-api/")
import requests
import settings

def get_news():
    """
        Fetches latest news from News API.
        Collects the top 5 news from google news.
        Shortens news links using Bitly APIs.
        Returns Aggregated news.
    """

    API_USER = settings.BITLY_API_USER
    API_KEY = settings.BITLY_API_KEY

    bitly_conn = bitly_api.Connection(API_USER, API_KEY)
    url = "https://newsapi.org/v2/top-headlines?sources=google-news-in&apiKey=" + settings.NEWS_API_KEY

    news = requests.get(url)
    data = news.json()

    get_news = ''
    for i in range(5):
        title = data["articles"][i]["title"]
        description = data["articles"][i]["description"]
        link = data["articles"][i]["url"]
        flink = bitly_conn.shorten(uri=link)
        flink = flink["url"]
        get_news += "*" + title + " :* _" + description + "_ \n" + flink + "\n\n"

    return get_news

# print(news())

It requires a News API key to fetch the latest news and a Bitly username and Bitly API key to shorten long URLs.

Note: You may find this article useful to install the Bitly Package from GitHub.

It returns the News with the title, description, and the news link as a formatted message.

Note: This is a subscription-based service so you need to set up a Database as per my shared schema at the beginning. You can then use a Schedule Python Library to schedule your live scores to be sent to the subscribed users after every set time interval.

Tasks

Our users can schedule tasks in the Slackbot and it will remind them for the set task at the set date and time. This will help our users manage and complete their tasks on time leading to an increase in their productivity at work.

This is our task.py file.

import datetime

def parse_tasks(message):
    """
        Assumes message startswith 'remind me on'.
        Splits message into lists.
        Checks for any underlying errors in date and time format.
        Returns date, task description and time set.
    """

    # a = "remind me on 4 dec for packing up early at 16"
    b = message.split('remind me on ',1)[1]
    dat = b.split(' for ',1)[0]
    try:
        verify_date = datetime.datetime.strptime(dat, '%d %b')
        dat = verify_date.strftime('%d %B')
        # We will further save it into our db to keep the formats static.
    except ValueError: 
        try:
            verify_date = datetime.datetime.strptime(dat, '%d %B')
            dat = verify_date.strftime('%d %B')
            # We will further save it into our db to keep the formats static.
        except ValueError:
            return ("Incorrect format for date.\n\nPlease try something like *remind me on 4 dec for packing up early at 16*\n\n OR \n\n*remind me on 4 dec for packing up early at 16:01*",)

    c = b.split(' for ', 1)[1]
    desc = c.split(" at ", 1)[0]
    time = c.split(" at ", 1)[1]
    try:
        verify_time = datetime.datetime.strptime(time, '%H')
        time = verify_time.strftime('%H:%M')
        # We will further save it into our db to keep the formats static.
    except ValueError: 
        try:
            verify_time = datetime.datetime.strptime(time, '%H:%M')
            time = verify_time.strftime('%H:%M')
            # We will further save it into our db to keep the formats static.
        except ValueError:
            return ("Incorrect format for time.\n\nPlease try something like *remind me on 4 dec for packing up early at 16*\n\n OR \n\n*remind me on 4 dec for packing up early at 16:01*",)

    return (dat, desc, time)

# print(tasks('remind me on 4 dec for going to school for sdfasresults at 16:01'))

If the user message starts with ‘remind me on’ then our parse_tasks function extracts the date, task description and time from the user message received and verifies whether the date and time provided are valid.

If everything is parsed correctly then the task gets stored in the tasks table in our database and our users get a confirmation message letting them know that the task is set with the formatted date and time for the event.

Note: You need to set up a tasks table in your database as per my shared schema at the beginning. When the current date and time are equal to the set date and time, then send the task to the assigned user.

Reminders

Users will be able to set reminders for birthdays and anniversaries of their colleagues and friends. This will help them stay connected and keep the communication going.

Here is our reminder.py file.

import datetime

def parse_reminders(message):
    """
        Assumes message startswith 'remind me on'.
        Splits message into lists.
        Checks for any underlying errors in date format.
        Capitalizes the name and occasion.
        If name does not contain 's then returns an error.
        Returns date, person's name and occasion.
    """

    # a = "remind me on 4 dec for sara's birthday"
    b = message.split('remind me on ')[1]
    dat = b.split(' for ', 1)[0]
    try:
        verify_date = datetime.datetime.strptime(dat, '%d %b')
        dat = verify_date.strftime('%d %B')
        # We will further save it into our db to keep the formats static.
    except ValueError: 
        try:
            verify_date = datetime.datetime.strptime(dat, '%d %B')
            dat = verify_date.strftime('%d %B')
            # We will further save it into our db to keep the formats static.
        except ValueError:
            return ("Incorrect format for date.\n\nPlease try something like *remind me on 4 dec for sara's birthday*",)

    c = b.split(' for ', 1)[1]
    try:
        name = (c.split("'s ", 1)[0]).capitalize()
        occa = (c.split("'s ", 1)[1]).capitalize()
    except IndexError:
        return ("Incorrect format for setting up a reminder.\n\nDid you forget to put an *'s* after the name?\n\nPlease try something like *remind me on 4 dec for sara's birthday*",)

    return (dat, name, occa)

# print(tasks('remind me on 4 dec for going to school for sdfasresults at 16:01'))

Our reminders module works similar to tasks but the only difference is reminders are sent every year whereas tasks are sent only once at the set date and time.

If the user message starts with ‘remind me on’ and does not contain time then our parse_reminders function extracts the date & reminders from the user message received and verifies whether the date provided is valid.

If everything is parsed correctly then the reminder gets stored in the reminders table in our database and our users get a confirmation message letting them know that the reminder is set with the formatted date for the occasion.

Note: You need to set up a reminders table in your database as per my shared schema at the beginning. When the current date is equal to the set date, then send the reminder to the assigned user every year.

Conclusion

Phew! We have finally come to an end. Congratulations on building your own Slackbot offered with some great features. Here is my Slackbot Github Repository.

There are many features like facts, quotes offered in the Slackbot which I haven’t discussed in this article as their implementations were pretty straight-forward. There is also a help command provided to our users where they can know about all the available features and their assigned commands.

I would be glad to review your pull requests if you contribute in this open-source community to make this Slackbot a better one.

Also, do check out my Lyrics Extractor Python Library to get song lyrics by just passing in spelled or misspelled song names.

Learn More

#python #machine-learning

A Guide to Building a Multi-featured Slackbot with Python
8 Likes129.55 GEEK