GitHub: Add announce feature

master
Valentin Lorentz 2011-02-27 09:31:54 +01:00
parent 8f79343811
commit 0c99ab5a4a
3 changed files with 202 additions and 2 deletions

View File

@ -52,5 +52,16 @@ conf.registerGlobalValue(GitHub.api, 'url',
registry.String('http://github.com/api/v2/json', _("""The URL of the
GitHub API to use. You probably don't need to edit it, but I let it
there, just in case.""")))
conf.registerGroup(GitHub, 'server')
conf.registerGlobalValue(GitHub.server, 'host',
registry.String('0.0.0.0', _("""The hostname the socket server will
bind. Reload the plugin to apply this setting.""")))
conf.registerGlobalValue(GitHub.server, 'port',
registry.Integer(36987, _("""The port the socket server will
bind. Reload the plugin to apply this setting.""")))
conf.registerGlobalValue(GitHub, 'announces',
registry.String('', _("""You shouldn't edit this configuration
variable yourself, unless you know what you do. Use '@Github announce
add' or '@Github announce remove' instead.""")))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

View File

@ -29,29 +29,79 @@
###
import json
import time
import urllib
import socket
import threading
import SocketServer
import supybot.log as log
import supybot.utils as utils
import supybot.world as world
from cStringIO import StringIO
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('GitHub')
#####################
# Server stuff
#####################
class ThreadedTCPServer(SocketServer.TCPServer):
pass
class RequestHandler(SocketServer.StreamRequestHandler):
def handle(self):
length = 0
currentLine = ''
data = ''
while self.server.enabled:
if not '\r\n' in data:
try:
data += self.request.recv(4096)
except socket.timeout:
time.sleep(0.1) # in case of odd problem
continue
if not data: # Server closed connection
return
if data.startswith('payload=') and \
len(data) == length:
payload = StringIO()
payload.write(urllib.unquote(data[len('payload='):]))
payload.seek(0)
self.server._plugin.announce.onPayload(json.load(payload))
return
elif '\r\n' in data:
splitted = data.split('\r\n')
currentLine = splitted[0]
data = '\r\n'.join(splitted[1:])
if currentLine.startswith('Content-Length: '):
length = int(currentLine[len('Content-Length: '):])
#####################
# API access stuff
#####################
def query(caller, type_, uri_end, args):
args = dict([(x,y) for x,y in args.items() if y is not None])
url = '%s/%s/%s?%s' % (caller._url(), type_, uri_end,
urllib.urlencode(args))
return json.load(utils.web.getUrlFd(url))
#####################
# Plugin itself
#####################
instance = None
@internationalizeDocstring
class GitHub(callbacks.Plugin):
"""Add the help for "@plugin help GitHub" here
This should describe *how* to use this plugin."""
threaded = True
def __init__(self, irc):
global instance
@ -59,6 +109,118 @@ class GitHub(callbacks.Plugin):
callbacks.Plugin.__init__(self, irc)
instance = self
if not world.testing:
host = self.registryValue('server.host')
port = self.registryValue('server.port')
while True:
try:
self._server = ThreadedTCPServer((host, port),
RequestHandler)
break
except socket.error: # Address already in use
time.sleep(1)
self._server.timeout = 0.5
# Used by request handlers:
self._server._plugin = self
self._server.enabled = True
threading.Thread(target=self._server.serve_forever,
name='GitHub commits listener').start()
class announce(callbacks.Commands):
def _createPrivmsg(self, channel, payload, commit):
bold = ircutils.bold
s = '%s/%s (in %s): %s committed %s %s' % \
(payload['repository']['owner']['name'],
bold(payload['repository']['name']),
bold(payload['ref'].split('/')[-1]),
commit['author']['name'],
bold(commit['message']),
commit['url'][0:-34])
return ircmsgs.privmsg(channel, s)
def onPayload(self, payload):
repo = '%s/%s' % (payload['repository']['owner']['name'],
payload['repository']['name'])
announces = self._load()
if repo not in announces:
log.info('Commit for repo %s not announced anywhere' % repo)
return
for channel in announces[repo]:
for irc in world.ircs:
if channel in irc.state.channels:
break
if channel not in irc.state.channels:
print repr(irc.state.channel)
log.info('Cannot announce commit for repo %s on %s' %
(repo, channel))
else:
for commit in payload['commits']:
msg = self._createPrivmsg(channel, payload, commit)
irc.queueMsg(msg)
def _load(self):
announces = instance.registryValue('announces').split(' || ')
if announces == ['']:
return {}
announces = [x.split(' | ') for x in announces]
output = {}
for repo, chan in announces:
if chan not in output:
output[repo] = []
output[repo].append(chan)
return output
def _save(self, data):
list_ = []
for repo, chans in data.items():
list_.extend([' | '.join([repo,chan]) for chan in chans])
string = ' || '.join(list_)
instance.setRegistryValue('announces', value=string)
@internationalizeDocstring
def add(self, irc, msg, args, channel, owner, name):
"""[<channel>] <owner> <name>
Announce the commits of the GitHub repository called
<owner>/<name> in the <channel>.
<channel> defaults to the current channel."""
repo = '%s/%s' % (owner, name)
announces = self._load()
if repo not in announces:
announces[repo] = [channel]
elif channel in announces[repo]:
irc.error(_('This repository is already announced to this '
'channel.'))
else:
announces[repo].append(channel)
self._save(announces)
irc.replySuccess()
add = wrap(add, ['channel', 'something', 'something'])
@internationalizeDocstring
def remove(self, irc, msg, args, channel, owner, name):
"""[<channel>] <owner> <name>
Don't announce the commits of the GitHub repository called
<owner>/<name> in the <channel> anymore.
<channel> defaults to the current channel."""
repo = '%s/%s' % (owner, name)
announces = self._load()
if repo not in announces:
announces[repo] = []
elif channel not in announces[repo]:
irc.error(_('This repository is not yet announce to this '
'channed.'))
else:
announces[repo].remove(channel)
self._save(announces)
irc.replySuccess()
remove = wrap(remove, ['channel', 'something', 'something'])
class repo(callbacks.Commands):
def _url(self):
return instance.registryValue('api.url')
@ -118,6 +280,13 @@ class GitHub(callbacks.Plugin):
info = wrap(info, ['something', 'something',
getopts({'enable': 'anything',
'disable': 'anything'})])
def die(self):
self.__parent.die()
if not world.testing:
self._server.enabled = False
time.sleep(1)
self._server.shutdown()
del self._server
Class = GitHub

View File

@ -31,7 +31,27 @@
from supybot.test import *
class GitHubTestCase(PluginTestCase):
plugins = ('GitHub',)
plugins = ('GitHub', 'Config')
def testAnnounceAdd(self):
self.assertNotError('config supybot.plugins.GitHub.announces ""')
self.assertNotError('github announce add #foo ProgVal Supybot-fr')
self.assertResponse('config supybot.plugins.GitHub.announces',
'ProgVal/Supybot-fr | #foo')
self.assertNotError('github announce add #bar ProgVal Supybot-plugins')
self.assertResponse('config supybot.plugins.GitHub.announces',
'ProgVal/Supybot-plugins | #bar || '
'ProgVal/Supybot-fr | #foo')
def testAnnounceRemove(self):
self.assertNotError('config supybot.plugins.GitHub.announces '
'ProgVal/Supybot-fr | #foo || '
'ProgVal/Supybot-plugins | #bar')
self.assertNotError('github announce remove #foo ProgVal Supybot-fr')
self.assertResponse('config supybot.plugins.GitHub.announces',
'ProgVal/Supybot-plugins | #bar')
self.assertNotError('github announce remove #bar '
'ProgVal Supybot-plugins')
self.assertResponse('config supybot.plugins.GitHub.announces', ' ')
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: