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.
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:
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.
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**"
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:
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!"
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:
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"
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:
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
:
[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
:
[Unit]
Description=Cisco Spark 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:
[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
SparkBot API¶
Submodules¶
sparkbot.core module¶
-
class
sparkbot.core.
SparkBot
(spark_api, root_url=None, logger=None)¶ 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 orWEBHOOK_URL
in the environment to know its public URL.SparkBot has a
help
command built in by default. These may be overridden using thecommand()
decorator and providing the “help” argument and a function with your desired behavior on callinghelp
. 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
-
my_help_all
()¶ Returns a markdown-formatted list of commands that this bot has. Command, meant to be called by a bot user. Called by a user by typing “help”, “help all”, or “help-all”.
-
my_help
(commandline)¶ Returns the help of the command given in
commandline
. Command, meant to be called by a bot user. Callsmy_help_all()
if no command (“”) is given or isall
. Called by a user by typinghelp
.
-
command
(command_strings=[], fallback=False)¶ Decorator that adds a command to this bot.
Parameters: - command_strings (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.
-
commandworker
(json_data)¶ Called by the bottle app when a command comes in. Glues together the behavior of SparkBot.
Parameters: json_data – The blob of json that Spark POSTs to the webhook parsed into a dictionary
-
remove_help
()¶ Removes the help command from the bot
This will remove the help command even if it has been overridden.
-
respond
(spark_room, markdown)¶ Sends a message to a Spark 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
-
class
sparkbot.core.
Command
(function)¶ Bases:
object
Represents a command that can be executed by a SparkBot
Parameters: function – The function that this command will execute. Must return a str. -
classmethod
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
(commandline=None, event=None, caller=None, callback=None, room_id=None)¶ Executes this command’s
function
Executes this Command’s target function using the given parameters as needed. All parameters are required for normal function, named parameters are used for ease of understanding of the written code.
Parameters: - commandline –
shlex.split()
-processed list of tokens which make up the bot user’s message - event – The webhook event that was sent to us by Webex Teams
- caller (ciscosparkapi.Person) – The Person who pinged the bot to start this process
- callback – Function to be used as a callback.
Command.create_callback()
is used to turn this into a partial function, so the first argument of this function must be a Spark room ID that the bot author will expect to act on. - room_id – The ID of the room that the bot was called in
Returns: str, the desired reply to the bot user
- commandline –
-
classmethod
sparkbot.receiver module¶
-
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)¶ Creates a falcon.API instance with the required behavior for a SparkBot receiver.
Currently the API webhook path is hard-coded to
/sparkbot
Parameters: bot – sparkbot.SparkBot
instance for this API instance to use
-
sparkbot.receiver.
random_bytes
(length)¶ Returns a random bytes array with uppercase and lowercase letters, of length length
sparkbot.commandhelpers module¶
Helpful additional functionality for commands to take advantage of
-
sparkbot.commandhelpers.
check_if_in_org
(organization, person)¶ Ensures that the given person is inside the desired organization
Parameters: - api – CiscoSparkAPI instance to query Spark with.
- organization – The ID of the organization to find this user in
- person – The person to check against the organization. Must be CiscoSparkAPI.Person.
-
sparkbot.commandhelpers.
check_if_in_team
(api, team_id, person)¶ Checks if a person is in a given team
Parameters: - api – CiscoSparkAPI instance to query Spark with.
- team_id – The ID of the team to check for
- person – The person to check against the team
-
sparkbot.commandhelpers.
get_person_by_email
(api, person_email)¶ Gets a person by e-mail
Parameters: - api – CiscoSparkAPI instance to query Spark with.
- person_email – The e-mail address of the person to search for.
Returns: ciscosparkapi.Person of found person
Raises: ValueError if person_email is invalid or does not return exactly one person
Raises: TypeError if argument types are incorrect
-
sparkbot.commandhelpers.
get_person_by_spark_id
(api, person_id)¶ Gets a person by their Spark ID
Parameters: - api – CiscoSparkAPI instance to query Spark with.
- person_id – The person’s unique ID from Spark
Returns: ciscosparkapi.Person of found person
-
sparkbot.commandhelpers.
is_group
(api, room)¶ Determines if the specified room is a group (multiple people) or direct (one-on-one)
Parameters: - api – CiscoSparkAPI instance to query Spark with.
- room – The room to check the status of. May be a CiscoSparkAPI Room or a Spark room ID as a string.
Returns: True if the room is a group, False if it is not.
-
sparkbot.commandhelpers.
mention_person
(person)¶ Creates a “mention” for the specified person.
Parameters: person – The person to mention (must be CiscoSparkAPI.Person) Returns: String with the format “<@personId:[personId]|[firstName]>”
-
sparkbot.commandhelpers.
minargs
(numargs, commandline)¶ Ensures that you have more than [numargs] arguments in [commandline]
Module contents¶
A chatbot base that makes it super easy to interface with Cisco Spark