Welcome to SparkBot’s documentation!

Welcome to the documentation for SparkBot! If you’re looking for the fastest way to get running, check out Quickstart!

Quickstart

This document will lead you through the steps to run the base Sparkbot instance.

Get a token from Webex Teams

Head over to Cisco Webex Teams for Developer’s My Apps portal and click the Add button to create a new bot. Go through the steps to create a bot. Once you’re finished, copy the Bot’s Access Token somewhere safe. We’ll need it later in this process.

Dependencies

First you’ll need to install the prerequisites for running SparkBot.

SparkBot requires the following software:

  • Python 3.5 or higher
  • Reverse proxy, such as nginx, for its webhook receiver. We’ll be using ngrok in this quickstart.

Ubuntu 16.04

To install the prerequisites on Ubuntu 16.04:

sudo apt install python3 python3-virtualenv python3-pip nginx

Clone the source

Clone the bot’s source to your desired location. From here on, we’ll assume that the bot’s source code is located in ~/sparkbot, but you can change the path as you need.

Copy run.py.example

run.py.example contains the code needed to run SparkBot. It’s also where you’ll create new commands for the bot. We’ll copy it to run.py now:

cp ~/sparkbot/run.py.example ~/sparkbot/run.py

Set up a virtualenv

Create and activate a Python(3.5+) virtualenv for the bot:

python3 -m virtualenv ~/sparkbotEnv
source ~/sparkbotEnv/bin/activate

Now we can install the required Python packages:

pip install -r ~/sparkbot/requirements.txt
pip install gunicorn

Use ngrok for a temporary reverse proxy

ngrok is a great service for setting up temporary public URLs. We’ll be using it to quickly test our bot configuration. Download ngrok, then run ngrok http 8000 to get it running.

Run the bot

We can now test the bot. Sparkbot requires that a few environment variables be set, so we’ll export them before we run:

cd ~/sparkbot
source ~/sparkbotEnv/bin/activate
export SPARK_ACCESS_TOKEN=[api_token]
export WEBHOOK_URL=[url]
gunicorn run:bot.receiver

Replace [url] with the URL that points to your webhook endpoint. Since we’re using ngrok, put the https Forwarding URL here. Replace [api_token] with the token that Webex Teams gave you for your bot.

The bot should now be running and, assuming your proxy is working correctly, be able to receive requests directed at it from Webex Teams. Try messaging the bot with ping or help to see if it will respond.

Next steps

Now that you’ve got the bot running, you may want to learn more about Writing Commands or Deploying SparkBot

Writing Commands

Introduction

SparkBot provides a very simple interface for writing commands. You will be familiar with it if you have ever used Flask. Here’s a simple ping command:

@MY_BOT.command("ping")
def ping():
    """
    Checks if the bot is running.

    Usage: `ping`

    Returns **pong**.
    """

    return "**pong**"

Let’s break down what’s happening here line-by-line.

First, we have the decorator, sparkbot.core.SparkBot.command(), which marks this function as a command for our bot:

@MY_BOT.command("ping")

bot is the SparkBot instance that we’re adding this command to. "ping" is what we want the user to type in order to invoke this command.

Next, the function definition and docstring:

def ping():
    """
    Usage: `ping`

    Returns **pong**.
    """

The docstring also serves as the command’s help, accessible via the help [command] command. It must all be equally spaced (so don’t put the description on the same line as the opening quotes like you would in most cases) and is formatted in Markdown. You should stick to the general format of description, usage, returns when writing your docstrings for commands.

Using 'help ping'

Finally, we see how simple it is to send formatted text back to the user:

return "**pong**"

When we add this to the area below the # Add commands here comment and re-run the bot, we can now use the ping command:

Using the 'ping' command

Note

Commands must always be added to the bot prior to the receiver starting. This means that the bot cannot add or remove commands from itself. Changes will always require a restart.

Taking arguments

In many cases you will want to take arguments to your commands. Sparkbot uses shlex.split to split the message sent by the user into multiple ‘tokens’ that are given to you in a list. These tokens are split in a similar way to a POSIX shell.

Here’s a command that uses this type of input. It returns the first token in the list:

@MY_BOT.command("testcommand")
def testcommand(commandline):
    """
    Usage: `testcommand something`

    A command used for testing. Returns the first word you typed.
    """

    if commandhelpers.minargs(1, commandline):
        return commandline[1]
    else:
        return 'This command requires at least one argument'

