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
					
				
			
		
					 5 changed files with 263 additions and 0 deletions
				
			
		contrib_bots
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue