bots: Add example bots for "followup" and "help".
This commit also starts to build out the infrastructure for helping Zulip contributors to more easily author bots in a way that sets up for running some bots on the server itself.
This commit is contained in:
parent
8d75662c7c
commit
38c7b611b6
0
contrib_bots/lib/__init__.py
Normal file
0
contrib_bots/lib/__init__.py
Normal file
53
contrib_bots/lib/followup.py
Normal file
53
contrib_bots/lib/followup.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# See readme.md for instructions on running this code.
|
||||||
|
|
||||||
|
class FollowupHandler(object):
|
||||||
|
'''
|
||||||
|
This plugin facilitates creating follow-up tasks when
|
||||||
|
you are using Zulip to conduct a virtual meeting. It
|
||||||
|
looks for messages starting with '@followup'.
|
||||||
|
|
||||||
|
In this example, we write follow up items to a special
|
||||||
|
Zulip stream called "followup," but this code could
|
||||||
|
be adapted to write follow up items to some kind of
|
||||||
|
external issue tracker as well.
|
||||||
|
'''
|
||||||
|
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def triage_message(self, message):
|
||||||
|
# return True iff we want to (possibly) response to this message
|
||||||
|
|
||||||
|
original_content = message['content']
|
||||||
|
|
||||||
|
# This next line of code is defensive, as we
|
||||||
|
# never want to get into an infinite loop of posting follow
|
||||||
|
# ups for own follow ups!
|
||||||
|
if message['display_recipient'] == 'followup':
|
||||||
|
return False
|
||||||
|
is_follow_up = (original_content.startswith('@followup') or
|
||||||
|
original_content.startswith('@follow-up'))
|
||||||
|
|
||||||
|
return is_follow_up
|
||||||
|
|
||||||
|
def handle_message(self, message, client):
|
||||||
|
original_content = message['content']
|
||||||
|
original_sender = message['sender_email']
|
||||||
|
new_content = original_content.replace('@followup',
|
||||||
|
'from %s:' % (original_sender,))
|
||||||
|
|
||||||
|
client.send_message(dict(
|
||||||
|
type='stream',
|
||||||
|
to='followup',
|
||||||
|
subject=message['sender_email'],
|
||||||
|
content=new_content,
|
||||||
|
))
|
||||||
|
|
||||||
|
handler_class = FollowupHandler
|
39
contrib_bots/lib/help.py
Normal file
39
contrib_bots/lib/help.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# See readme.md for instructions on running this code.
|
||||||
|
|
||||||
|
class HelpHandler(object):
|
||||||
|
def usage(self):
|
||||||
|
return '''
|
||||||
|
This plugin will give info about Zulip to
|
||||||
|
any user that types a message saying "help".
|
||||||
|
|
||||||
|
This is example code; ideally, you would flesh
|
||||||
|
this out for more useful help pertaining to
|
||||||
|
your Zulip instance.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def triage_message(self, message):
|
||||||
|
# return True if we think the message may be of interest
|
||||||
|
original_content = message['content']
|
||||||
|
|
||||||
|
if message['type'] != 'stream':
|
||||||
|
return True
|
||||||
|
|
||||||
|
if original_content.lower().strip() != 'help':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_message(self, message, client):
|
||||||
|
help_content = '''
|
||||||
|
Info on Zulip can be found here:
|
||||||
|
https://github.com/zulip/zulip
|
||||||
|
'''.strip()
|
||||||
|
|
||||||
|
client.send_message(dict(
|
||||||
|
type='stream',
|
||||||
|
to=message['display_recipient'],
|
||||||
|
subject=message['subject'],
|
||||||
|
content=help_content,
|
||||||
|
))
|
||||||
|
|
||||||
|
handler_class = HelpHandler
|
78
contrib_bots/lib/readme.md
Normal file
78
contrib_bots/lib/readme.md
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
This directory contains library code for running Zulip
|
||||||
|
bots that react to messages sent by users.
|
||||||
|
|
||||||
|
This document explains how to run the code, and it also
|
||||||
|
talks about the architecture for creating bots.
|
||||||
|
|
||||||
|
## Running bots
|
||||||
|
|
||||||
|
Here is an example of running the "follow-up" bot from
|
||||||
|
inside a Zulip repo:
|
||||||
|
|
||||||
|
cd ~/zulip/contrib_bots
|
||||||
|
python run.py lib/followup.py
|
||||||
|
|
||||||
|
Once the bot code starts running, you will see a
|
||||||
|
message explaining how to use the bot, as well as
|
||||||
|
some log messages. You can use the `--quiet` option
|
||||||
|
to suppress these messages.
|
||||||
|
|
||||||
|
The bot code will run continuously until you kill them with
|
||||||
|
control-C (or otherwise).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
In order to make bot development easy, we separate
|
||||||
|
out boilerplate code (loading up the Client API, etc.)
|
||||||
|
from bot-specific code (do what makes the bot unique).
|
||||||
|
|
||||||
|
All of the boilerplate code lives in `../run.py`. The
|
||||||
|
runner code does things like find where it can import
|
||||||
|
the Zulip API, instantiate a client with correct
|
||||||
|
credentials, set up the logging level, find the
|
||||||
|
library code for the specific bot, etc.
|
||||||
|
|
||||||
|
Then, for bot-specific logic, you will find `.py` files
|
||||||
|
in the `lib` directory (i.e. the same directory as the
|
||||||
|
document you are reading now).
|
||||||
|
|
||||||
|
Each bot library simply needs to do the following:
|
||||||
|
|
||||||
|
- Define a class that supports the methods `usage`,
|
||||||
|
`triage_message`, and `handle_message`.
|
||||||
|
- Set `handler_class` to be the name of that class.
|
||||||
|
|
||||||
|
(We make this a two-step process, so that you can give
|
||||||
|
a descriptive name to your handler class.)
|
||||||
|
|
||||||
|
## Portability
|
||||||
|
|
||||||
|
Creating a handler class for each bot allows your bot
|
||||||
|
code to be more portable. For example, if you want to
|
||||||
|
use your bot code in some other kind of bot platform, then
|
||||||
|
if all of your bots conform to the `handler_class` protocol,
|
||||||
|
you can write simple adapter code to use them elsewhere.
|
||||||
|
|
||||||
|
Another future direction to consider is that Zulip will
|
||||||
|
eventually support running certain types of bots on
|
||||||
|
the server side, to essentially implement post-send
|
||||||
|
hooks and things of those nature.
|
||||||
|
|
||||||
|
Conforming to the `handler_class` protocol will make
|
||||||
|
it easier for Zulip admins to integrate custom bots.
|
||||||
|
|
||||||
|
In particular, `run.py` already passes in instances
|
||||||
|
of a restricted variant of the Client class to your
|
||||||
|
library code, which helps you ensure that your bot
|
||||||
|
does only things that would be acceptable for running
|
||||||
|
in a server-side environment.
|
||||||
|
|
||||||
|
## Other approaches
|
||||||
|
|
||||||
|
If you are not interested in running your bots on the
|
||||||
|
server, then you can still use the full Zulip API. The
|
||||||
|
hope, though, is that this architecture will make
|
||||||
|
writing simple bots a quick/easy process.
|
||||||
|
|
93
contrib_bots/run.py
Normal file
93
contrib_bots/run.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
our_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# For dev setups, we can find the API in the repo itself.
|
||||||
|
if os.path.exists(os.path.join(our_dir, '../api/zulip')):
|
||||||
|
sys.path.append('../api')
|
||||||
|
|
||||||
|
from zulip import Client
|
||||||
|
|
||||||
|
class RestrictedClient(object):
|
||||||
|
def __init__(self, client):
|
||||||
|
# Only expose a subset of our Client's functionality
|
||||||
|
self.send_message = client.send_message
|
||||||
|
|
||||||
|
def get_lib_module(lib_fn):
|
||||||
|
lib_fn = os.path.abspath(lib_fn)
|
||||||
|
if os.path.dirname(lib_fn) != os.path.join(our_dir, 'lib'):
|
||||||
|
print('Sorry, we will only import code from contrib_bots/lib.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not lib_fn.endswith('.py'):
|
||||||
|
print('Please use a .py extension for library files.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.path.append('lib')
|
||||||
|
base_lib_fn = os.path.basename(os.path.splitext(lib_fn)[0])
|
||||||
|
module_name = 'lib.' + base_lib_fn
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
return module
|
||||||
|
|
||||||
|
def run_message_handler_for_bot(lib_module, quiet):
|
||||||
|
# Make sure you set up your ~/.zuliprc
|
||||||
|
client = Client()
|
||||||
|
restricted_client = RestrictedClient(client)
|
||||||
|
|
||||||
|
message_handler = lib_module.handler_class()
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
print(message_handler.usage())
|
||||||
|
|
||||||
|
def handle_message(message):
|
||||||
|
logging.info('waiting for next message')
|
||||||
|
if message_handler.triage_message(message=message):
|
||||||
|
message_handler.handle_message(
|
||||||
|
message=message,
|
||||||
|
client=restricted_client)
|
||||||
|
|
||||||
|
logging.info('starting message handling...')
|
||||||
|
client.call_on_each_message(handle_message)
|
||||||
|
|
||||||
|
def run():
|
||||||
|
usage = '''
|
||||||
|
python run.py <lib file>
|
||||||
|
|
||||||
|
Example: python run.py lib/followup.py
|
||||||
|
|
||||||
|
(This program loads bot-related code from the
|
||||||
|
library code and then runs a message loop,
|
||||||
|
feeding messages to the library code to handle.)
|
||||||
|
|
||||||
|
Please make sure you have a current ~/.zuliprc
|
||||||
|
file with the credentials you want to use for
|
||||||
|
this bot.
|
||||||
|
|
||||||
|
See lib/readme.md for more context.
|
||||||
|
'''
|
||||||
|
|
||||||
|
parser = optparse.OptionParser(usage=usage)
|
||||||
|
parser.add_option('--quiet', '-q',
|
||||||
|
action='store_true',
|
||||||
|
help='Turn off logging output.')
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
if len(args) == 0:
|
||||||
|
print('You must specify a library!')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
lib_module = get_lib_module(lib_fn=args[0])
|
||||||
|
|
||||||
|
if not options.quiet:
|
||||||
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||||
|
|
||||||
|
run_message_handler_for_bot(lib_module, quiet=options.quiet)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
Loading…
Reference in a new issue