While the help says that this will only return the first word, this command will also return the first quoted string that’s typed as well.

Using the 'testcommand' command from above

Let’s go over this line-by-line:

 @MY_BOT.command("testcommand")
 def testcommand(commandline):

As usual, we use the sparkbot.core.SparkBot.command() decorator to add this function to our bot’s list of commands. However, notice that we defined the function to take the argument commandline. This is one of several keywords that SparkBot recognizes. When executing your function, it will find this keyword and send the commandline property accordingly.

When the user types testcommand some cool stuff, this code receives the following list as its commandline argument:

['testcommand', 'some', 'cool', 'stuff']

Whereas testcommand "some cool" stuff will yield the following:

['testcommand', 'some cool', 'stuff']

Using a helper function, sparkbot.commandhelpers.minargs(), we check to make sure we have at least one argument (token) in the commandline. Then, we return either the first token if there is one or more, or an error if there are no tokens:

if commandhelpers.minargs(1, commandline):
    return commandline[1]
else:
    return 'This command requires at least one argument'

As you can see, you can quickly create a CLI-like interface by iterating over the tokens in this list.

Replying early

SparkBot allows you to use the yield keyword in place of return to reply to the user before your command’s code has completed. This may be useful if you have a command that will perform a very long operation and you would like to notify the user that it is in progress.

 @MY_BOT.command("ping")
 def ping_callback():
     """
     Usage: `ping`

     Returns **pong**, but with a twist.
     """

     yield "a twist"

     # Some code which runs for a long time

     yield "**pong**"
Using the ping command with an interim response

Changed in version 0.1.0: yield to reply early has been added as a replacement for the callback argument previously used to get a function used for the same purpose. callback will be removed in SparkBot version 1.0.0.

Overriding behavior

SparkBot comes with default behavior that will work well for simple bots. However, you may need to override some of this behavior to provide a richer experience for your users.

“Help” command

Override

The default SparkBot help command is simplistic:

Default SparkBot help command

If you want to do something different when your user asks for help, you can add a new command in the same slot as “help”:

@bot.command("help")
def new_help():
    return "It's the new help command!"
Overriden behavior for the help command
Remove

If you’d prefer to remove the help command altogether, you can do so by calling SparkBot.remove_help().

Note

Similar to adding commands, removing commands must be performed before the bot has started. It is not possible to remove help “in-flight”, such as from another command.

“Command not found”

By default, when the user tries to use a command that doesn’t exist, they get an error:

Default SparkBot "Command not found" error

It may be desirable for you to do something else (return a more fun error message, give suggestions rather than an error, or maybe use NLP to determine what the user wanted).

You can add a command as a fallback by omitting its command strings and adding the fallback_command=True argument to the command decorator:

@bot.command(fallback=True)
def fallback():
    return "This is a fallback command"
Overridden "Command not found" behavior

List of recognized keywords

Keyword Data
commandline List containing user’s message split into tokens by shlex.split. Taking arguments
event Dictionary containing the event request from Spark.
caller ciscosparkapi.Person for the user that called this command
room_id Str containing the ID of the room where this command was called

Deploy

When it comes time to deploy your bot to a server, we recommend using gunicorn and nginx. The following information will help you run the bot under gunicorn with nginx as its reverse proxy.

This information is adapted from the Deploying Gunicorn document, you may wish to head to it for more advanced setups.

Install required system packages

Before starting, it is important to have nginx and the appropriate Python 3 packages installed.

Ubuntu 16.04 / 18.04

sudo apt install nginx python3 python3-pip python3-virtualenv

Install Python packages in a virtualenv

Create a virtualenv for SparkBot with the required packages. This will keep system-level Python packages separate from your SparkBot packages.

It’s a good idea to create a new service account with the bare minimum permissions for Sparkbot:

sudo useradd --system --create-home sparkbot

Now, log in to the sparkbot user so we can install the virtualenv:

sudo -Hu sparkbot /bin/bash

Finally, create the virtualenv and install SparkBot into it:

python3 -m virtualenv --python=python3 /home/sparkbot/sparkbotenv
source /home/sparkbot/sparkbotenv/bin/activate
pip install git+https://github.com/universalsuperbox/SparkBot.git gunicorn
deactivate
exit

