### # Copyright (c) 2011, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import json import time import urllib import socket import threading import supybot.log as log import supybot.utils as utils import supybot.world as world from supybot.commands import * import supybot.plugins as plugins import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks import supybot.httpserver as httpserver from . import ur1ca if sys.version_info[0] < 3: from cStringIO import StringIO else: from io import StringIO try: from supybot.i18n import PluginInternationalization from supybot.i18n import internationalizeDocstring _ = PluginInternationalization('GitHub') except: # This are useless functions that's allow to run the plugin on a bot # without the i18n plugin _ = lambda x:x internationalizeDocstring = lambda x:x ##################### # Server stuff ##################### class GithubCallback(httpserver.SupyHTTPServerCallback): name = "GitHub announce callback" defaultResponse = _(""" You shouldn't be there, this subfolder is not for you. Go back to the index and try out other plugins (if any).""") def doPost(self, handler, path, form): if not handler.address_string().endswith('.rs.github.com') and \ not handler.address_string().endswith('.cloud-ips.com') and \ not handler.address_string() == 'localhost': log.warning("""'%s' tried to act as a web hook for Github, but is not GitHub.""" % handler.address_string()) else: self.plugin.announce.onPayload(json.loads(form['payload'].value)) ##################### # 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.""" def __init__(self, irc): global instance self.__parent = super(GitHub, self) callbacks.Plugin.__init__(self, irc) instance = self callback = GithubCallback() callback.plugin = self httpserver.hook('github', callback) class announce(callbacks.Commands): def _createPrivmsg(self, channel, payload, commit, hidden=None): bold = ircutils.bold url = commit['url'] # ur1.ca try: post_param = ur1ca.parameterize(url) answerfile = ur1ca.request(post_param) doc = ur1ca.retrievedoc(answerfile) answerfile.close() status, url2 = ur1ca.scrape(doc) if status: url = url2 except Exception as e: log.error('Cannot connect to ur1.ca: %s' % e) 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'].split('\n')[0]), url) if hidden is not None: s += _(' (+ %i hidden commits)') % hidden return ircmsgs.privmsg(channel, s.encode('utf8')) 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 commits = payload['commits'] if channel not in irc.state.channels: log.info('Cannot announce commit for repo %s on %s' % (repo, channel)) elif len(commits) == 0: log.warning('GitHub callback called without any commit.') else: hidden = None last_commit = commits[-1] if last_commit['message'].startswith('Merge ') and \ len(commits) > 5: hidden = len(commits) + 1 payload['commits'] = [last_commit] for commit in payload['commits']: msg = self._createPrivmsg(channel, payload, commit, hidden) 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 repo 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): """[] Announce the commits of the GitHub repository called / in the . 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.')) return 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): """[] Don't announce the commits of the GitHub repository called / in the anymore. 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 announced to this ' 'channel.')) return else: announces[repo].remove(channel) self._save(announces) irc.replySuccess() remove = wrap(remove, ['channel', 'something', 'something']) class repo(callbacks.Commands): def _url(self): url = instance.registryValue('api.url') if url == 'http://github.com/api/v2/json': # old api url = 'https://api.github.com' instance.setRegistryValue('api.url', value=url) return url @internationalizeDocstring def search(self, irc, msg, args, search, optlist): """ [--page ] [--language ] Searches the string in the repository names database. You can specify the page of the results, and restrict the search to a particular programming .""" args = {'page': None, 'language': None} for name, value in optlist: if name in args: args[name] = value results = query(self, 'legacy/repos/search', urllib.quote_plus(search), args) reply = ' & '.join('%s/%s' % (x['owner'], x['name']) for x in results['repositories']) if reply == '': irc.error(_('No repositories matches your search.')) else: irc.reply(reply.encode('utf8')) search = wrap(search, ['something', getopts({'page': 'id', 'language': 'somethingWithoutSpaces'})]) @internationalizeDocstring def info(self, irc, msg, args, owner, name, optlist): """ [--enable ...] \ [--disable ] Displays informations about 's . Enable or disable features (ie. displayed data) according to the request).""" enabled = ['watchers', 'forks', 'pushed_at', 'open_issues', 'description'] for mode, features in optlist: features = features.split(' ') for feature in features: if mode == 'enable': enabled.append(feature) else: try: enabled.remove(feature) except ValueError: # No error is raised, because: # 1. it wouldn't break anything # 2. it enhances cross-compatiblity pass results = query(self, 'repos', '%s/%s' % (owner, name), {}) output = [] for key, value in results.items(): if key in enabled: output.append('%s: %s' % (key, value)) irc.reply(', '.join(output).encode('utf8')) info = wrap(info, ['something', 'something', getopts({'enable': 'anything', 'disable': 'anything'})]) def die(self): self.__parent.die() httpserver.unhook('github') Class = GitHub # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: