Hey,

When I started playing with reviewboard for my team, I had an idea
kicking around in my head about writing a plugin for our IRC channel's
kibot instance which announced new review requests.  I put the
finishing touches on it today, and I'm releasing it under the MIT/X11
license.  Check it out.  If you end up using it, let me know.  We're
currently running it in irc.gimp.net, #mono-a11y.

Best,

-Brad

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"reviewboard" group.
To post to this group, send email to reviewboard@googlegroups.com
To unsubscribe from this group, send email to 
reviewboard+unsubscr...@googlegroups.com
For more options, visit this group at 
http://groups.google.com/group/reviewboard?hl=en
-~----------~----~----~----~------~----~------~--~---

#!/usr/bin/env python

# Permission is hereby granted, free of charge, to any person obtaining 
# a copy of this software and associated documentation files (the 
# "Software"), to deal in the Software without restriction, including 
# without limitation the rights to use, copy, modify, merge, publish, 
# distribute, sublicense, and/or sell copies of the Software, and to 
# permit persons to whom the Software is furnished to do so, subject to 
# the following conditions: 
#  
# The above copyright notice and this permission notice shall be 
# included in all copies or substantial portions of the Software. 
#  
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
# 
# Copyright (c) 2009 Novell, Inc. (http://www.novell.com) 
#
# Authors: 
#     Brad Taylor <b...@getcoded.net>

#
# reviewboard.py is a kibot plugin which interfaces with Review Board
# (http://www.review-board.org).  It offers a basic query interface and an
# optional notification of new review requests.
#
# This plugin has only been tested with kibot 0.0.12, so if you're using a
# different version, YMMV.
# 
# Installation:
#   - Modify the user-editable configuration section.
#   - Copy reviewboard.py into your kibot modules directory.  Usually this
#     will be /usr/share/kibot/modules or /usr/local/share/kibot/modules
#   - Use kibot-control to get into the administrative interface for kibot
#     and:
#         load reviewboard
#   - To configure new review request notification, in kibot-control, do:
#         set reviewboard.notify_channel #my-channel
#

# -- Begin user-editable configuration section --

# The URL to your review board installation. This will be used to do the JSON
# API calls and to search the commit log messages for review request links.
REVIEWBOARD_URL = None

# The timezone of your review board installation.
REVIEWBOARD_TIMEZONE = 'US/Pacific'

# The user to log in as.
USERNAME = ''
PASSWORD = ''

# How often to poll for new review requests in seconds.  If set to 0, the
# plugin will not watch for new review requests.
REVIEW_REQUEST_POLL_INTERVAL = 60

# -- End user-editable configuration section --

import cookielib
import mimetools
import re
import simplejson
import subprocess
import sys
import urllib2
from urlparse import urljoin

class APIError(Exception):
    pass

# This is shamelessly ripped off from post-review
class ReviewBoardServer:
    """
    An instance of a Review Board server.
    """
    def __init__(self, url):
        self.url = url
        if self.url[-1] != '/':
            self.url += '/'
        cj = cookielib.CookieJar()
        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
        urllib2.install_opener(opener)

    def login(self, username, password):
        """
        Logs in to a Review Board server, prompting the user for login
        information if needed.
        """
        try:
            self.api_post('api/json/accounts/login/', {
                'username': username,
                'password': password,
            })
        except APIError, e:
            rsp, = e.args

            die("Unable to log in: %s (%s)" % (rsp["err"]["msg"],
                                               rsp["err"]["code"]))

    def set_submitted(self, review_request_id):
        """
        Marks a review request as submitted.
        """
        self.api_post('api/json/reviewrequests/%s/close/submitted/' %
                      review_request_id)

    def process_json(self, data):
        """
        Loads in a JSON file and returns the data if successful. On failure,
        APIError is raised.
        """
        rsp = simplejson.loads(data)

        if rsp['stat'] == 'fail':
            raise APIError, rsp

        return rsp

    def _make_url(self, path):
        """Given a path on the server returns a full http:// style url"""
        url = urljoin(self.url, path)
        if not url.startswith('http'):
            url = 'http://%s' % url
        return url

    def http_get(self, path):
        """
        Performs an HTTP GET on the specified path, storing any cookies that
        were set.
        """
        url = self._make_url(path)

        try:
            rsp = urllib2.urlopen(url).read()
            return rsp
        except urllib2.HTTPError, e:
            print "Unable to access %s (%s). The host path may be invalid" % \
                (url, e.code)

            die()

    def api_get(self, path):
        """
        Performs an API call using HTTP GET at the specified path.
        """
        return self.process_json(self.http_get(path))

    def http_post(self, path, fields, files=None):
        """
        Performs an HTTP POST on the specified path, storing any cookies that
        were set.
        """
        url = self._make_url(path)

        content_type, body = self._encode_multipart_formdata(fields, files)
        headers = {
            'Content-Type': content_type,
            'Content-Length': str(len(body))
        }

        try:
            r = urllib2.Request(url, body, headers)
            data = urllib2.urlopen(r).read()
            return data
        except urllib2.URLError, e:
            die("Unable to access %s. The host path may be invalid\n%s" % \
                (url, e))
        except urllib2.HTTPError, e:
            die("Unable to access %s (%s). The host path may be invalid\n%s" % \
                (url, e.code, e.read()))

    def api_post(self, path, fields=None, files=None):
        """
        Performs an API call using HTTP POST at the specified path.
        """
        return self.process_json(self.http_post(path, fields, files))

    def _encode_multipart_formdata(self, fields, files):
        """
        Encodes data for use in an HTTP POST.
        """
        BOUNDARY = mimetools.choose_boundary()
        content = ""

        fields = fields or {}
        files = files or {}

        for key in fields:
            content += "--" + BOUNDARY + "\r\n"
            content += "Content-Disposition: form-data; name=\"%s\"\r\n" % key
            content += "\r\n"
            content += fields[key] + "\r\n"

        for key in files:
            filename = files[key]['filename']
            value = files[key]['content']
            content += "--" + BOUNDARY + "\r\n"
            content += "Content-Disposition: form-data; name=\"%s\"; " % key
            content += "filename=\"%s\"\r\n" % filename
            content += "\r\n"
            content += value + "\r\n"

        content += "--" + BOUNDARY + "--\r\n"
        content += "\r\n"

        content_type = "multipart/form-data; boundary=%s" % BOUNDARY

        return content_type, content

