22 KiB
Zulip bot system
Zulip's features can be extended by the means of bots and integrations.
- Integrations are used to connect Zulip with different chat, scheduling and workflow software. If this is what you are looking for, please check out the integrations guide.
- Bots, as a more general concept, intercept and react to messages. If this is what you are looking for, read on!
The purpose of this documentation is to provide you with information about Zulip's bot system.
On this page you'll find:
- A step-by-step tutorial on how to run a bot.
- A step-by-step tutorial on how to develop a bot.
- A documentation of the bot API.
- Common problems when developing/running bots and their solutions.
Contributions to this guide are very welcome, so if you run into any issues following these instructions or come up with any tips or tools that help with writing bots, please visit #integrations on the Zulip development community server, open an issue, or submit a pull request to share your ideas!
The bots system
Zulip's bot system resides in the python-zulip-api repository.
The structure of the bots ecosystem looks like the following:
zulip_bots
└───zulip_bots
├───bots
│ ├───bot1
│ └───bot2
│ │
│ ├───bot2.py
│ ├───bot2.conf
│ ├───doc.md
│ ├───test_bot2.py
│ ├───assets
│ │ │
│ │ └───pic.png
│ ├───fixtures
│ │ │
│ │ └───test1.json
│ └───libraries
│ │
│ └───lib1.py
├─── lib.py
├─── test_lib.py
├─── run.py
└─── provision.py
Each subdirectory in bots
contains a bot. When developing bots, try to use the structure outlined
above as an orientation.
Installing the zulip_bots
package
The zulip_bots
package comes with all you need to run a bot.
Installing a stable version
Run pip install zulip_bots
.
Installing a development version
-
git clone https://github.com/zulip/python-zulip-api.git
- clone the python-zulip-api repository. -
cd python-zulip-api
- navigate into your cloned repository. -
./tools/provision
- install all requirements in a Python virtualenv. -
Run the
source <activation/path>
command printed in the previous step to activate the virtualenv. -
Finished. You should now see the name of your venv preceding your prompt, e.g.
(ZULIP-~1)
.
Hint: ./tools/provision
installs zulip
, zulip_bots
, and zulip_botserver
in developer
mode. This enables you to make changes to the code after the packages are installed.
How to run a bot
This guide will show you how to run a bot on a running Zulip
server. It assumes you want to use one of the existing zulip_bots/bots
bots in your Zulip organization. If you want to write a new one, you
just need to write the <my-bot>.py
script and put it into zulip_bots/bots/<my-bot>
directory.
Looking for an easy way to test a bot's output? Check out this section.
You need:
- An account in an organization on a Zulip server (e.g. chat.zulip.org or yourSubdomain.zulipchat.com, or your own development server). Within that Zulip organization, users will be able to interact with your bot.
- A computer where you're running the bot from.
Note: Please be considerate when testing experimental bots on public servers such as chat.zulip.org.
-
Register a new bot user on the Zulip server's web interface.
- Log in to the Zulip server.
- Navigate to Settings () -> Your bots -> Add a new bot. Select Generic bot for bot type, fill out the form and click on Create bot.
- A new bot user should appear in the Active bots panel.
-
Download the bot's
.zuliprc
configuration file to your computer.- In the Active bots panel, click on the little green download icon to download its configuration file .zuliprc (the structure of this file is explained here).
- Copy the file to a destination of your choice, e.g. to
~/.zuliprc
.
-
Run the bot.
-
Run
zulip-run-bot <bot-name> --config-file ~/.zuliprc
(using the path to the
.zuliprc
file from step 3). -
Check the output of the command. It should start with the text the
usage
function returns, followed by logging output similar to this:INFO:root:starting message handling... INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost
-
Congrats! Now, your bot should be ready to test.
-
Testing the helloworld bot
- The
helloworld
bot is a simple bot that responds with a 'beep boop' when queried. It can be used as a template to build more complex bots. - In a stream and topic of your choice, type
@<your bot name>
. Thehelloworld
bot should respond with "beep boop".
Testing a bot's output
If you just want to see how a bot reacts to a message, but don't want to set it up on a server,
we have a little tool to help you out: zulip-bot-output
-
Run
zulip-bot-output <bot-name> --message "<your-message>"
to test one of the bots inzulip_bots/bots
-
Example:
zulip-bot-output converter --message "12 meter yard"
Response:
12.0 meter = 13.12336 yard
-
-
Run
zulip-bot-output <path/to/bot.py> --message "<your-message>"
to specify the bot's path yourself.-
Example:
zulip-bot-output zulip_bots/zulip_bots/bots/converter/converter.py --message "12 meter yard"
Response:
12.0 meter = 13.12336 yard
-
Zulip Botserver
The Zulip Botserver is for people who want to
- run bots in production.
- run multiple bots at once.
The Zulip Botserver is a Python (Flask) server that implements Zulip's Outgoing Webhooks API. You can of course write your own servers using the Outgoing Webhooks API, but the Botserver is designed to make it easy for a novice Python programmer to write a new bot and deploy it in production.
Installing the Zulip Botserver
Install the zulip_botserver
PyPI package using pip
:
pip install zulip_botserver
Running bots using the Zulip Botserver
-
Register new bot users on the Zulip server's web interface.
- Log in to the Zulip server.
- Navigate to Settings () -> Your bots -> Add a new bot. Select Outgoing webhook for bot type, fill out the form and click on Create bot.
- A new bot user should appear in the Active bots panel.
-
Download the
flaskbotrc
from theyour-bots
settings page. It contains the configuration details for all the active outgoing webhook bots. It's structure is very similar to that of .zuliprc. -
Run the Zulip Botserver by passing the
flaskbotrc
to it. The command format is:zulip-bot-server --config-file <path_to_flaskbotrc> --hostname <address> --port <port>
If omitted,
hostname
defaults to127.0.0.1
andport
to5002
. -
Now set up the outgoing webhook service which will interact with the server: Create an Outgoing webhook bot with its base url of the form:
http://<hostname>:<port>/bots/<bot_name>
bot_name
refers to the name in the email address you specified for the bot. It can be obtained by removing-bot@*.*
from the bot email: For example, the bot name of a bot with an emailfollowup-bot@zulip.com
isfollowup
.In the development environment, an outgoing webhook bot and corresponding service already exist, with the email
outgoing-webhook@zulip.com
. This can be used for interacting with flask server bots. -
Congrats, everything is set up! Test your botserver like you would test a normal bot.
Running Zulip Botserver with supervisord
supervisord is a popular tool for running services in production. It helps ensure the service starts on boot, manages log files, restarts the service if it crashes, etc. This section documents how to run the Zulip Botserver using supervisord.
Running the Zulip Botserver with supervisord works almost like running it manually.
-
Install supervisord via your package manager; e.g. on Debian/Ubuntu:
sudo apt-get install supervisor
-
Configure supervisord. supervisord stores its configuration in
/etc/supervisor/conf.d
.- Do one of the following:
- Download the sample config file
and store it in
/etc/supervisor/conf.d/zulip-botserver.conf
. - Copy the following section into your existing supervisord config file.
[program:zulip-bot-server] command=zulip-bot-server --config-file=<path/to/your/flaskbotrc> --hostname <address> --port <port> startsecs=3 stdout_logfile=/var/log/zulip-botserver.log ; all output of your botserver will be logged here redirect_stderr=true
- Download the sample config file
and store it in
- Edit the
<>
sections according to your preferences.
- Do one of the following:
-
Update supervisord to read the configuration file:
supervisorctl reread supervisorctl update
(or you can use
/etc/init.d/supervisord restart
, but this is less disruptive if you're using supervisord for other services as well). -
Test if your setup is successful:
supervisorctl status
The output should include a line similar to this:
zulip-bot-server RUNNING pid 28154, uptime 0:00:27
The standard output of the bot server will be logged to the path in your supervisord configuration.
How to develop a bot
The tutorial below explains the structure of a bot <my-bot>.py
,
which is the only file you need to create for a new bot. You
can use this as boilerplate code for developing your own bot.
Every bot is built upon this structure:
class MyBotHandler(object):
'''
A docstring documenting this bot.
'''
def usage(self):
return '''Your description of the bot'''
def handle_message(self, message, bot_handler, state_handler):
# add your code here
handler_class = MyBotHandler
-
The class name (in this case MyBotHandler) can be defined by you and should match the name of your bot. To register your bot's class, adjust the last line
handler_class = MyBotHandler
to match your class name. -
Every bot needs to implement the functions
usage(self)
handle_message(self, message, bot_handler)
-
These functions are documented in the next section.
Bot API
This section documents functions available to the bot and the structure of the bot's config file.
With this API, you can
- intercept, view, and process messages sent by users on Zulip.
- send out new messages as replies to the processed messages.
With this API, you cannot
- modify an intercepted message (you have to send a new message).
- send messages on behalf of or impersonate other users.
- intercept private messages (except for PMs with the bot as an explicit recipient).
usage
usage(self)
is called to retrieve information about the bot.
Arguments
- self - the instance the method is called on.
Return values
- A string describing the bot's functionality
Example implementation
def usage(self):
return '''
This plugin will allow users to flag messages
as being follow-up items. Users should preface
messages with "@followup".
Before running this, make sure to create a stream
called "followup" that your API user can send to.
'''
handle_message
handle_message(self, message, bot_handler)
handles user message.
Arguments
-
self - the instance the method is called on.
-
message - a dictionary describing a Zulip message
-
bot_handler - used to interact with the server, e.g. to send a message
-
state_handler - used to save states/information of the bot beta
- use
state_handler.set_state(state)
to set a state (any object) - use
state_handler.get_state()
to retrieve the state set; returns aNoneType
object if no state is set
- use
Return values
None.
Example implementation
def handle_message(self, message, bot_handler, state_handler):
original_content = message['content']
original_sender = message['sender_email']
new_content = original_content.replace('@followup',
'from %s:' % (original_sender,))
bot_handler.send_message(dict(
type='stream',
to='followup',
subject=message['sender_email'],
content=new_content,
))
bot_handler.send_message
bot_handler.send_message(message)
will send a message as the bot user. Generally, this is less convenient than send_reply, but it offers additional flexibility about where the message is sent to.
Arguments
- message - a dictionary describing the message to be sent by the bot
Example implementation
bot_handler.send_message(dict(
type='stream', # can be 'stream' or 'private'
to=stream_name, # either the stream name or user's email
subject=subject, # message subject
content=message, # content of the sent message
))
bot_handler.send_reply
bot_handler.send_reply(message, response)
will reply to the triggering message to the same place the original message was sent to, with the content of the reply being response.
Arguments
- message - Dictionary containing information on message to respond to
(provided by
handle_message
). - response - Response message from the bot (string).
bot_handler.update_message
bot_handler.update_message(message)
will edit the content of a previously sent message.
Arguments
- message - dictionary defining what message to edit and the new content
Example
From zulip_bots/bots/incrementor/incrementor.py
:
bot_handler.update_message(dict(
message_id=self.message_id, # id of message to be updated
content=str(self.number), # string with which to update message with
))
Configuration file
[api]
key=<api-key>
email=<email>
site=<dev-url>
-
key - the API key you created for the bot; this is how Zulip knows the request is from an authorized user.
-
email - the email address of the bot, e.g.
some-bot@zulip.com
-
site - your development environment URL; if you are working on a development environment hosted on your computer, use
localhost:9991
Writing tests for bots
Bots, like most software that you want to work, should have unit tests. In this section,
we detail our framework for writing unit tests for bots. We require that bots in the main
python-zulip-api
repository include a reasonable set of unit tests, so that future developers can easily
refactor them.
Unit tests for bots make heavy use of mocking. If you want to get comfortable with mocking, mocking strategies, etc. you should check out our mocking guide.
A simple example
Let's have a look at a simple test suite for the helloworld
bot (the actual test is written slightly more compact).
from __future__ import absolute_import
from zulip_bots.test_lib import BotTestCase # The test system library
class TestHelloWorldBot(BotTestCase):
bot_name = "helloworld" # The bot's name (should be the name of the bot module to test).
def test_bot(self): # A test case (must start with `test`)
# Messages we want to test and the expected bot responses.
message_response_pairs = {"" : "beep boop",
"foo" : "beep boop",
"Hi, my name is abc" : "beep boop"}
self.check_expected_responses(message_response_pairs) # Test the bot with our message_response_pair dict.
The helloworld
bot replies with "beep boop" to every message @-mentioning it.
Note that our helper method check_expected_responses
adds the @-mention for us - the only
thing we need to do is to specify the rest of the message and the expected response. In this
case, we want to assert that the bot always replies with "beep boop". To do so, we specify
several test messages ("", "foo", "Hi, my name is abc") and assert that the response is always
correct, which for this simple bot, means always sending a reply with the content "beep boop".
Test your test
Once you have written a test suite, you want to verify that everything works as expected.
-
To test a bot in Zulip's bot directory:
tools/test-bots <botname>
-
To run any test:
python -m unittest -v <package.bot_test>
-
To run all bot tests:
tools/test-bots
Advanced testing
This section shows advanced testing techniques for more complicated bots that have configuration files or interact with third-party APIs. The code for the bot testing library can be found here.
Asserting individual messages
self.assert_bot_response(
message = {'content': 'foo'},
response = {'content': 'bar'},
expected_method='send_reply'
)
Use assert_bot_response()
to test individual messages. Specify additional message
settings, such as the stream or subject, in the message
and response
dicts.
Testing bots with config files
Some bots, such as Giphy, support or require user configuration options to control how the bot works. To test such a bot, you can use the following helper method:
with self.mock_config_info({'entry': 'value'}):
# self.assert_bot_response(...)
mock_config_info()
mocks a bot's config file. All config files are specified in the
.ini format, with one default section. The dict passed to mock_config_info()
specifies
the keys and values of that section.
Testing bots with internet access
Some bots, such as Giphy, depend on a third-party we service, such as the Giphy webapp, in order to work. Because we want our test suite to be reliable and not add load to these third-party APIs, tests for these services need to have "test fixtures": sample HTTP request/response pairs to be used by the tests. You can specify which one to use in your test code using the following helper method:
with self.mock_http_conversation('test_fixture_name'):
# self.assert_bot_response(...)
mock_http_conversation(fixture_name)
patches requests.get
and returns the data specified
in the file fixtures/<fixture_name>.py
. For an example, check out the giphy bot.
Tip: You can use requestb.in or a similar tool to capture payloads from the service your bot is interacting with.
Testing bots that specify initialize()
Some bots, such as Giphy,
implement an initialize()
method, which is executed on the startup of the bot. To test
such a bot, you can call its initialize()
method with the following helper method:
self.initialize_bot()
Calling initialize_bot()
invokes the initialize()
method specified by the bot.
Examples
Check out our bots to see examples of bot tests.
Common problems
-
I modified my bot's code, yet the changes don't seem to have an effect.
- Ensure that you restarted the
zulip-run-bot
script.
- Ensure that you restarted the
-
My bot won't start
- Ensure that your API config file is correct (download the config file from the server).
- Ensure that you bot script is located in zulip_bots/bots//
- Are you using your own Zulip development server? Ensure that you run your bot outside the Vagrant environment.
- Some bots require Python 3. Try switching to a Python 3 environment before running your bot.
Future direction
The long-term plan for this bot system is to allow the same
ExternalBotHandler
code to eventually be usable in several contexts:
- Run directly using the Zulip
call_on_each_message
API, which is how the implementation above works. This is great for quick development with minimal setup. - Run in a simple Python webserver server, processing messages received from Zulip's outgoing webhooks integration.
- For bots merged into the mainline Zulip codebase, enabled via a button in the Zulip web UI, with no code deployment effort required.