Get your run.py script

This guide assumes that your SparkBot script is called run.py and is placed at /home/sparkbot/run.py. If your script is named differently, change run in run:bot.receiver in the ExecStart entry to the script’s name (without .py). If your script is located in a different directory, change the WorkingDirectory.

Add nginx configuration

We’ll use nginx to proxy requests to the bot. You may use this configuration as a template for your reverse proxy for the bot’s webhook receiver:

/etc/nginx/conf.d/sparkbot.conf
upstream app_server {
  # fail_timeout=0 means we always retry an upstream even if it failed
  # to return a good HTTP response

  # for UNIX domain socket setups
  server unix:/run/sparkbot/socket fail_timeout=0;
}

server {
  # if no Host match, close the connection to prevent host spoofing
  listen 80 default_server;
  return 444;
}

server {
  # use 'listen 80 deferred;' for Linux
  # use 'listen 80 accept_filter=httpready;' for FreeBSD
  listen 80;
  client_max_body_size 4G;

  # set the correct host(s) for your site
  server_name example.com www.example.com;

  keepalive_timeout 5;

  # path for static files
  root /path/to/app/current/public;

  location / {
    # checks for static file, if not found proxy to app
    try_files $uri @proxy_to_app;
  }

  location @proxy_to_app {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    # we don't want nginx trying to do something clever with
    # redirects, we set the Host: header above already.
    proxy_redirect off;
    proxy_pass http://app_server;
  }
}

Remember to set the server_name property to the FQDN of your server.

It is highly recommended to use HTTPS for this reverse proxy, but setting that up is outside of the scope of this guide.

Auto-start with systemd

First, we’ll add a unit file for the Gunicorn socket. This goes at /etc/systemd/system/sparkbot.socket:

/etc/systemd/system/sparkbot.socket
[Unit]
Description=SparkBot gunicorn socket

[Socket]
ListenStream=/run/sparkbot/socket

[Install]
WantedBy=sockets.target

Next, create the file /etc/systemd/system/sparkbot.service with the following content. Once finished, save and close the file then run systemctl daemon-reload:

/etc/systemd/system/sparkbot.service
[Unit]
Description=Webex Teams chatbot
Requires=sparkbot.socket
After=network.target

[Service]
PIDFile=/run/sparkbot/pid
RuntimeDirectory=sparkbot
Environment="SPARK_ACCESS_TOKEN="
Environment="WEBHOOK_URL="
User=sparkbot
ExecStart=/home/sparkbot/sparkbotenv/bin/gunicorn \
            --bind unix:/run/gunicorn/socket run:bot.receiver
WorkingDirectory=/home/sparkbot/
Restart=on-abort
StandardOutput=journal
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Next, run systemctl edit sparkbot.service and enter the following, changing the options in curly brackets to match your desired settings:

systemctl edit sparkbot.service
[Service]
Environment="SPARK_ACCESS_TOKEN={api_token}"
Environment="WEBHOOK_URL={url}"

The values should be the same as the ones you used when you followed the Quickstart guide.

Once that’s finished, run the following to enable the bot on startup:

sudo systemctl daemon-reload
sudo systemctl enable sparkbot.socket
sudo systemctl enable sparkbot.service
sudo systemctl start sparkbot.socket
sudo systemctl start sparkbot.service

API Documentation

This page contains information about SparkBot’s internals. Bot authors may not find it terribly useful, SparkBot hackers will.

SparkBot

class sparkbot.SparkBot(spark_api, root_url=None, logger=None, skip_receiver_setup=None, custom_receiver_resources={})

Bases: object

A bot for Cisco Webex Teams

SparkBot automatically creates a webhook for itself and will delete any other webhooks on its bot account. To do this, it uses the root_url parameter or WEBHOOK_URL in the environment to know its public URL.

SparkBot has a help command built in by default. These may be overridden using the command() decorator and providing the “help” argument and a function with your desired behavior on calling help. See Writing Commands for more information on writing commands.

