2017-10-04 21:20:17 +02:00
|
|
|
|
# Writing interactive bots
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 21:20:17 +02:00
|
|
|
|
Zulip's API supports a few different ways of integrating with a
|
|
|
|
|
third-party service.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 21:20:17 +02:00
|
|
|
|
* **Incoming webhook integrations**, for when you just want notifications from
|
2017-11-16 17:36:52 +01:00
|
|
|
|
a tool to be sent into Zulip. See the [integrations guide](integration-guide).
|
2017-10-04 21:20:17 +02:00
|
|
|
|
* **Interactive bots**, for when you want the tool to react to
|
|
|
|
|
messages in Zulip.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 21:20:17 +02:00
|
|
|
|
* This guide is about writing and testing interactive bots. We assume
|
|
|
|
|
familiarity with our
|
2017-11-15 11:49:15 +01:00
|
|
|
|
[guide for running bots](running-bots).
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
|
|
|
|
On this page you'll find:
|
2017-11-15 11:49:15 +01:00
|
|
|
|
|
2017-10-04 21:20:17 +02:00
|
|
|
|
* A step-by-step
|
2017-11-17 19:58:55 +01:00
|
|
|
|
[guide](#installing-a-development-version-of-the-zulip-bots-package)
|
2017-10-04 21:20:17 +02:00
|
|
|
|
on how to set up a development environment for writing bots with all
|
|
|
|
|
of our nice tooling to make it easy to write and test your work.
|
2017-10-04 18:01:31 +02:00
|
|
|
|
* A [guide](#writing-a-bot) on writing a bot.
|
|
|
|
|
* A [guide](#adding-a-bot-to-zulip) on adding a bot to Zulip.
|
2017-11-15 19:12:50 +01:00
|
|
|
|
* A [guide](#testing-a-bots-output) on testing a bot's output.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
* A [documentation](#bot-api) of the bot API.
|
2017-05-21 23:19:07 +02:00
|
|
|
|
* Common [problems](#common-problems) when developing/running bots and their solutions.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-11-17 14:12:37 +01:00
|
|
|
|
## Installing a development version of the Zulip bots package
|
2017-10-04 18:01:31 +02:00
|
|
|
|
|
|
|
|
|
1. `git clone https://github.com/zulip/python-zulip-api.git` - clone the [python-zulip-api](
|
|
|
|
|
https://github.com/zulip/python-zulip-api) repository.
|
|
|
|
|
|
|
|
|
|
2. `cd python-zulip-api` - navigate into your cloned repository.
|
|
|
|
|
|
|
|
|
|
3. `./tools/provision` - install all requirements in a Python virtualenv.
|
|
|
|
|
|
2017-10-04 21:20:17 +02:00
|
|
|
|
4. Run the `source <activation/path>` command printed in the previous
|
|
|
|
|
step to activate the virtualenv.
|
2017-10-04 18:01:31 +02:00
|
|
|
|
|
|
|
|
|
5. *Finished*. You should now see the name of your venv preceding your prompt, e.g. `(ZULIP-~1)`.
|
|
|
|
|
|
2017-10-04 21:20:17 +02:00
|
|
|
|
*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.*
|
2017-10-04 18:01:31 +02:00
|
|
|
|
|
|
|
|
|
## Writing a bot
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
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.
|
|
|
|
|
'''
|
|
|
|
|
|
2017-11-27 14:19:13 +01:00
|
|
|
|
def usage(self) -> str:
|
2017-10-04 18:01:31 +02:00
|
|
|
|
return '''Your description of the bot'''
|
|
|
|
|
|
2017-11-27 14:19:13 +01:00
|
|
|
|
def handle_message(self, message, bot_handler) -> None:
|
2017-10-04 18:01:31 +02:00
|
|
|
|
# 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).
|
|
|
|
|
|
|
|
|
|
## Adding a bot to Zulip
|
2017-05-21 23:19:07 +02:00
|
|
|
|
|
2017-08-22 16:38:36 +02:00
|
|
|
|
Zulip's bot system resides in the [python-zulip-api](
|
|
|
|
|
https://github.com/zulip/python-zulip-api) repository.
|
2017-05-21 23:19:07 +02:00
|
|
|
|
|
2017-08-22 16:38:36 +02:00
|
|
|
|
The structure of the bots ecosystem looks like the following:
|
2017-05-21 23:19:07 +02:00
|
|
|
|
|
|
|
|
|
```
|
2017-08-22 16:38:36 +02:00
|
|
|
|
zulip_bots
|
|
|
|
|
└───zulip_bots
|
|
|
|
|
├───bots
|
|
|
|
|
│ ├───bot1
|
|
|
|
|
│ └───bot2
|
|
|
|
|
│ │
|
|
|
|
|
│ ├───bot2.py
|
2017-09-06 14:54:34 +02:00
|
|
|
|
│ ├───bot2.conf
|
|
|
|
|
│ ├───doc.md
|
2017-11-21 16:36:17 +01:00
|
|
|
|
│ ├───requirements.txt
|
2017-09-06 14:54:34 +02:00
|
|
|
|
│ ├───test_bot2.py
|
|
|
|
|
│ ├───assets
|
2017-08-22 16:38:36 +02:00
|
|
|
|
│ │ │
|
2017-09-06 14:54:34 +02:00
|
|
|
|
│ │ └───pic.png
|
|
|
|
|
│ ├───fixtures
|
|
|
|
|
│ │ │
|
|
|
|
|
│ │ └───test1.json
|
|
|
|
|
│ └───libraries
|
2017-08-22 16:38:36 +02:00
|
|
|
|
│ │
|
2017-09-06 14:54:34 +02:00
|
|
|
|
│ └───lib1.py
|
2017-08-22 16:38:36 +02:00
|
|
|
|
├─── lib.py
|
|
|
|
|
├─── test_lib.py
|
|
|
|
|
├─── run.py
|
|
|
|
|
└─── provision.py
|
2017-05-21 23:19:07 +02:00
|
|
|
|
```
|
2017-05-05 22:23:15 +02:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
Each subdirectory in `bots` contains a bot. When writing bots, try to use the structure outlined
|
2017-05-21 23:19:07 +02:00
|
|
|
|
above as an orientation.
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
## Testing a bot's output
|
2017-09-01 17:27:50 +02:00
|
|
|
|
|
|
|
|
|
If you just want to see how a bot reacts to a message, but don't want to set it up on a server,
|
2017-12-12 01:22:39 +01:00
|
|
|
|
we have a little tool to help you out: `zulip-terminal`
|
2017-09-01 17:27:50 +02:00
|
|
|
|
|
2017-11-18 00:55:42 +01:00
|
|
|
|
* [Install all requirements](#installing-a-development-version-of-the-zulip-bots-package).
|
2017-09-01 17:27:50 +02:00
|
|
|
|
|
2017-12-12 01:22:39 +01:00
|
|
|
|
* Run `zulip-terminal -b <path-to-bot.config> <bot-name>` to test one of the bots in
|
|
|
|
|
[`zulip_bots/bots`](https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots).
|
|
|
|
|
Here, the `-b` or `--bot-config-file` arguement is for optional third party config file (e.g. ~/giphy.conf)
|
|
|
|
|
|
|
|
|
|
* Example: `zulip-terminal converter`
|
|
|
|
|
```
|
|
|
|
|
Enter your message: "12 meter yard"
|
|
|
|
|
Response: 12.0 meter = 13.12336 yard
|
|
|
|
|
```
|
|
|
|
|
* Example: `zulip-terminal -b ~/followup.conf followup`
|
|
|
|
|
```
|
|
|
|
|
Enter your message: "Task Completed"
|
|
|
|
|
Response: stream: followup topic: foo_sender@zulip.com
|
|
|
|
|
from foo_sender@zulip.com: Task Completed
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
* Run `zulip-terminal <path/to/bot.py>"` to specify the bot's path yourself.
|
|
|
|
|
* Example: `zulip-terminal zulip_bots/zulip_bots/bots/converter/converter.py`
|
|
|
|
|
```
|
|
|
|
|
Enter your message: "12 meter yard"
|
|
|
|
|
Response: 12.0 meter = 13.12336 yard
|
|
|
|
|
```
|
2017-09-01 17:27:50 +02:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
## Bot API
|
2017-01-05 23:23:16 +01:00
|
|
|
|
|
2017-05-25 04:12:34 +02:00
|
|
|
|
This section documents functions available to the bot and the structure of the bot's config file.
|
2017-05-21 23:19:07 +02:00
|
|
|
|
|
|
|
|
|
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).
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### usage
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
*usage(self)*
|
|
|
|
|
|
|
|
|
|
is called to retrieve information about the bot.
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Arguments
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
* self - the instance the method is called on.
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Return values
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
* A string describing the bot's functionality
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Example implementation
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
```
|
2017-11-27 14:19:13 +01:00
|
|
|
|
def usage(self) -> str:
|
2016-12-18 16:11:46 +01:00
|
|
|
|
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.
|
|
|
|
|
'''
|
|
|
|
|
```
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### handle_message
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2017-06-11 15:03:39 +02:00
|
|
|
|
*handle_message(self, message, bot_handler)*
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-03-05 14:52:12 +01:00
|
|
|
|
handles user message.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Arguments
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
* self - the instance the method is called on.
|
|
|
|
|
|
2016-12-20 04:30:25 +01:00
|
|
|
|
* message - a dictionary describing a Zulip message
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-06-11 15:03:39 +02:00
|
|
|
|
* bot_handler - used to interact with the server, e.g. to send a message
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Return values
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
None.
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Example implementation
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
|
|
|
|
```
|
2017-11-27 14:19:13 +01:00
|
|
|
|
def handle_message(self, message, bot_handler) -> None:
|
2016-12-18 16:11:46 +01:00
|
|
|
|
original_content = message['content']
|
|
|
|
|
original_sender = message['sender_email']
|
|
|
|
|
new_content = original_content.replace('@followup',
|
|
|
|
|
'from %s:' % (original_sender,))
|
|
|
|
|
|
2017-06-11 15:03:39 +02:00
|
|
|
|
bot_handler.send_message(dict(
|
2016-12-18 16:11:46 +01:00
|
|
|
|
type='stream',
|
|
|
|
|
to='followup',
|
|
|
|
|
subject=message['sender_email'],
|
|
|
|
|
content=new_content,
|
|
|
|
|
))
|
|
|
|
|
```
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### bot_handler.send_message
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2017-06-11 15:03:39 +02:00
|
|
|
|
*bot_handler.send_message(message)*
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2017-09-15 16:03:22 +02:00
|
|
|
|
#### Arguments
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
* message - a dictionary describing the message to be sent by the bot
|
|
|
|
|
|
2017-09-15 16:03:22 +02:00
|
|
|
|
#### Example implementation
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
```
|
2017-06-11 15:03:39 +02:00
|
|
|
|
bot_handler.send_message(dict(
|
2017-05-25 04:12:34 +02:00
|
|
|
|
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
|
|
|
|
|
))
|
|
|
|
|
```
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### bot_handler.send_reply
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2017-06-11 15:03:39 +02:00
|
|
|
|
*bot_handler.send_reply(message, response)*
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
will reply to the triggering message to the same place the original
|
|
|
|
|
message was sent to, with the content of the reply being *response*.
|
|
|
|
|
|
2017-09-15 16:03:22 +02:00
|
|
|
|
#### Arguments
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
* message - Dictionary containing information on message to respond to
|
|
|
|
|
(provided by `handle_message`).
|
|
|
|
|
* response - Response message from the bot (string).
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### bot_handler.update_message
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2017-06-11 15:03:39 +02:00
|
|
|
|
*bot_handler.update_message(message)*
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
will edit the content of a previously sent message.
|
|
|
|
|
|
2017-09-15 16:03:22 +02:00
|
|
|
|
#### Arguments
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
* message - dictionary defining what message to edit and the new content
|
|
|
|
|
|
2017-09-15 16:03:22 +02:00
|
|
|
|
#### Example
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
2017-08-22 16:38:36 +02:00
|
|
|
|
From `zulip_bots/bots/incrementor/incrementor.py`:
|
2017-05-25 04:12:34 +02:00
|
|
|
|
|
|
|
|
|
```
|
2017-06-11 15:03:39 +02:00
|
|
|
|
bot_handler.update_message(dict(
|
2017-05-25 04:12:34 +02:00
|
|
|
|
message_id=self.message_id, # id of message to be updated
|
|
|
|
|
content=str(self.number), # string with which to update message with
|
|
|
|
|
))
|
|
|
|
|
```
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-24 15:57:44 +02:00
|
|
|
|
### bot_handler.storage
|
|
|
|
|
|
|
|
|
|
**Note: This feature is under development. Permanent storage in the
|
|
|
|
|
database of a Zulip server is currently only supported for Zulip's embedded
|
|
|
|
|
bots. For external bots, all data stored with this feature is lost with
|
|
|
|
|
the termination of a bot.**
|
|
|
|
|
|
|
|
|
|
A common problem when writing an interactive bot is that you want to
|
|
|
|
|
be able to store a bit of persistent state for the bot (e.g. for an
|
|
|
|
|
RSVP bot, the RSVPs). For a sufficiently complex bot, you want need
|
|
|
|
|
your own database, but for simpler bots, we offer a convenient way for
|
|
|
|
|
bot code to persistently store data.
|
|
|
|
|
|
|
|
|
|
The interface for doing this is `bot_handler.storage`.
|
|
|
|
|
|
|
|
|
|
The data is stored in the Zulip Server's database. Each bot user has
|
|
|
|
|
an independent storage quota available to it.
|
|
|
|
|
|
|
|
|
|
#### Performance considerations
|
|
|
|
|
|
|
|
|
|
Since each access to `bot_handler.storage` will involve a round-trip
|
|
|
|
|
to the server, we recommend writing bots so that they do a single
|
|
|
|
|
`bot_handler.storage.get` at the start of `handle_message`, and a
|
|
|
|
|
single `bot_handler.storage.put` at the end to submit the state to the
|
|
|
|
|
server. We plan to offer a context manager that takes care of this
|
|
|
|
|
automatically.
|
|
|
|
|
|
|
|
|
|
#### bot_handler.storage.put
|
|
|
|
|
|
|
|
|
|
*bot_handler.storage.put(key, value)*
|
|
|
|
|
|
|
|
|
|
will store the value `value` in the entry `key`.
|
|
|
|
|
|
|
|
|
|
##### Arguments
|
|
|
|
|
|
|
|
|
|
* key - a UTF-8 string
|
|
|
|
|
* value - a UTF-8 string
|
|
|
|
|
|
|
|
|
|
##### Example
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
bot_handler.storage.put("foo", "bar") # set entry "foo" to "bar"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### bot_handler.storage.get
|
|
|
|
|
|
|
|
|
|
*bot_handler.storage.get(key)*
|
|
|
|
|
|
|
|
|
|
will retrieve the value for the entry `key`.
|
|
|
|
|
|
|
|
|
|
###### Arguments
|
|
|
|
|
|
|
|
|
|
* key - a UTF-8 string
|
|
|
|
|
|
|
|
|
|
##### Example
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
bot_handler.storage.put("foo", "bar")
|
|
|
|
|
print(bot_handler.storage.get("foo")) # print "bar"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### bot_handler.storage.contains
|
|
|
|
|
|
|
|
|
|
*bot_handler.storage.contains(key)*
|
|
|
|
|
|
|
|
|
|
will check if the entry `key` exists.
|
|
|
|
|
|
|
|
|
|
##### Arguments
|
|
|
|
|
|
|
|
|
|
* key - a UTF-8 string
|
|
|
|
|
|
|
|
|
|
##### Example
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
bot_handler.storage.contains("foo") # False
|
|
|
|
|
bot_handler.storage.put("foo", "bar")
|
|
|
|
|
bot_handler.storage.contains("foo") # True
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### bot_handler.storage marshaling
|
|
|
|
|
|
|
|
|
|
By default, `bot_handler.storage` accepts any object for keys and
|
|
|
|
|
values, as long as it is JSON-able. Internally, the object then gets
|
|
|
|
|
converted to an UTF-8 string. You can specify custom data marshaling
|
|
|
|
|
by setting the functions `bot_handler.storage.marshal` and
|
|
|
|
|
`bot_handler.storage.demarshal`. These functions parse your data on
|
|
|
|
|
every call to `put` and `get`, respectively.
|
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
### Configuration file
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
[api]
|
|
|
|
|
key=<api-key>
|
|
|
|
|
email=<email>
|
|
|
|
|
site=<dev-url>
|
|
|
|
|
```
|
|
|
|
|
|
2017-01-05 23:23:16 +01:00
|
|
|
|
* key - the API key you created for the bot; this is how Zulip knows
|
|
|
|
|
the request is from an authorized user.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
|
|
|
|
* email - the email address of the bot, e.g. `some-bot@zulip.com`
|
|
|
|
|
|
2017-01-05 23:23:16 +01:00
|
|
|
|
* site - your development environment URL; if you are working on a
|
|
|
|
|
development environment hosted on your computer, use
|
|
|
|
|
`localhost:9991`
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
## Writing tests for bots
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
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`](https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots)
|
|
|
|
|
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](
|
2017-11-16 18:54:01 +01:00
|
|
|
|
https://zulip.readthedocs.io/en/latest/testing/testing-with-django.html#testing-with-mocks).*
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### A simple example
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
Let's have a look at a simple test suite for the [`helloworld`](
|
|
|
|
|
https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots/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).
|
|
|
|
|
|
2017-11-27 14:19:13 +01:00
|
|
|
|
def test_bot(self) -> None: # A test case (must start with `test`)
|
2017-09-15 16:05:48 +02:00
|
|
|
|
# Messages we want to test and the expected bot responses.
|
2017-10-24 15:56:15 +02:00
|
|
|
|
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 list.
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
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".
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### Testing your test
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
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](
|
|
|
|
|
https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots):
|
|
|
|
|
`tools/test-bots <botname>`
|
|
|
|
|
|
|
|
|
|
* To run any test: `python -m unittest -v <package.bot_test>`
|
|
|
|
|
|
|
|
|
|
* To run all bot tests: `tools/test-bots`
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
### Advanced testing
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
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](
|
|
|
|
|
https://github.com/zulip/python-zulip-api/blob/master/zulip_bots/zulip_bots/test_lib.py).*
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Asserting individual messages
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Testing bots with config files
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
Some bots, such as [Giphy](
|
|
|
|
|
https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots/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.
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Testing bots with internet access
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
Some bots, such as [Giphy](
|
|
|
|
|
https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots/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](
|
|
|
|
|
https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots/giphy).
|
|
|
|
|
|
|
|
|
|
*Tip: You can use [requestb.in](http://requestb.in) or a similar tool to capture payloads from the
|
|
|
|
|
service your bot is interacting with.*
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Testing bots that specify `initialize()`
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
Some bots, such as [Giphy](
|
|
|
|
|
https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots/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.
|
|
|
|
|
|
2017-10-04 18:01:31 +02:00
|
|
|
|
#### Examples
|
2017-09-15 16:05:48 +02:00
|
|
|
|
|
|
|
|
|
Check out our [bots](https://github.com/zulip/python-zulip-api/tree/master/zulip_bots/zulip_bots/bots)
|
|
|
|
|
to see examples of bot tests.
|
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
## Common problems
|
2017-01-05 23:23:16 +01:00
|
|
|
|
|
2016-12-18 16:11:46 +01:00
|
|
|
|
* I modified my bot's code, yet the changes don't seem to have an effect.
|
2017-09-28 10:17:37 +02:00
|
|
|
|
* Ensure that you restarted the `zulip-run-bot` script.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
|
|
|
|
* My bot won't start
|
2016-12-20 03:25:35 +01:00
|
|
|
|
* Ensure that your API config file is correct (download the config file from the server).
|
2017-08-22 16:38:36 +02:00
|
|
|
|
* Ensure that you bot script is located in zulip_bots/bots/<my-bot>/
|
2017-05-05 19:54:51 +02:00
|
|
|
|
* Are you using your own Zulip development server? Ensure that you run your bot outside
|
|
|
|
|
the Vagrant environment.
|
2017-05-23 00:28:18 +02:00
|
|
|
|
* Some bots require Python 3. Try switching to a Python 3 environment before running
|
2017-08-22 16:38:36 +02:00
|
|
|
|
your bot.
|
2016-12-18 16:11:46 +01:00
|
|
|
|
|
2017-05-05 22:26:57 +02:00
|
|
|
|
## Future direction
|
|
|
|
|
|
|
|
|
|
The long-term plan for this bot system is to allow the same
|
2017-08-22 16:38:36 +02:00
|
|
|
|
`ExternalBotHandler` code to eventually be usable in several contexts:
|
2017-05-05 22:26:57 +02:00
|
|
|
|
|
|
|
|
|
* 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.
|