Building a World Cup Bot with Python, Twilio SMS and Slack

July 13, 2018
Written by

PbNR3N460mUYZaMA_4inLJMrLwkFI4YF0KH8wa15rketjBnX95e8ytdMstU-JBu-5-GUT2nerKhopBBq1tA7pEbQee1D72KKqZmKVQYM6TBouit2UJJCWmivhiXFaffmSRtle8Si

The World Cup. For us Brazilians, it’s like the Super Bowl, NBA Finals, World Series, and Stanley Cup, combined, multiplied by 100.

For those of us diehard ‘futebol’ fans following the games in Russia from the Western hemisphere, it’s been quite the challenge keeping up since most of the games are on while we’re at work.

I knew I wasn’t the only one in the office trying to keep up with the games. To solve this problem I created a bot that combines the Slack API with the Twilio SMS API to provide fans with real-time updates of every exciting moment in every world cup game – fouls, penalties, goals, and all.

This post will walk through how I built it. Feel free to follow these instructions to build your own World Cup bot or a bot for your sport of preference. You can check out the final code here.

Alright, let’s roll!

World Cup 2018 greatest moments

Tools We Need

Our bot, which we’ll name WorldCupBot, requires a few libraries and APIs. To build our bot we need:

Here’s a handy step-by-step guide to setting up Python, pip, virtualenv and Flask.
The Slack dependencies are:

Our Twilio requirements include:

Set up our environment
Before we get started, let’s set up our environment. This project was written in Python 3.6. Create a new directory to work with and activate a Python 3 virtualenv:

virtualenv env
source env/bin/activate

Next, we will install the packages we will need on our project directory:

$> pip install flask twilio slackclient python-dotenv python-dateutil

Now, we will create a .env file at the root directory to store the credentials we will need to interact with Twilio and Slack APIs. It will look like this:

#Twilio Credentials
TWILIO_ACCOUNT_SID=''
TWILIO_AUTH_TOKEN=''
TWILIO_MESSAGING_SERVICE_SID=''

#Slack Credentials
SLACK_BOT_TOKEN=''
SLACK_BOT_ID=''

We will add the credentials to this file later on. Since you most likely don’t want to commit your environment variables to a repository, make sure to add the .env file to your .gitignore to avoid accidentally pushing it.

For this project, we will use the python-dotenv library to load the .env file – this will make our lives easier. If you prefer, you can source the environment variables from your terminal.

Now let’s get the access token and bot ID for Slack and our Twilio credentials.

Slack Real Time Messaging API

If you don’t already have a Slack organization to work with, first create that. Since we’ll be using the Slack API, click the “Create a Slack App” button circled below:

This will return this web page, where you can enter your app’s name. Make sure you add a meaningful name – mine is ‘pythonworldcupbot’.  Once you fill out the information, click “Create App”.

Dialog stepping through creating a Slack App

Several options will appear that you can add to your application, including “Bots” which is circled below.

Create a Slack Bot for API Keys in the Python integration
Once you click the “Bots” option, there will be an “Add a Bot User” which you’ll need to click to continue the process.

Add a bot user button

Just as you’ve done before, fill out the needed fields and select “Add Bot User”.

You’ll need the default username later on, so take note of it!

Next, on the left sidebar click the option ‘OAuth & Permissions’, then click ‘Install App to your Workspace’. Copy the Bot User OAuth Access Token and save it as SLACK_BOT_TOKEN in your .env file.

Getting your Slack Bot ID

Let’s write a quick Python script to get callbot’s ID because it varies based on the Slack team. We need the callbot ID because it will allow our application code to determine if messages parsed from the Slack Real Time Messaging API are directed at our bot.

This script will also help test that our SLACK_BOT_TOKEN environment variable is set properly. Create a new file named get_slackbot_id.py with the following code:

import os
from dotenv import load_dotenv, find_dotenv
from slackclient import SlackClient

load_dotenv(find_dotenv())

BOT_NAME = "pythonworldcupbot" #update with your bot's 'Default username'

slack_client = SlackClient(os.getenv('SLACK_BOT_TOKEN'))

if __name__ == "__main__":
   api_call = slack_client.api_call("users.list")
   if api_call.get('ok'):
   # retrieve all users so we can find our bot
       users = api_call.get('members')
       for user in users:
           if 'name' in user and user.get('name') == BOT_NAME:
               print("Bot ID for '" + user['name'] + "' is " + user.get('id'))
   else:
       print("API Call failed")

The BOT_NAME variable should be assigned to the name you gave to your bot while setting up the Slack app.

The above code imports SlackClient and instantiates it with our SLACK_BOT_TOKEN. When the script is executed by the python command we hit the API for a list of Slack users and get the ID for the one that matches the name pythonworldcupbot (or the bot of your preference).

We only need to run this script once to obtain our bot’s ID.

$> python get_slackbot_id.py

When we run the script, we’ll get a single line of output with our Bot’s ID.

python get_slackbot_id.py
Bot ID for 'pythonworldcupbot' is XXXXXX

Copy the ID and add it to your .env  file as the SLACK_BOT_ID variable value.

Great! Now we just need to get our Twilio account credentials and we will be screaming “GOOOOOOOOOOOOOOOOOOOAAAAAAAAALLLLLL” in no time.

Twilio Messaging Service

We need access to the Twilio API to make phone calls from our application. Sign up for a Twilio account or log into your existing account if you already have one.
One of the most interesting features of our bot is the ability to send World Cup updates in real time. A regular Twilio number can only send 1 message per second. If we get up to 60 subscribers this is going to take a whole minute to send the messages and it gets worse from there.

We can prepare for this with a messaging service. Messaging services can pool numbers and scale sending out over the whole pool. As our subscriber base grows we can add numbers to keep up with demand.

Jump into the Twilio console, select ‘Programmable SMS’ and create a new Messaging Service, enter a friendly name of your preference and select “Chat Bot/Interactive 2 way” from the use case drop down.


Instead of using a “From” number to send messages, we will use the messaging service SID. Grab hold of the SERVICE SID and add it to the TWILIO_MESSAGING_SERVICE_SID credential on your .env file.

Then go to the numbers section for this service and add a number to the service. You can use one of our active numbers or get a new one.

Finally, go to the Console Dashboard screen and look for your Twilio Account SID and Auth Token:

Now we are ready to write the code for our WorldCupBot!

Setting up your Flask App

We have all the appropriate environment variables set for our Python code to appropriately use the Twilio and Slack APIs.

Now let’s get this ball rolling by writing a module to send SMS your subscribers.

Create a new file named server.py on your project directory and import these dependencies:

import os
import csv
from dotenv import load_dotenv, find_dotenv
from flask import Flask
from flask import jsonify
from flask import request
from http import HTTPStatus
from twilio.rest import Client

Next, add the code to instantiate and run the flask app, as well as the environment variables that we will retrieve using the dotenv library:

app = Flask(__name__)

TWILIO_ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID')
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
CLIENT = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
MESSAGING_SERVICE_SID = os.getenv("TWILIO_MESSAGING_SERVICE_SID")

 
if __name__ == "__main__":
   app.debug = True
   app.run(debug=True)

Save your work and get back to your terminal. On the same directory where the server.py lives, create a new file named subscribers.csv. We will use this file to store the phone numbers of your subscribers as comma-separated values. This format is the most common import and export format for spreadsheets and databases. We won’t be using any persistent data storages in this tutorial, but we need to save the numbers somewhere.

Next, let’s add the path to our subscribers.csv to our server.py module. We will also write some helper functions to add new phone numbers and retrieve a list of subscribers:

SUBSCRIBERS_DB = './subscribers.csv'