Parameters:
  • spark_api (ciscosparkapi.CiscoSparkAPI) – CiscoSparkAPI instance that this bot should use
  • root_url (str) – The base URL for the SparkBot webhook receiver. May also be provided as WEBHOOK_URL in the environment.
  • logger (logging.Logger) – Logger that the bot will output to
  • skip_receiver_setup ("all", "webhook") – Set to “all” or “webhook” to skip setting up a receiver when instancing SparkBot. “all” skips creating the receiver and the webhook. “webhook” creates a receiver but does not register a webhook with Webex Teams.
  • custom_receiver_resources – dict containing custom resources for the receiver. Pass a dict of Falcon resource(s) with the endpoint you would like them hosted at as the key. For example, {"/my_endpoint": EndpointResource} will serve EndpointResource at /my_endpoint.
my_help_all()

Returns a markdown-formatted list of commands that this bot has. This function is used by the default “help” command to show the full list.

my_help(commandline)

Returns the help of the command given in commandline. Calls my_help_all() if no command is given or is all. Called by a user by typing help. This function is the default “help” command and can be removed using remove_help().

command(command_strings=[], fallback=False)

Decorator that adds a command to this bot.

Parameters:
  • command_strings (list or str) – Callable name(s) of command. When a bot user types this (these), they call the decorated function. Pass a single string for a single command name. Pass a list of strings to give a command multiple names.
  • fallback (bool) – False by default, not required. If True, sets this command as a “fallback command”, used when the user requests a command that does not exist.
Raises:
  • CommandSetupError – Arguments or combination of arguments was incorrect. The error description will have more details.
  • TypeError – Type of arguments was incorrect.
command_dispatcher(user_request)

Executes a command for the user’s request.

This method is called by the receiver when a command comes in. It uses the information in the user_request to execute a command (using execute_command()) and send its reply back to the user.

Parameters:user_request (ciscosparkapi.WebhookEvent) – Event where the user called the bot
create_callback(respond, room_id)

Pre-fills room ID in the function given by respond

Adds the room ID as the first argument of the function given in respond, simplifying the ‘callback’ experience for bot developers.

Parameters:
  • respond – The method to add the room ID to
  • room_id – The ID of the room to preset in respond
execute_command(command_str, **kwargs)

Runs the command given by ‘command_str’ if it exists with the possible arguments in **kwargs.

Note that execute_command is “dumb”. It does not enforce the return type of a command function. It will happily return anything that the bot writer’s command does. Contrast to command_dispatcher() which checks whether a command (executed by this function) returns either a Generator or a str.

Parameters:
  • command_str (str) – The ‘command’ that the user wants to run. Should match a command string that has previously been added to the bot.
  • commandline (list) – The user’s complete message to the bot parsed into a list of tokens by shlex.split().
  • event (ciscosparkapi.WebhookEvent) – ciscosparkapi.WebhookEvent object describing the request causing this command.
  • caller (ciscosparkapi.Person) – The user who sent the message we’re processing.
  • room_id (str) – The ID of the room that the message we’re processing was sent in.
Keyword Arguments:
 

Each keyword argument sent here will be used as a possible argument for commands. For example, the keyword argument commandline= will allow a command function (the ones defined with command()) to take an argument by the name commandline. The current list can be found at List of recognized keywords.

If a logger is defined, SparkBot will raise a warning if an argument is requested but not provided by the kwargs given to this function. This means that there is either a typo in the argument on the command function or command_dispatcher() has failed to do its job correctly (the first is more likely).

remove_help()

Removes the help command from the bot

This will remove the help command even if it has been overridden.

send_message(spark_room, markdown)

Sends a message to a Teams room.

Parameters:
  • markdown – Markdown formatted string to send
  • spark_room – The room that we should send this response to, either CiscoSparkAPI.Room or str containing the room ID

Default receiver

The receiver waits for a request from Webex Teams and executes sparkbot.SparkBot.command_dispatcher() when one comes in.

class sparkbot.receiver.ReceiverResource(bot)

Bases: object

on_post(req, resp)

Receives messages and passes them to the sparkbot instance in BOT_INSTANCE

sparkbot.receiver.create(bot, **kwargs)

Creates a falcon.API instance with the required behavior for a SparkBot receiver.

Currently the API webhook path is hard-coded to /sparkbot

Parameters:botsparkbot.SparkBot instance for this API instance to use
Keyword Arguments:
 Additional arguments may be used to specify more resources that should be exposed by the SparkBot receiver. For example, "/my_webhook"=[Falcon resource] will expose your Falcon resource at /my_webhook on the server.
sparkbot.receiver.random_bytes(length)

Returns a random bytes array with uppercase and lowercase letters, of length length

Indices and tables