def die(msg=None):
    """
    Cleanly exits the program with an error message.
    """
    if msg:
        print msg


from kibot.m_irclib import Timer
from kibot.Settings import Setting
import kibot.BaseModule

from datetime import datetime
import pytz

class reviewboard(kibot.BaseModule.BaseModule):
    """Monitors a Review Board instance and notifies notify_channel when a new
    review request is posted and provides a basic query interface.
    """
    _settings = [
        Setting('notify_channel', None, 'the channel to notify about new review requests'),
    ]

    _stash_attrs = ['notify_channel']

    _commands = ['reviews']

    def __init__(self, bot):
        self.bot = bot
        kibot.BaseModule.BaseModule.__init__(self, bot)
        self._unstash()

        self.last_poll = datetime.now(pytz.utc)

        if REVIEW_REQUEST_POLL_INTERVAL > 0:
            self.timer = Timer(0, self._timer_cb,
                               repeat=REVIEW_REQUEST_POLL_INTERVAL)
            self.bot.set_timer(self.timer)

    def _unload(self):
        self._stash()
        self.bot.del_timer(self.timer)

    _reviews_cperm = 1 # allow anyone to execute
    def reviews(self, cmd):
        """Lists the open review requests.
           reviews <review board group>
        """
        args = cmd.asplit()
        
        try:
            server = ReviewBoardServer(REVIEWBOARD_URL)
            server.login(USERNAME, PASSWORD)
        except:
            cmd.nreply('Could not connect to the Review Board server.')
            return

        if len(args) == 0:
            requests = server.api_get('/api/json/reviewrequests/all/count')
            num_requests = int(requests['count'])
            if num_requests == 1:
                cmd.nreply('There is %d open review request.' % num_requests)
            else:
                cmd.nreply('There are %d open review requests.' % num_requests)
            return

        group = args[0]
        requests = server.api_get('/api/json/reviewrequests/to/group/%s' % group)

        review_requests = requests['review_requests']
        if len(review_requests) > 0:
            for request in review_requests:
                cmd.nreply("#%d: %s" % (request['id'], request['description']))
                cmd.nreply("%s/r/%d" % (REVIEWBOARD_URL, request['id']))
        else:
            cmd.nreply('No review requests found for %s.' % args[0])

    def _timer_cb(self):
        if self.notify_channel == None:
            # Don't stop the timer as this could be set at a later date.
            return 1

        try:
            server = ReviewBoardServer(REVIEWBOARD_URL)
            server.login(USERNAME, PASSWORD)
        except:
            # Don't stop the timer due to an exception as the server may be
            # having a temporary hiccup.
            return 1

        resp = server.api_get("/api/json/reviewrequests/all")
        requests = resp['review_requests']
        for r in requests:
            time_added = datetime.strptime(r['time_added'], '%Y-%m-%d %H:%M:%S')
            remote_tz = pytz.timezone(REVIEWBOARD_TIMEZONE)
            time_added = remote_tz.localize(time_added).astimezone(pytz.utc)

            if time_added < self.last_poll:
                continue

            bugs_closed = r['bugs_closed']
            if len(bugs_closed) <= 0:
                self.bot.conn.privmsg(self.notify_channel, '%s has put #%d up for review'
                                      % (r['submitter']['fullname'], r['id']))
            else:
                bugs_list = ', '.join(['#%s' % b for b in bugs_closed])
                self.bot.conn.privmsg(self.notify_channel, '%s has put #%d, affecting %s, up for review'
                                      % (r['submitter']['fullname'], r['id'], bugs_list))

            self.bot.conn.privmsg(self.notify_channel, '#%d: %s' % (r['id'], r['summary']))

            target_groups = ', '.join([g['display_name'].capitalize() for g in r['target_groups']])
            if target_groups == '':
                self.bot.conn.privmsg(self.notify_channel, 'Please review it at: %s/r/%d'
                                      % (REVIEWBOARD_URL, r['id']))
            else:
                self.bot.conn.privmsg(self.notify_channel, '%s, please review it at: %s/r/%d'
                                      % (target_groups, REVIEWBOARD_URL, r['id']))
        
        self.last_poll = datetime.now(pytz.utc)
        return 1

Reply via email to