def add_subscriber(number: str)->None:
   with open(SUBSCRIBERS_DB, 'a', newline='') as csv_file:
       # creating a csv writer object
       csv_writer = csv.writer(csv_file,  delimiter=' ', quotechar='|')
       # writing the fields
       csv_writer.writerow([number])
       csv_file.close()

def get_subscribers()->list:
   subscribers = []
   with open(SUBSCRIBERS_DB, 'r') as csvfile:
       reader_ = csv.reader(csvfile, delimiter=' ', quotechar='|')
       # skip header
       next(reader_, None)
       for row in reader_:
           if len(row):
               subscribers.append(row[0])
   return subscribers

Now we can retrieve a list of phone numbers. Next, we will write some helper functions to loop over these numbers and send each one of them an SMS. To achieve our goal of sending multiple messages at once, we will use Twilio’s Messaging Service.

def send_message(number:str, body:str=None)-> "twilio.rest.api.v2010.account.message.MessageInstance":
       message = CLIENT.messages.create(
       to=number,
       from_=MESSAGING_SERVICE_SID,
       body=body)
       return message


def send_group_message(numbers_list: list, body: str=None)->None or Exception:
   for number in numbers_list:
       try:
           message = send_message(number, body)
       except Exception as e:
           return e

What if we want to confirm that a phone number really exists before sending a text?

VAR declarations took a new meaning during this World Cup

While we don’t have VAR to assist us with this one, fortunately we have an easy way to check if a number is valid with Twilio Lookup API.

Twilio Lookup is a simple REST API  for obtaining information about a phone number. Lookup can determine if a number exists, its type (landline, mobile, VoIP), and its carrier (Verizon, Sprint, etc) association. Lookup can also check if a number is able to receive text messages as well as format numbers into a standard format.

Still on our server.py, let’s write another helper function that calls the Twilio lookup api to check if a number is valid:

def lookup(phone_number:str) -> bool:
   """
   Calls Twilio lookup API to verify if a number is valid
   :param phone_number:
   :return: True if number is a valid phone number, False otherwise
   """
   # option: use regex
   try:
       phone_number = CLIENT.lookups.phone_numbers(f'{phone_number}').fetch(
                       type='carrier')
       print(phone_number.phone_number)
       return True
   except Exception as e:
       return False

We are almost done with our server module. We still need to add two routes on this app: /subscribe to add new users to our subscribers list, and /updates to send SMS to our subscribers list when an interesting event happens during a game.

Let’s start with /subscribe:

@app.route('/subscribe', methods=['POST'])
def subscribe():

   phone_number = request.get_json()["number"]
   if phone_number:
       is_valid = lookup(phone_number)
       if is_valid:
           # TODO: get number from verify formatted
           add_subscriber(phone_number)
           resp = send_message(phone_number, "Welcome to the World Cup live updates!")
           if resp.status == "accepted":
               # Return status code 200, with empty body
               return jsonify({"message": "You are subscribed!"}), HTTPStatus.OK
           else:
               return jsonify({"message": resp.msg}), HTTPStatus.BAD_REQUEST
       return jsonify({"message":"invalid number"}), 400

This endpoint can be called via an HTTP POST request. It calls the helper functions we wrote previously to: 1) verify if the number is valid and 2) if it is valid, send an SMS to the number confirming the subscription.

It returns a json with a ‘message’ attribute and the corresponding HTTP Status.

Now, let’s add our final endpoint, /updates:

@app.route('/updates', methods=['POST'])
def send_updates():
   """
   Sends SMS to subscribers when an interesting event happens during a live match
   :return: 200 if successful, JSON with message error if it fails to send message
   """
   message_body = request.get_json()['message']
   subscribers_list = get_subscribers()
   resp = send_group_message(subscribers_list, message_body)

   if resp:
       return jsonify({"message": resp.msg}, status=HTTPStatus.BAD_REQUEST)
   else:
       return '', 200

Here, once again, we have an endpoint that is reached via HTTP POST request.

Our helper functions will retrieve the list of subscribers, and iterate over the list to send every number a text. If we run into an error, it will return a JSON with the corresponding error message.

Let’s fire up our Flask app by running:

$> python server.py

The app will start and expose the subscribe and updates routes on http://localhost:5000.

Using the Slack Web API

The central part of this project is the Slack bot. It will post World Cup updates to the channel of your choice. You’ll also be able to subscribe to SMS updates via the Slack bot.

Let’s write some code to achieve this.

First, let’s let our flask app running and open up a new terminal. On your project directory, activate the virtualenv and create a new file named slack_handler.py.

$> touch slack_handler.py

Open the module on the editor of your choice, and add the following imports:

import os
import csv
import requests
import time
from dotenv import load_dotenv, find_dotenv
from flask import Flask
from flask import jsonify
from flask import request
from http import HTTPStatus
from twilio.rest import Client
from slackclient import SlackClient

Now that we have imported our dependencies we can use python-dotenv to grab those environment variables values and instantiate the Slack and Twilio clients.

load_dotenv(find_dotenv())

# constants
SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")
SLACK_BOT_ID = os.getenv("SLACK_BOT_ID")

AT_BOT = f'<@{SLACK_BOT_ID}>'
BOT_COMMAND = "subscribe"
TWILIO_ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID')
TWILIO_AUTH_TOKEN = os.getenv(‘TWILIO_AUTH_TOKEN’)
# instantiate Twilio Client
CLIENT = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
# instantiate Slack Client
SLACK_CLIENT = SlackClient(SLACK_BOT_TOKEN)

Our code instantiates the SlackClient with our SLACK_BOT_TOKEN. The  TwilioRestClient uses the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN  to be instantiated.

Next, we will kick off the bot with the following code:

if __name__ == "__main__":
   READ_WEBSOCKET_DELAY = 1  # 1 second delay between reading from firehose
   if SLACK_CLIENT.rtm_connect():
       print("pythonworldcupbot connected and running!")
       while True:
           command, channel = parse_slack_output(SLACK_CLIENT.rtm_read()) or (None, None)
           if command and channel:
               handle_command(command, channel)
           time.sleep(READ_WEBSOCKET_DELAY)
   else:
       print("Connection failed.")

SlackClient connects to the Slack Real Time Messaging API WebSocket connection. With the ‘While True’ statement we create a continuous loop that reads all the data coming the API. If any of those messages are directed at our bot, a function named handle_command will determine what to do with the command.

Above the Python code we just wrote, add four new functions to parse Slack output, handle commands and post messages:

def add_subscriber(phone_number: str):
   """
   Calls endpoint to add subscriber to SMS updates.
   If successful, returns success message. Returns error message otherwise.
   :param phone_number:
   :return:  str: text message
   """
   response = requests.post("http://localhost:5000/subscribe",
                            json={"number": phone_number})

   return response.json()["message"]


def handle_command(command: str, channel: str):
   """
   Receives commands directed at the world cup bot and determines if they
   are valid commands. If so, then acts on the commands. If not,
   returns back what it needs for clarification.
 """

   response = f'Not sure what you mean. To subscribe for SMS World Cup updates, use the *{BOT_COMMAND}* command ' 
              f'followed by your phone number (with area code), delimited by spaces. '
   if command.startswith(BOT_COMMAND) and channel == SLACK:
       response = add_subscriber(command[len(BOT_COMMAND):].strip())
   post_to_slack(channel, response)



def post_to_slack(channel, body=None, attachment_text=None):

   SLACK_CLIENT.api_call("chat.postMessage", channel=channel,
                          text=body, attachments=[{"text": attachment_text}], as_user=True)


def parse_slack_output(slack_rtm_output):
   """
   The Slack Real Time Messaging API is a firehose of data, so
   this parsing function returns None unless a message is
   directed at the Bot, based on its ID.
 """
   output_list = slack_rtm_output
   if output_list and len(output_list) > 0:
       for output in output_list:
           if 'text' in output and AT_BOT in output['text']:
               # return text after the @ mention, whitespace removed - should be phone number
               return output['text'].split(AT_BOT)[1].strip(), output['channel']
       return

