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