The parse_slack_output function takes messages from Slack and determines if they are directed at our WorldCupBot. If a message starts with a direct message to our bot ID, then we know our bot needs to handle a command. handle_command function calls the add_subscriber if the command is valid, and calls post_to_slack with a success or failure message.

Add_subscriber makes a POST request to our Flask app /subscribe endpoint. If the number is valid, it adds to the subscribers.csv file and returns a success message. It returns a failure message otherwise.

Now let’s fire up the slack handler by running the python slack_handler.py command. You should see a success message:

$> python slack_handler.py
WorldCupBot connected and running!

 

Testing out our Slack Bot

With the server.py and slack_handler.py running, open up the slack channel you configured to receive the World Cup updates (or create a new one). Add the pythonworldcupbot to the channel by clicking on Add an app or @mentioning the bot.

Try sending a random message to the bot – if everything is working properly, the bot will reply prompting you to use the command word subscribe followed by your phone number.

If you @mention the bot followed by the word subscribe and a valid number, you will receive a success message on slack and on your cell phone:

Creating our World Cup Events Notifier

Let’s start by writing a module to interact with the FIFA World Cup API, which is a URL that returns some JSON.

We are finally approaching the final whistle. The final step on our World Cup bot project is to write a module that will interact with the FIFA API and get updates on live matches. It will also call the post_to_slack function from our slack_handler module and generate a post request to our /updates endpoint.

Create a new file named notifier.py and add the following imports:

import os
import time
import json
import requests
import math
import dateutil.parser
from requests import Request, Session

Next, we will declare constants that represent the game events that are meaningful to us, and their correspondent values returned by the FIFA API:

#set the timezone to be the same as the in the FIFA API:
os.environ['TZ'] = 'UTC'
time.tzset()
# FIFA API 2018 CONSTANTS
FIFA_API_URL = "https://api.fifa.com/api/v1/"
ID_COMPETITION = 17
ID_SEASON = 254645
# Match Statuses
MATCH_STATUS_FINISHED = 0
MATCH_STATUS_NOT_STARTED = 1
MATCH_STATUS_LIVE = 3
MATCH_STATUS_PREMATCH = 12
# Event Types
EVENT_GOAL = 0
EVENT_YELLOW_CARD = 2
EVENT_STRAIGHT_RED = 3
EVENT_SECOND_YELLOW_CARD_RED = 4
EVENT_PERIOD_START = 7
EVENT_PERIOD_END = 8
EVENT_END_OF_GAME = 26
EVENT_OWN_GOAL = 34
EVENT_FREE_KICK_GOAL = 39
EVENT_PENALTY_GOAL = 41
EVENT_PENALTY_SAVED = 60
EVENT_PENALTY_CROSSBAR = 46
EVENT_PENALTY_MISSED = 65
EVENT_FOUL_PENALTY = 72
# Periods
PERIOD_1ST_HALF = 3
PERIOD_2ND_HALF = 5
PERIOD_1ST_ET = 7
PERIOD_2ND_ET = 9
PERIOD_PENALTY = 11
# Language
LOCALE = 'en-GB'

The FIFA API supports multiple languages. To keep things simple with this project, we will use only English. Next, we will create a dictionary where the key is the language localization, and the value is a list of strings that correspond to interesting match events:

language = { 'en-GB': [
   'The match between',
   'is about to start',
   'Yellow card',
   'Red card',
   'Own goal',
   'Penalty',
   'GOOOOAL',
   'Missed penalty',
   'has started',
   'HALF TIME',
   'FULL TIME',
   'has resumed',
   'END OF 1ST ET',
   'END OF 2ND ET',
   'End of penalty shoot-out'
   ]}

You can add support to other languages (I’ve added Portuguese support to my bot).

Now, let’s write a generic get_url function that takes in a URL and uses the python Requests library to make an HTTP request. This makes our code easier to customize, if you want to use it for other purposes:

def get_url(url, do_not_use_etag=False):

   proxies = dict()
   s = Session()
   req = Request('GET', url)
   prepped = s.prepare_request(req)

   resp = s.send(prepped,
                 proxies=proxies,
                 timeout=10,
                 verify=False
                 )

   if resp.status_code == requests.codes.ok:
       content = resp.text

       if len(content.strip()) == 0:
              return False
       return content
   else:
       print(resp.status_code, resp.content)

We’re getting close! Let’s write a couple of helper functions that will actually call the FIFA API:

def get_all_matches():
   return json.loads(get_url(f'{FIFA_API_URL}calendar/matches?idCompetition={ID_COMPETITION}&idSeason={ID_SEASON}&count=500&language={LOCALE}'))


def get_player_alias(player_id):

   resp = json.loads(get_url(f'{FIFA_API_URL}players/{player_id}', False))

   return resp["Alias"][0]["Description"]

What is happening here? We wrote a function get_all_matches that retrieves all the world cup matches, by calling the endpoint with the constants we defined at the top of our code. We use the json library method load to decode the returned JSON.

The get_player_alias function will be called when we need to identify a specific player. It returns a string with the player’s name.

Now that we have all the data we need, let’s find a way to store that data, so later on we can parse it and look for interesting events.

For the scope of this project, we will store the data in a .json file, similarly to what we did previously with our subscribe.csv file.

Let’s save our progress and create a new file on our terminal, where we will store data about on-going games:

$> touch worldCupData.json

Let’s get back to notifier.py and finish up our work. Add the path to our JSON file where you declared the module’s constants:

# Language
LOCALE = 'en-GB'

DATA_FILE = './worldCupData.json'

We will write a function that we can call whenever we need to update our data file:

def save_to_json(file):
   with open(DATA_FILE, 'w') as f:
       json.dump(file, f)

We will keep track of the last time we updated the data file by adding a Unix timestamp as a key-value pair. Here is a quick helper function to create the timestamp:

def microtime(get_as_float=False):
   """Return current Unix timestamp in microseconds."""
   if get_as_float:
       return time.time()
   else:
       x, y = math.modf(time.time())
       return f'{x} {y}'

Before we call the FIFA API and parse the data, let’s make sure that we can send the interesting events we might find to Slack and to our SMS subscribers.

First, let’s write a function that sends a POST request to our Flask app:

def send_sms(text: str, attachment: str=None)->None:
   """

   :param text: Match update
   :param attachment: Update details
   :return: None
   """
   if attachment:
       text = text   " "   attachment
   resp = requests.post("http://localhost:5000/updates",
                        json={'message': text})

   if resp.status_code != 200:
       print(resp)

This function will make a POST request to our updates endpoint and post the passed in text and attachment to all of our futbol fans.

Posting to slack is even easier:  Go to the top of the file, where we added all the import statements, and import the post_to_slack method from our slack_handler module:

import dateutil.parser
from slack_handler import post_to_slack

SLACK_CHANNEL="#your-slack-channel-here"

Add another constant that holds the name of the slack channel you’d like to post the updates to. Make sure that your bot is added to that channel.

The final piece of code you’ll have to write will handle calling the FIFA API and check if there are any live matches going on.

If there is one or more matches happening, for each live match, we make an API call to retrieve events.

Then, if any of the events retrieved match the interesting event constants, we will call the send_SMS and post_to_slack functions.

# Let’s grab the data we have on our json file:
DB = json.loads(open(DATA_FILE).read())
resp = get_all_matches()

matches = {}

if resp != 'null':
   matches = resp.get("Results")

# Find live matches and update score
for match in matches:

   if match.get('MatchStatus') == MATCH_STATUS_LIVE and match.get("IdMatch") not in DB["live_matches"]:
       DB["live_matches"].append(match["IdMatch"])

       DB[match["IdMatch"]] = {
           'stage_id': match["IdStage"],
           'teamsById': {
               match["Home"]["IdTeam"]: match["Home"]["TeamName"][0]["Description"],
               match["Away"]["IdTeam"]: match["Away"]["TeamName"][0]["Description"]
           },
           'teamsByHomeAway': {
               'Home': match["Home"]["TeamName"][0]["Description"],
               'Away': match["Away"]["TeamName"][0]["Description"]
           },
           'last_update': microtime()
       }
       # send sms and save data
       send_sms(f'{language[LOCALE][0]} {match["Home"]["TeamName"][0]["Description"]} vs. 
       {match["Away"]["TeamName"][0]["Description"]} {language[LOCALE][1]}!')

   if match["IdMatch"] in DB["live_matches"]:
       # update score
       DB[match["IdMatch"]]["score"] = f'{match["Home"]["TeamName"][0]["Description"]} {match["Home"]["Score"]} - ' 
                                       f'{match["Away"]["Score"]} {match["Away"]["TeamName"][0]["Description"]} '
   # save to file to avoid loops
   save_to_json(DB)


live_matches = DB["live_matches"]
for live_match in live_matches:
   for key, value in DB[live_match].items():
       if not DB.get(live_match):
           continue
       home_team_name = DB[live_match]['teamsByHomeAway']["Home"]
       away_team_name = DB[live_match]['teamsByHomeAway']["Away"]
       last_update_secs = DB[live_match]["last_update"].split(" ")[1]

       # retrieve match events
       response = json.loads(get_url(
           f'{FIFA_API_URL}timelines/{ID_COMPETITION}/{ID_SEASON}/{DB[live_match]["stage_id"]}/{live_match}?language={LOCALE}'))

       # in case of 304
       if response is None:
           continue
       events = response.get("Event")
       for event in events:
           event_type = event["Type"]
           period = event["Period"]
           event_timestamp = event_timestamp = dateutil.parser.parse(event["Timestamp"])

           event_time_secs = time.mktime(event_timestamp.timetuple())

           if event_time_secs > float(last_update_secs):
               match_time = event["MatchMinute"]
               _teams_by_id = DB[live_match]['teamsById']
               for key, value in _teams_by_id.items():
                   if key == event["IdTeam"]:
                       event_team = value
                   else:
                       event_other_team = value
               event_player_alias = None
               score = f'{home_team_name} {event["HomeGoals"]} - {event["AwayGoals"]} {away_team_name}'
               subject = ''
               details = ''
               interesting_event = True

               if event_type == EVENT_PERIOD_START:
                   if period == PERIOD_1ST_HALF:
                       subject = f'{language[LOCALE][0]} {home_team_name} vs. {away_team_name} {language[LOCALE][8]}!'

                   elif period == PERIOD_2ND_HALF or period == PERIOD_1ST_ET or period == PERIOD_2ND_ET or period == PERIOD_PENALTY:
                       subject = f'{language[LOCALE][0]} {home_team_name} vs. {away_team_name} {language[LOCALE][11]}!'

               elif event_type == EVENT_PERIOD_END:
                   if period == PERIOD_1ST_HALF:
                       subject = f'{language[LOCALE][9]} {score}'
                       details = match_time
                   elif period == PERIOD_2ND_HALF:
                       subject = f'{language[LOCALE][10]} {score}'
                       details = match_time
                   elif period == PERIOD_1ST_ET:
                       subject = f'{language[LOCALE][12]} {score}'
                       details = match_time
                   elif period == PERIOD_2ND_ET:
                       subject = f'{language[LOCALE][13]} {score}'
                       details = match_time
                   elif period == PERIOD_PENALTY:
                       subject = f'{language[LOCALE][13]} {score} ({event["HomePenaltyGoals"]} - {event["AwayPenaltyGoals"]})'
                       details = match_time


               elif event_type == EVENT_GOAL or event_type == EVENT_FREE_KICK_GOAL or event_type == EVENT_PENALTY_GOAL:
                   event_player_alias = get_player_alias(event["IdPlayer"])
                   subject = f'{language[LOCALE][6]} {event_team}!!!'
                   details = f'{event_player_alias} ({match_time}) {score}'

               elif event_type == EVENT_OWN_GOAL:
                   event_player_alias = get_player_alias(event["IdPlayer"])
                   subject = f'{language[LOCALE][4]} {event_team}!!!'
                   details = f'{event_player_alias} ({match_time}) {score}'

               # cards

               elif event_type == EVENT_YELLOW_CARD:
                   event_player_alias = get_player_alias(event["IdPlayer"])
                   subject = f'{language[LOCALE][2]} {event_team}'
                   details = f'{event_player_alias} ({match_time})'


               elif event_type == EVENT_SECOND_YELLOW_CARD_RED or event_type == EVENT_STRAIGHT_RED:
                   event_player_alias = get_player_alias(event["IdPlayer"])
                   subject = f'{language[LOCALE][3]} {event_team}'
                   details = f'{event_player_alias} ({match_time})'


               elif event_type == EVENT_FOUL_PENALTY:
                   subject = f'{language[LOCALE][5]} {event_other_team}!!!'

               elif event_type == EVENT_PENALTY_MISSED or event_type == EVENT_PENALTY_SAVED:
                   event_player_alias = get_player_alias(event["IdPlayer"])
                   subject = f'{language_[LOCALE][7]} {event_team}!!!'
                   details = f'{event_player_alias} ({match_time})'

               elif event_type == EVENT_END_OF_GAME:
                   DB['live_matches'].remove(live_match)
                   del DB[live_match]
                   save_to_json(DB)
                   interesting_event = False

               else:
                   interesting_event = False
                   continue

               if interesting_event:
                   send_sms(subject, details)
                   post_to_slack(SLACK_CHANNEL, subject, details)
                   DB[live_match]['last_update'] = microtime()

               if not DB["live_matches"]:
                   DB["live_matches"] = []

save_to_json(DB)
exit(0)

To test your notifier, open the worldCupData.json file and paste the following:

{"live_matches": ["300331543"], "etag": {}, "300331543": {"stage_id": "275095", "teamsById": {"43930": "Uruguay", "43946": "France"}, "teamsByHomeAway": {"Home": "Uruguay", "Away": "France"}, "last_update": "0.839353084564209 1530890343.0", "score": "Uruguay 0 - 2 France "}}

Now, with your server.py and slack_handler.py running, open a new terminal and run the notifier (make sure to run when a game is NOT happening):

$> python notifier.py

You should receive the updates from that match on the Slack channel you added to the notifier.py (remember to add your Slack bot to it):


And if you subscribed, you should see the following messages:

Finally, check your WorldCupData.json , it should look like this:

{"live_matches": [] "etag": {} }

 

Ole ole ole!

As the last step, we will setup crontab to run notifier every minute, so we can offer real time updates on live matches. Windows users can check out Windows Task Scheduler.  Run:

$> nano crontab -e

And add the following script:

SHELL=/bin/bash
* * * * * cd [path_to_your_directory] && source [virtualenv_name]/bin/activate && python notifier.py >> ./notifier.log

Remember, you will have to keep slack_handler.py and  server.py persistently running somewhere – or at least while a match is happening.

Wrapping up

It’s time to celebrate! You did it!


But there’s so much more you can do with Slack and Twilio APIs. Here are some ideas:
  1. Add emojis to your messages
  2. Implement a persistent backend like PostgreSQL and use it to store match events and subscribers
  3. The next World Cup will be in 2022. Until then, why don’t you create a new notifier to stay up to date with other championships or other sports?

Let me know what you come up with!

  • Email: mbichoffe@twilio.com