commit aea38c93ff8d7f11e253a3d9c5a6f37b28619f7f Author: Dennis Kaarsemaker Date: Mon Jun 26 19:57:20 2006 +0200 Initial checkin diff --git a/.bzrignore b/.bzrignore new file mode 100644 index 0000000..41c16ff --- /dev/null +++ b/.bzrignore @@ -0,0 +1,2 @@ +*tar.gz +*pyc diff --git a/Bantracker/README.txt b/Bantracker/README.txt new file mode 100644 index 0000000..d60b47a --- /dev/null +++ b/Bantracker/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/Bantracker/__init__.py b/Bantracker/__init__.py new file mode 100644 index 0000000..bcd09fe --- /dev/null +++ b/Bantracker/__init__.py @@ -0,0 +1,22 @@ +""" +This plugin can store all kick/ban/remove/mute actions +""" + +import supybot +import supybot.world as world + +__version__ = "0.2" +__author__ = supybot.Author("Dennis Kaarsemaker","Seveas","dennis@kaarsemaker.net") +__contributors__ = {} +__url__ = 'https://bots.ubuntulinux.nl' + +import config +reload(config) +import plugin +reload(plugin) + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure diff --git a/Bantracker/bans.cgi b/Bantracker/bans.cgi new file mode 100755 index 0000000..e538dd9 --- /dev/null +++ b/Bantracker/bans.cgi @@ -0,0 +1,349 @@ +#!/usr/bin/python +import sys +sys.path.append('/home/dennis/public_html') +from commoncgi import * +import lp_auth + +### Variables +db = '/home/dennis/ubugtu/data/bans.db' +lp_group = 'ubuntu-irc' +num_per_page = 100 + +con = sqlite.connect(db) +cur = con.cursor() + +# Login check +person = None +error = '' + +# Delete old sessions +cur.execute("""DELETE FROM sessions WHERE time < %d""", int(time.time()) - 86400) +# Registration? +if form.has_key('lpuser') and form.has_key('lpmail'): + cur.execute("""SELECT * FROM USERS WHERE username = %s""", form['lpuser'].value) + if len(cur.fetchall()): + error = """User is already registered""" + else: + import sha, commands, random + try: + newperson = lp_auth.LaunchpadPerson(nick=form['lpuser'].value, email=form['lpmail'].value) + except: + error = """Username incorrect. Your username is the $someone in + http://launchpad.net/people/$someone that is your + launchpad homepage""" + else: + mailsha = sha.new('mailto:%s' % form['lpmail'].value).hexdigest().lower() + if mailsha in newperson.mail_shasums: + if not newperson.key: + error = """Your launchpad account does not have a GPG key. Please + set a GPG key on launchpad""" + else: + chars = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" + password = "" + salt = "" + for i in xrange(8): + password += chars[random.randint(0,len(chars)-1)] + salt += chars[random.randint(0,len(chars)-1)] + os.system('gpg --homedir /tmp --keyserver hkp://subkeys.pgp.net --recv-keys %s' % newperson.key) + (infd, outfd) = os.popen2('gpg --homedir /tmp --encrypt --armor --trust-model always --recipient %s' + % newperson.key) + infd.write(password) + infd.close() + gpg = outfd.read() + outfd.close() + fd = os.popen('mail -a "From: Ubugtu " -s "Your bantracker account" %s' + % form['lpmail'].value.replace('ubuntu@sourceguru.net','mezzle@gmail.com'), 'w') + fd.write(gpg) + fd.close() + error = "Your password has been sent (encrypted) to your e-mail address" + cur.execute("""INSERT INTO users (username, salt, password) VALUES (%s, %s, %s)""", + (form['lpuser'].value, salt, + sha.new(salt + sha.new(password + salt).hexdigest().lower()).hexdigest().lower())) + con.commit() + else: + error = """Username and mailaddress don't match. Username is the $someone + in http://launchpad.net/people/$someone that is your + launchpad homepage""" + +# Session handling +if cookie.has_key('sess'): + try: + sess = cookie['sess'].value + cur.execute("""SELECT user FROM sessions WHERE session_id=%s""",sess) + user = cur.fetchall()[0][0] + person = pickle.loads(user) + except: + con.commit() + pass + +# Login +if not person and form.has_key('user') and form.has_key('pw'): + import sha + cur.execute("SELECT salt, password FROM users WHERE username = %s", form['user'].value) + data = cur.fetchall() + if data: + salt, password = data[0] + if password != sha.new(salt + sha.new(form['pw'].value + salt).hexdigest().lower()).hexdigest().lower(): + error = "Username or password incorrect" + else: + try: + person = lp_auth.LaunchpadPerson(nick = form['user'].value) + except lp_auth.LaunchpadException: + person = None + error = 'An error occured while talking to launchpad' + person.authenticated = True + if person.check_group_membership(lp_group): + # Create a session + sessid = md5.new('%s%s%d' % (os.environ['REMOTE_ADDR'], time.time(), random.randint(1,100000))).hexdigest() + cookie['sess'] = sessid + try: + cur.execute("""INSERT INTO sessions (session_id, user, time) VALUES + (%s, %s, %d);""", (sessid, pickle.dumps(person), int(time.time()))) + except: + con.commit() + raise + con.commit() + else: + person.authenticated = False + error = "You are not in the '%s' group on launchpad" % lp_group + +# Not authenticated. +if not person or not person.authenticated: + if error: + print """%s""" % error + print """
+ The old launchpad based authentication system has been + disabled!

+ Login:
+
+ Password:
+

+ +
+
+ No account yet? Enter your launchpad name and mailaddress + here.

+ Name:
+
+ Mail address:
+

+ +
+ """ + send_page('bans.tmpl') + +# Log +if form.has_key('log'): + cur.execute("""SELECT log FROM bans WHERE id=%s""", form['log'].value) + log = cur.fetchall() + con.commit() + print q(log[0][0]).replace('\n', '
') + send_page('empty.tmpl') + +# Main page +# Process comments +if form.has_key('comment') and form.has_key('comment_id'): + cur.execute("""SELECT ban_id FROM comments WHERE ban_id=%s and comment=%s""", (form['comment_id'].value, form['comment'].value)) + comm = cur.fetchall() + if not len(comm): + cur.execute("""INSERT INTO comments (ban_id, who, comment, time) VALUES (%s, %s, %s, %s)""", + (form['comment_id'].value,person.name,form['comment'].value,pickle.dumps(datetime.datetime.now(pytz.UTC)))) + con.commit() + +# Write the page +print '
' + +# Personal data +print '
Logged in as: %s
Timezone: ' % person.name +if form.has_key('tz') and form['tz'].value in pytz.common_timezones: + tz = form['tz'].value +elif cookie.has_key('tz') and cookie['tz'].value in pytz.common_timezones: + tz = cookie['tz'].value +else: + tz = 'UTC' +cookie['tz'] = tz +print '
' +tz = pytz.timezone(tz) + +# Search form +print '' + +# Pagination, only when not processing a search +if not form.has_key('query'): + sort = '' + if form.has_key('sort'): + sort='&sort=' + form['sort'].value + print '
·' + cur.execute('SELECT COUNT(id) FROM bans') + nump = math.ceil(int(cur.fetchall()[0][0]) / float(num_per_page)) + for i in range(nump): + print '%d ·' % (i, sort, i+1) + print '
' + +# Empty log div, will be filled with AJAX +print '
 
' + +# Main bans table +# Table heading +print '' +for h in [['Channel',0], ['Nick/Mask',1], ['Operator',2], ['Time',6]]: + # Negative integers for backwards searching + try: + v = int(form['sort'].value) + if v < 10: h[1] += 10 + except: + pass + print '' % (h[1],h[0]) +print '' + +# Select and filter bans +cur.execute("SELECT channel,mask,operator,time,removal,removal_op,id FROM bans ORDER BY id DESC") +bans = cur.fetchall() + +def myfilter(item, regex, kick, ban, oldban, mute, oldmute): + if '!' not in item[1]: + if not kick: return False + elif item[1][0] == '%': + if item[4]: + if not oldmute: return False + else: + if not mute: return False + else: + if item[4]: + if not oldban: return False + else: + if not ban: return False + return regex.search(item[1]) or regex.search(item[2]) or regex.search(item[0]) or (item[5] and regex.search(item[5])) + +if form.has_key('query'): + k = b = ob = m = om = False + if form.has_key('kicks'): k = True + if form.has_key('oldbans'): ob = True + if form.has_key('bans'): b = True + if form.has_key('oldmutes'): om = True + if form.has_key('mutes'): m = True + regex = re.compile(re.escape(form['query'].value).replace('\%','.*'), re.DOTALL | re.I) + bans = filter(lambda x: myfilter(x, regex, k, b, ob, m, om), bans) + start = 0; end = len(bans) +else: + page = 0 + try: + page = int(form['page'].value) + except: + pass + start = page * num_per_page + end = (page+1) * num_per_page + +# Sort the bans +def _sortf(x1,x2,field): + if x1[field] < x2[field]: return -1 + if x1[field] > x2[field]: return 1 + return 0 + +if form.has_key('sort'): + try: + field = int(form['sort'].value) + except: + pass + else: + if field in (0,1,2,6,10,11,12,16): + bans.sort(lambda x1,x2: _sortf(x1,x2,field%10)) + if field >= 10: + bans.reverse() + +# And finally, display them! +i = 0 +for b in bans[start:end]: + print '' + # Channel + print '' % ('',b[0]) + # Mask + print '' + # Operator + print '' + # Time + print '' + # Log link + print """""" % b[6] + print '' + + # Comments + print '' + print '' + +print '
%sLog
%s %s%s' % b[1] + # Ban removal + if b[4]: + print '
(Removed)' + print'
%s' % b[2] + if b[4]: # Ban removal + print '
%s' % b[5] + print '
%s' % pickle.loads(b[3]).astimezone(tz).strftime("%b %d %Y %H:%M:%S") + if b[4]: # Ban removal + print '
%s' % pickle.loads(b[4]).astimezone(tz).strftime("%b %d %Y %H:%M:%S") + print '
Show/Hide log
' + cur.execute("""SELECT who, comment, time FROM comments WHERE ban_id = %s""" % b[6]) + comments = cur.fetchall() + if len(comments) == 0: + print '(No comments) ' + else: + for c in comments: + print '%s
%s, %s

' % \ + (q(c[1]),c[0],pickle.loads(c[2]).astimezone(tz).strftime("%b %d %Y %H:%M:%S")) + print """Add comment""" % b[6] + print """
' + +# Aaaaaaaaaaaaaaaaand send! +send_page('bans.tmpl') diff --git a/Bantracker/bans.tmpl b/Bantracker/bans.tmpl new file mode 100644 index 0000000..64a8fe1 --- /dev/null +++ b/Bantracker/bans.tmpl @@ -0,0 +1,68 @@ + + + Ubugtu bantracker + + + + + +
+

Ubugtu Bantracker

+

+ %s +

+

©2006 Dennis Kaarsemaker

+
+ + diff --git a/Bantracker/config.py b/Bantracker/config.py new file mode 100644 index 0000000..b2dda78 --- /dev/null +++ b/Bantracker/config.py @@ -0,0 +1,9 @@ +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + conf.registerPlugin('Bantracker', True) + +Bantracker = conf.registerPlugin('Bantracker') +conf.registerChannelValue(conf.supybot.plugins.BanTracker, 'enabled', + registry.Boolean(False, """Enable the bantracker""")) diff --git a/Bantracker/empty.tmpl b/Bantracker/empty.tmpl new file mode 100644 index 0000000..3aa8911 --- /dev/null +++ b/Bantracker/empty.tmpl @@ -0,0 +1 @@ +%s diff --git a/Bantracker/lp_auth.py b/Bantracker/lp_auth.py new file mode 100755 index 0000000..50d531d --- /dev/null +++ b/Bantracker/lp_auth.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# +# Simple authentication against launchpad and group membership checking. +# Feel free to use, modify and distribute as you see fit. +# (c) 2006 Dennis Kaarsemaker + +import urllib,urllib2 +import xml.dom.minidom as dom +import sha, re + +_login_url = 'https://launchpad.net/+login' +_login_data = 'loginpage_email=%s&loginpage_password=%s&loginpage_submit_login=Log%%20In' +_login_re = re.compile('logged in as.*?a href=".*?/people/(.*?)"', re.DOTALL) +_urlopener = urllib2.build_opener(urllib2.HTTPCookieProcessor(), urllib2.HTTPRedirectHandler()) + +class LaunchpadException(Exception): + pass + +class LaunchpadPerson: + def __init__(self, email = None, password = None, nick = None): + self.authenticated = False + if email and password: + self.email = email + _nick = self._login(password) + if _nick: + self.authenticated = True + nick = _nick + if nick: + self.get_data(nick) + + def get_data(self, nick): + self.nick = nick + try: + u = urllib2.urlopen('http://launchpad.net/people/%s/+rdf' % urllib.quote(nick)) + rdf = u.read() + rdf = dom.parseString(rdf) + except: + raise + raise LaunchpadException('Could not parse launchpad data') + self.mail_shasums = map(lambda x: x.childNodes[0].data.lower(), rdf.getElementsByTagName('foaf:mbox_sha1sum')) + self.name = rdf.getElementsByTagName('foaf:name')[0].childNodes[0].data + try: + self.img = rdf.getElementsByTagName('foaf:img')[0].getAttribute('rdf:resource') + except: # No image + self.img = None + try: + self.key = rdf.getElementsByTagName('wot:fingerprint')[0].childNodes[0].data + except: # No image + self.key = None + + def check_group_membership(self, group): + try: + self.mail_shasums + except AttributeError: + raise LaunchpadException("Person not logged in and launchpad username not known") + try: + fd = urllib2.urlopen('http://launchpad.net/people/%s/+rdf' % urllib.quote(group)) + rdf = fd.read() + rdf = dom.parseString(rdf) + except: + raise LaunchpadException('Could not parse launchpad data') + group_mail_shasums = map(lambda x: x.childNodes[0].data.lower(), rdf.getElementsByTagName('foaf:mbox_sha1sum')) + # If the intersection of shasums and shasums2 is not empty, the persons + # prefered mail address is in the group. + return len([x for x in self.mail_shasums if x in group_mail_shasums]) > 0 + + def _login(self, pw): + req = urllib2.Request(_login_url, _login_data % (urllib.quote(self.email),urllib.quote(pw))) + try: + fd = _urlopener.open(req) + data = fd.read().lower() + except: # Launchpad offline perhaps... + raise LaunchpadException('Could not parse launchpad data') + try: + return _login_re.search(data).group(1) + except: + return False + +if __name__ == '__main__': + import sys + person = LaunchpadPerson(sys.argv[1], sys.argv[2]) + print person.authenticated + try: + print person.nick + print person.name + print person.mail_shasums + print person.img + except: + raise + pass + print person.check_group_membership(sys.argv[3]) diff --git a/Bantracker/plugin.py b/Bantracker/plugin.py new file mode 100644 index 0000000..631ad5d --- /dev/null +++ b/Bantracker/plugin.py @@ -0,0 +1,188 @@ +# Based on the standard log plugin + +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +import supybot.ircmsgs as ircmsgs +import supybot.conf as conf + +import sqlite, pytz, cPickle, datetime, time + +db = '/home/dennis/ubugtu/data/bans.db' +tz = 'Europe/Amsterdam' + +def now(): + return cPickle.dumps(datetime.datetime.now(pytz.timezone(tz))) + +def db_run(query, parms, expect_result = False, expect_id = False): + con = sqlite.connect(db) + cur = con.cursor() + try: + cur.execute(query, parms) + except: + con.close() + raise + data = None + if expect_result: data = cur.fetchall() + if expect_id: data = con.insert_id() + con.commit() + con.close() + return data + +class Bantracker(callbacks.Plugin): + """This plugin has no commands""" + noIgnore = True + + def __init__(self, irc): + self.__parent = super(Bantracker, self) + self.__parent.__init__(irc) + self.lastMsgs = {} + self.lastStates = {} + self.logs = {} + + def __call__(self, irc, msg): + try: + # I don't know why I put this in, but it doesn't work, because it + # doesn't call doNick or doQuit. + # if msg.args and irc.isChannel(msg.args[0]): + super(self.__class__, self).__call__(irc, msg) + if irc in self.lastMsgs: + if irc not in self.lastStates: + self.lastStates[irc] = irc.state.copy() + self.lastStates[irc].addMsg(irc, self.lastMsgs[irc]) + finally: + # We must make sure this always gets updated. + self.lastMsgs[irc] = msg + + def reset(self): + self.logs.clear() + self.lastMsgs.clear() + self.lastStates.clear() + + def doLog(self, irc, channel, s): + if not self.registryValue('enabled', channel): + return + channel = ircutils.toLower(channel) + if channel not in self.logs.keys(): + self.logs[channel] = [] + format = conf.supybot.log.timestampFormat() + if format: + s = time.strftime(format) + " " + ircutils.stripFormatting(s) + self.logs[channel] = self.logs[channel][-199:] + [s.strip()] + + def doKickban(self, irc, channel, nick, target, kickmsg = None): + print "DoKickban: %s - %s - %s - %s" % (channel, nick, target, kickmsg) + if not self.registryValue('enabled', channel): + return + n = now() + id = db_run("INSERT INTO bans (channel, mask, operator, time, log) values(%s, %s, %s, %s, %s)", + (channel, target, nick, n, '\n'.join(self.logs[channel])), expect_id=True) + if kickmsg and id and not (kickmsg == nick): + db_run("INSERT INTO comments (ban_id, who, comment, time) values(%s,%s,%s,%s)", (id, nick, kickmsg, n)) + + def doUnban(self, irc, channel, nick, mask): + print "DoUnban: %s - %s - %s" % (channel, nick, mask) + if not self.registryValue('enabled', channel): + return + data = db_run("SELECT MAX(id) FROM bans where channel=%s and mask=%s", (channel, mask), expect_result=True) + if len(data) and not (data[0][0] == None): + db_run("UPDATE bans SET removal=%s , removal_op=%s WHERE id=%s", (now(), nick, int(data[0][0]))) + + def doPrivmsg(self, irc, msg): + (recipients, text) = msg.args + for channel in recipients.split(','): + if irc.isChannel(channel): + nick = msg.nick or irc.nick + if ircmsgs.isAction(msg): + self.doLog(irc, channel, + '* %s %s\n' % (nick, ircmsgs.unAction(msg))) + else: + self.doLog(irc, channel, '<%s> %s\n' % (nick, text)) + + def doNotice(self, irc, msg): + (recipients, text) = msg.args + for channel in recipients.split(','): + if irc.isChannel(channel): + self.doLog(irc, channel, '-%s- %s\n' % (msg.nick, text)) + + def doNick(self, irc, msg): + oldNick = msg.nick + newNick = msg.args[0] + for (channel, c) in irc.state.channels.iteritems(): + if newNick in c.users: + self.doLog(irc, channel, + '*** %s is now known as %s\n' % (oldNick, newNick)) + def doJoin(self, irc, msg): + for channel in msg.args[0].split(','): + self.doLog(irc, channel, + '*** %s has joined %s\n' % (msg.nick or msg.prefix, channel)) + + def doKick(self, irc, msg): + if len(msg.args) == 3: + (channel, target, kickmsg) = msg.args + else: + (channel, target) = msg.args + kickmsg = '' + if kickmsg: + self.doLog(irc, channel, + '*** %s was kicked by %s (%s)\n' % (target, msg.nick, kickmsg)) + else: + self.doLog(irc, channel, + '*** %s was kicked by %s\n' % (target, msg.nick)) + self.doKickban(irc, channel, msg.nick, target, kickmsg) + + def doPart(self, irc, msg): + for channel in msg.args[0].split(','): + self.doLog(irc, channel, '*** %s has left %s %s\n' % (msg.nick, channel, msg.args[1])) + if msg.args[1].startswith('requested by'): + args = msg.args[1].split() + self.doKickban(irc, channel, args[2].replace(':',''), msg.nick, ' '.join(args[3:])[1:-1].strip()) + + def doMode(self, irc, msg): + channel = msg.args[0] + if irc.isChannel(channel) and msg.args[1:]: + self.doLog(irc, channel, + '*** %s sets mode: %s %s\n' % + (msg.nick or msg.prefix, msg.args[1], + ' '.join(msg.args[2:]))) + if 'b' in msg.args[1] or 'd' in msg.args[1]: + i = 2 + plusmin = False + print msg.args + for c in msg.args[1]: + if c == '-': plusmin = False + elif c == '+': plusmin = True + else: + if c == 'b': + if plusmin: self.doKickban(irc, channel, msg.nick, msg.args[i]) + else: self.doUnban(irc,channel, msg.nick, msg.args[i]) + i += 1 + if c == 'd': + if plusmin: self.doKickban(irc, channel, msg.nick, msg.args[i] + ' (realname)') + else: self.doUnban(irc,channel, msg.nick, msg.args[i] + ' (realname)') + i += 1 + + def doTopic(self, irc, msg): + if len(msg.args) == 1: + return # It's an empty TOPIC just to get the current topic. + channel = msg.args[0] + self.doLog(irc, channel, + '*** %s changes topic to "%s"\n' % (msg.nick, msg.args[1])) + + def doQuit(self, irc, msg): + for (channel, chan) in self.lastStates[irc].channels.iteritems(): + if msg.nick in chan.users: + self.doLog(irc, channel, '*** %s has quit IRC\n' % msg.nick) + + def outFilter(self, irc, msg): + # Gotta catch my own messages *somehow* :) + # Let's try this little trick... + if msg.command in ('PRIVMSG', 'NOTICE'): + # Other messages should be sent back to us. + m = ircmsgs.IrcMsg(msg=msg, prefix=irc.prefix) + self(irc, m) + return msg + +Class = Bantracker diff --git a/Bantracker/test.py b/Bantracker/test.py new file mode 100644 index 0000000..49ca8d5 --- /dev/null +++ b/Bantracker/test.py @@ -0,0 +1,4 @@ +from supybot.test import * + +class BantrackerTestCase(PluginTestCase): + plugins = ('Bantracker',) diff --git a/Bugtracker/README.txt b/Bugtracker/README.txt new file mode 100644 index 0000000..fbcc4eb --- /dev/null +++ b/Bugtracker/README.txt @@ -0,0 +1,35 @@ +Copyright (c) 2005-2006, Dennis Kaarsemaker + +This program is free software; you can redistribute it and/or modify +it under the terms of version 2 of the GNU General Public License as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +The syntax to add a tracker is weird, here are some examples: +@bugtracker add freedesktop bugzilla https://bugs.freedesktop.org Freedesktop +@bugtracker add malone malone http://launchpad.net/malone Malone +@bugtracker add debian debbugs http://bugs.debian.org Debian +@bugtracker add openoffice issuezilla http://www.openoffice.org/issues OpenOffice +@bugtracker add django trac http://code.djangoproject.com/ticket Django +@bugtracker add gaim sourceforge http://sourceforge.net/tracker/?group_id=235&atid=100235 Gaim + +In general: @bugtracker add [description] +Bugtracker dialects (types) this plugin understands: +* Bugzilla +* Issuezilla (OpenOffice.org's tjernobyl transformation of bugzilla) +* Malone +* Debbugs (debbugs sucks donkeyballs - please fix debbugs) +* Trac (with not-too-buggered-up templates, it needs to do screenscraping) +* Sourceforge (needs atid and group_id in the url!) + +To request a bug report, use this syntax: + +bug 123 +bug #123 +supybot bug 123 +bug 123, 4, 5 +bug 1, 3 and 89 diff --git a/Bugtracker/__init__.py b/Bugtracker/__init__.py new file mode 100644 index 0000000..3de4c99 --- /dev/null +++ b/Bugtracker/__init__.py @@ -0,0 +1,35 @@ +### +# Copyright (c) 2005,2006 Dennis Kaarsemaker +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +### + +""" +This plugin will display bug information when requested. +""" + +import supybot +import supybot.world as world + +__version__ = "2.0" +__author__ = supybot.Author("Dennis Kaarsemaker","Seveas","dennis@kaarsemaker.net") +__contributors__ = {} +__url__ = 'http://bots.ubuntulinux.nl/' + +import config +reload(config) +import plugin +reload(plugin) + +if world.testing: + import test +Class = plugin.Class +configure = config.configure diff --git a/Bugtracker/config.py b/Bugtracker/config.py new file mode 100644 index 0000000..648c250 --- /dev/null +++ b/Bugtracker/config.py @@ -0,0 +1,44 @@ +### +# Copyright (c) 2005,2006 Dennis Kaarsemaker +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +### + +import supybot.conf as conf +import supybot.registry as registry +import supybot.ircutils as ircutils + +def configure(advanced): + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Bugtracker', True) + + +Bugtracker = conf.registerPlugin('Bugtracker') + +conf.registerChannelValue(conf.supybot.plugins.Bugtracker, 'bugSnarfer', + registry.Boolean(False, """Determines whether the bug snarfer will be + enabled, such that any Bugtracker URLs and bug ### seen in the channel + will have their information reported into the channel.""")) +conf.registerChannelValue(conf.supybot.plugins.Bugtracker, 'replyNoBugtracker', + registry.String('I don\'t have a bugtracker %s.', """Determines the phrase + to use when notifying the user that there is no information about that + bugtracker site.""")) +conf.registerChannelValue(conf.supybot.plugins.Bugtracker, 'snarfTarget', + registry.String('', """Determines the bugtracker to query when the + snarf command is triggered""")) + +class Bugtrackers(registry.SpaceSeparatedListOfStrings): + List = ircutils.IrcSet +conf.registerGlobalValue(conf.supybot.plugins.Bugtracker, 'bugtrackers', + Bugtrackers([], """Determines what bugtrackers will be added to the bot when it starts.""")) +conf.registerGlobalValue(conf.supybot.plugins.Bugtracker, 'replyWhenNotFound', + registry.Boolean(False, """Whether to send a message when a bug could not be + found""")) diff --git a/Bugtracker/plugin.py b/Bugtracker/plugin.py new file mode 100644 index 0000000..5f965d9 --- /dev/null +++ b/Bugtracker/plugin.py @@ -0,0 +1,577 @@ +### +# Copyright (c) 2005,2006 Dennis Kaarsemaker +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +### + +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +import supybot.conf as conf +import supybot.registry as registry + +import re +import xml.dom.minidom as minidom +from htmlentitydefs import entitydefs as entities +import email.FeedParser + +def registerBugtracker(name, url='', description='', trackertype=''): + conf.supybot.plugins.Bugtracker.bugtrackers().add(name) + group = conf.registerGroup(conf.supybot.plugins.Bugtracker.bugtrackers, name) + URL = conf.registerGlobalValue(group, 'url', registry.String(url, '')) + DESC = conf.registerGlobalValue(group, 'description', registry.String(description, '')) + TRACKERTYPE = conf.registerGlobalValue(group, 'trackertype', registry.String(trackertype, '')) + if url: + URL.setValue(url) + if description: + DESC.setValue(description) + if trackertype: + if defined_bugtrackers.has_key(trackertype.lower()): + TRACKERTYPE.setValue(trackertype.lower()) + else: + raise BugtrackerError("Unknown trackertype: %s" % trackertype) + +entre = re.compile('&(\S*?);') +def _getnodetxt(node): + L = [] + for childnode in node.childNodes: + if childnode.nodeType == childnode.TEXT_NODE: + L.append(childnode.data) + val = ''.join(L) + if node.hasAttribute('encoding'): + encoding = node.getAttribute('encoding') + if encoding == 'base64': + try: + val = val.decode('base64') + except: + val = 'Cannot convert bug data from base64.' + while entre.search(val): + entity = entre.search(val).group(1) + if entity in entities: + val = entre.sub(entities[entity], val) + else: + val = entre.sub('?', val) + return val + +class BugtrackerError(Exception): + """A bugtracker error""" + pass + +class BugNotFoundError(Exception): + """Pity, bug isn't there""" + pass + +class Bugtracker(callbacks.PluginRegexp): + """Show a link to a bug report with a brief description""" + threaded = True + callBefore = ['URL'] + regexps = ['turlSnarfer', 'bugSnarfer', 'oopsSnarfer'] + + def __init__(self, irc): + callbacks.PluginRegexp.__init__(self, irc) + self.db = ircutils.IrcDict() + for name in self.registryValue('bugtrackers'): + registerBugtracker(name) + group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False) + if group.trackertype() in defined_bugtrackers.keys(): + self.db[name] = defined_bugtrackers[group.trackertype()](name, group.url(), group.description()) + else: + raise BugtrackerError("Unknown trackertype: %s" % group.trackertype()) + self.shorthand = utils.abbrev(self.db.keys()) + + def add(self, irc, msg, args, name, trackertype, url, description): + """ [] + + Add a bugtracker to the list of defined bugtrackers. is the + type of the tracker (currently only Malone, Debbugs, Bugzilla, + Issuezilla and Trac are known). is the name that will be used to + reference the bugzilla in all commands. Unambiguous abbreviations of + will be accepted also. is the common name for the + bugzilla and will be listed with the bugzilla query; if not given, it + defaults to . + """ + name = name.lower() + if not description: + description = name + if url[-1] == '/': + url = url[:-1] + trackertype = trackertype.lower() + if trackertype in defined_bugtrackers: + self.db[name] = defined_bugtrackers[trackertype](name,url,description) + else: + irc.error("Bugtrackers of type '%s' are not understood" % trackertype) + return + registerBugtracker(name, url, description, trackertype) + self.shorthand = utils.abbrev(self.db.keys()) + irc.replySuccess() + add = wrap(add, ['something', 'something', 'url', additional('text')]) + + def remove(self, irc, msg, args, name): + """ + + Remove the bugtracker associated with from the list of + defined bugtrackers. + """ + try: + name = self.shorthand[name.lower()] + del self.db[name] + self.registryValue('bugtrackers').remove(name) + self.shorthand = utils.abbrev(self.db.keys()) + irc.replySuccess() + except KeyError: + s = self.registryValue('replyNoBugtracker', msg.args[0]) + irc.error(s % name) + remove = wrap(remove, ['text']) + + def list(self, irc, msg, args, name): + """[abbreviation] + + List defined bugtrackers. If [abbreviation] is specified, list the + information for that bugtracker. + """ + if name: + name = name.lower() + try: + name = self.shorthand[name] + (url, description, type) = (self.db[name].url, self.db[name].description, + self.db[name].__class__.__name__) + irc.reply('%s: %s, %s [%s]' % (name, description, url, type)) + except KeyError: + s = self.registryValue('replyNoBugtracker', msg.args[0]) + irc.error(s % name) + else: + if self.db: + L = self.db.keys() + L.sort() + irc.reply(utils.str.commaAndify(L)) + else: + irc.reply('I have no defined bugtrackers.') + list = wrap(list, [additional('text')]) + + def bugSnarfer(self, irc, msg, match): + r"""\b(?P(([a-z]+)?\s+bugs?|[a-z]+))\s+#?(?P\d+(?!\d*\.\d+)((,|\s*(and|en|et|und))\s*#?\d+(?!\d*\.\d+))*)""" + if not self.registryValue('bugSnarfer', msg.args[0]): + return + # Don't double on commands + s = str(msg).split(':')[2] + if s[0] in str(conf.supybot.reply.whenAddressedBy.chars): + return + sure_bug = match.group('bt').endswith('bug') or match.group('bt').endswith('bug') + # FIXME dig into supybot docs/code + #if conf.supybot.reply.whenAddressedBy.strings: + # print type(conf.supybot.reply.whenAddressedBy.strings) + # for p in conf.supybot.reply.whenAddressedBy.strings: + # if s.startswith(str(p)): + # return + # Get tracker name + bugids = match.group('bug') + reps = ((' ',''),('#',''),('and',','),('en',','),('et',','),('und',',')) + for r in reps: + bugids = bugids.replace(r[0],r[1]) + bugids = bugids.split(',')[:5] + bt = map(lambda x: x.lower(), match.group('bt').split()) + name = '' + if len(bt) == 1 and not (bt[0] in ['bug','bugs']): + try: + name = bt[0].lower() + tracker = self.db[name] + except: + return + elif len(bt) == 2: + try: + name = bt[0].lower() + tracker = self.db[name] + except: + name = '' + pass + if not name: + snarfTarget = self.registryValue('snarfTarget', msg.args[0]) + if not snarfTarget: + return + try: + name = self.shorthand[snarfTarget.lower()] + except: + s = self.registryValue('replyNoBugtracker', name) + irc.error(s % name) + try: + tracker = self.db[name] + except KeyError: + s = self.registryValue('replyNoBugtracker', name) + irc.error(s % name) + else: + for bugid in bugids: + bugid = int(bugid) + try: + report = self.get_bug(tracker,bugid) + except BugtrackerError, e: + if not sure_bug and bugid < 30: + return + irc.error(str(e)) + else: + irc.reply(report, prefixNick=False) + + #show_bug.cgi?id=|bugreport.cgi?bug=|(bugs|+bug)/|ticket/|tracker/.*aid= + #&group_id=\d+&at_id=\d+ + def turlSnarfer(self, irc, msg, match): + "(?Phttps?://.*?)(show_bug.cgi\?id=|bugreport.cgi\?bug=|(bugs|\+bug)/|/ticket/|tracker/.*aid=)(?P\d+)(?P&group_id=\d+&at_id=\d+)?" + if not self.registryValue('bugSnarfer', msg.args[0]): + return + try: + tracker = self.get_tracker(match.group(0),match.group('sfurl')) + if not tracker: + return + report = self.get_bug(tracker,int(match.group('bug')), do_url = False) + except BugtrackerError, e: + irc.error(str(e)) + else: + irc.reply(report, prefixNick=False) + turlSnarfer = urlSnarfer(turlSnarfer) + + # Only useful for launchpad developers + def oopsSnarfer(self, irc, msg, match): + r"OOPS-(?P\d*[A-Z]\d+)" + oopsid = match.group(1) + irc.reply("https://chinstrap.ubuntu.com/~jamesh/oops.cgi/%s" % oopsid, prefixNick=False) + + def get_tracker(self,snarfurl,sfdata): + for t in self.db.keys(): + tracker = self.db[t] + url = tracker.url.replace('http://','').replace('https://','') + if 'sourceforge.net' in url: + # Try to find the correct sf tracker + if str(sfdata) in tracker.url: + return tracker + if '/' in url: + url = url[:url.index('/')] + if url in snarfurl: + return tracker + if 'sourceforge.net' in snarfurl: + return self.db['sourceforge'] + # No tracker found, bummer. Let's try and add one + if 'show_bug.cgi' in snarfurl: + tracker = Bugzilla().get_tracker(snarfurl) + if tracker: + self.db[tracker.name] = tracker + self.shorthand = utils.abbrev(self.db.keys()) + return tracker + return None + + def get_bug(self, tracker, id, do_url = True): + (product, title, severity, status, url) = tracker.get_bug(id) + severity = severity[0].upper() + severity[1:].lower() + status = status[0].upper() + status[1:].lower() + if not do_url: + url = '' + if product: + return "%s bug %s in %s \"%s\" [%s,%s] %s" % (tracker.description, id, product, + title, severity, status, url) + return "%s bug %s \"%s\" [%s,%s] %s" % (tracker.description, id, title, severity, status, url) + +# Define all bugtrackers +class IBugtracker: + def __init__(self, name=None, url=None, description=None): + self.name = name + self.url = url + self.description = description + + def get_bug(self, id): + raise BugTrackerError("Bugtracker class does not implement get_bug") + + def get_tracker(self, url): + raise BugTrackerError("Bugtracker class does not implement get_tracker") + +class Bugzilla(IBugtracker): + def get_tracker(self, url): + url = url.replace('show_bug','xml') + try: + bugxml = utils.web.getUrl(url) + tree = minidom.parseString(bugxml) + url = str(tree.getElementsByTagName('bugzilla')[0].attributes['urlbase'].childNodes[0].data) + if url[-1] == '/': + url = url[:-1] + name = url[url.find('//') + 2:] + if '/' in name: + name = name[:name.find('/')] + desc = name + registerBugtracker(name, url, desc, 'bugzilla') + tracker = Bugzilla(name, url, desc) + return tracker + except: + return None + def get_bug(self, id): + url = "%s/xml.cgi?id=%d" % (self.url,id) + try: + bugxml = utils.web.getUrl(url) + zilladom = minidom.parseString(bugxml) + except Exception, e: + s = 'Could not parse XML returned by %s: %s' % (self.description, e) + raise BugtrackerError, s + bug_n = zilladom.getElementsByTagName('bug')[0] + if bug_n.hasAttribute('error'): + errtxt = bug_n.getAttribute('error') + s = 'Error getting %s bug #%s: %s' % (self.description, id, errtxt) + raise BugtrackerError, s + try: + title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0]) + status = _getnodetxt(bug_n.getElementsByTagName('bug_status')[0]) + try: + status += ": " + _getnodetxt(bug_n.getElementsByTagName('resolution')[0]) + except: + pass + component = _getnodetxt(bug_n.getElementsByTagName('component')[0]) + severity = _getnodetxt(bug_n.getElementsByTagName('bug_severity')[0]) + except Exception, e: + s = 'Could not parse XML returned by %s bugzilla: %s' % (self.description, e) + raise BugtrackerError, s + return (component, title, severity, status, "%s/show_bug.cgi?id=%d" % (self.url, id)) + +class Issuezilla(IBugtracker): + def get_bug(self, id): + url = "%s/xml.cgi?id=%d" % (self.url,id) + try: + bugxml = utils.web.getUrl(url) + zilladom = minidom.parseString(bugxml) + except Exception, e: + s = 'Could not parse XML returned by %s: %s' % (self.description, e) + raise BugtrackerError, s + bug_n = zilladom.getElementsByTagName('issue')[0] + if not (bug_n.getAttribute('status_code') == '200'): + s = 'Error getting %s bug #%s: %s' % (self.description, id, bug_n.getAttribute('status_message')) + raise BugtrackerError, s + try: + title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0]) + status = _getnodetxt(bug_n.getElementsByTagName('issue_status')[0]) + try: + status += ": " + _getnodetxt(bug_n.getElementsByTagName('resolution')[0]) + except: + pass + component = _getnodetxt(bug_n.getElementsByTagName('component')[0]) + severity = _getnodetxt(bug_n.getElementsByTagName('issue_type')[0]) + except Exception, e: + s = 'Could not parse XML returned by %s bugzilla: %s' % (self.description, e) + raise BugtrackerError, s + return (component, title, severity, status, "%s/show_bug.cgi?id=%d" % (self.url, id)) + +class Malone(IBugtracker): + def _parse(self, task): + parser = email.FeedParser.FeedParser() + parser.feed(task) + return parser.close() + def _sort(self, task1, task2): + # Status sort: + try: + statuses = ['Rejected', 'Fix Committed', 'Fix Released', 'Confirmed', 'In Progress', 'Needs Info', 'Unconfirmed'] + severities = ['Wishlist', 'Minor', 'Normal', 'Major', 'Critical'] + if task1['status'] not in statuses and task2['status'] in statuses: return 1 + if task1['status'] in statuses and task2['status'] not in statuses: return -1 + if task1['severity'] not in severities and task2['severity'] in severities: return 1 + if task1['severity'] in severities and task2['severity'] not in severities: return -1 + if not (task1['status'] == task2['status']): + if statuses.index(task1['status']) < statuses.index(task2['status']): + return -1 + return 1 + if not (task1['severity'] == task2['severity']): + if severities.index(task1['severity']) < severities.index(task2['severity']): + return -1 + return 1 + except: # Launchpad changed again? + return 0 + return 0 + def get_bug(self, id): + try: + bugdata = utils.web.getUrl("%s/%d/+text" % (self.url,id)) + except Exception, e: + if '404' in str(e): + s = 'Error getting %s bug #%s: Bug does not exist' % (self.description, id) + raise BugtrackerError, s + s = 'Could not parse data returned by %s: %s' % (self.description, e) + raise BugtrackerError, s + summary = {} + # Trap private bugs + if "" in bugdata: + raise BugtrackerError, "This bug is private" + try: + # Split bug data into separate pieces (bug data, task data) + data = bugdata.split('\n\n') + bugdata = data[0] + taskdata = data[1:] + parser = email.FeedParser.FeedParser() + parser.feed(bugdata) + bugdata = parser.close() + taskdata = map(self._parse, taskdata) + taskdata.sort(self._sort) + taskdata = taskdata[-1] + + except Exception, e: + s = 'Could not parse data returned by %s: %s' % (self.description, e) + raise BugtrackerError, s + t = taskdata['task'] + if '(' in t: + t = t[:t.rfind('(') -1] + return (t, bugdata['title'], taskdata['importance'], + taskdata['status'], "%s/bugs/%s" % (self.url.replace('/malone',''), id)) + +# +# Debbugs sucks donkeyballs +# * HTML pages are inconsistent +# * Parsing mboxes gets incorrect with cloning perversions (eg with bug 330000) +# * No sane way of accessing bug reports in a machine readable way (bts2ldap has no search on bugid) +# +# So sometimes the plugin will return incorrect things - so what. Fix the +# damn bts before complaining. +# There's a patch against the thing since august 2003 for enabling machine +# readable output: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=207225 +# +# It's not only releases that go slow in Debian, apparently the bugtracker +# development is even slower than that... +# +class Debbugs(IBugtracker): + def parse_mail(self, id, text, data): + (headers, text) = text.split("\n\n", 1) + for h in headers.split("\n"): + h2 = h.lower() + if h2.startswith('to') and ('%d-close' % id in h2 or '%d-done' % id in h2): + data['status'] = 'Closed' + if not data['title'] and h2.startswith('subject'): + data['title'] = h.strip() + + infirstmail = False + for l in text.split("\n"): + l2 = l.lower().split() + if len(l2) == 0: + if infirstmail: return + continue + if l2[0] in ['quit', 'stop', 'thank', '--']: + return + elif l2[0] == 'package:': + data['package'] = l2[1] + infirstmail = True + elif l2[0] == 'severity:': + data['severity'] = l2[1] + try: + if len(l2) > 1: + if l2[0] in ['reassign', 'reopen', 'retitle', 'severity'] and not (int(l2[1]) == id): + continue + except ValueError: # Parsing to int failed, so not an integer + if l2[0] == 'reassign': + data['package'] = l2[2] + elif l2[0] == 'reopen': + data['status'] = 'Open' + elif l2[0] == 'retitle': + data['title'] = l.split(None,2)[2] + elif l2[0] == 'severity': + data['severity'] = ls[2] + + def get_bug(self, id): + url = "%s/cgi-bin/bugreport.cgi?bug=%d;mbox=yes" % (self.url,id) + try: + bugdata = utils.web.getUrl(url) + except Exception, e: + s = 'Could not parse data returned by %s: %s' % (self.description, e) + raise BugtrackerError, s + if '

There is no record of Bug' in bugdata: + raise BugtrackerError, "%s bug %d does not exist" % (self.description, id) + try: + data = {'package': None,'title': None,'severity':None,'status':'Open'} + for m in bugdata.split("\n\n\nFrom"): + self.parse_mail(id, m, data) + except Exception, e: + s = 'Could not parse data returned by %s bugtracker: %s' % (self.description, e) + raise BugtrackerError, s + return (data['package'], data['title'], data['severity'], data['status'], "%s/%s" % (self.url, id)) + +# For trac based trackers we also need to do some screenscraping - should be +# doable unless a certain track instance uses weird templates. +class Trac(IBugtracker): + def get_bug(self, id): + url = "%s/%d" % (self.url, id) + try: + bugdata = utils.web.getUrl(url) + except Exception, e: + s = 'Could not parse data returned by %s: %s' % (self.description, e) + raise BugtrackerError, s + for l in bugdata.split("\n"): + if '

Ticket' in l: + severity = l[l.find('(')+1:l.find(')')] + if 'class="summary"' in l: + title = l[l.find('>')+1:l.find('')+8:l.find('')] + if 'headers="h_component"' in l: + package = l[l.find('>')+1:l.find('')+1:l.find('\[.*?\]\s*(?P.*?)</h2> + .*? + Priority.*?(?P<priority>\d+) + .*? + Status.*?<br>\s+(?P<status>\S+) + .*? + Resolution.*?<br>\s+(?P<resolution>\S+) + .*? + """, re.VERBOSE | re.DOTALL | re.I) + +class Sourceforge(IBugtracker): + _sf_url = 'http://sf.net/support/tracker.php?aid=%d' + def get_bug(self, id): + url = self._sf_url % id + try: + bugdata = utils.web.getUrl(url) + except Exception, e: + s = 'Could not parse data returned by %s: %s' % (self.description, e) + raise BugtrackerError, s + try: + reo = sfre.search(bugdata) + status = reo.group('status') + resolution = reo.group('resolution') + if not (resolution.lower() == 'none'): + status += ' ' + resolution + return (None, reo.group('title'), "Pri: %s" % reo.group('priority'), status, self._sf_url % id) + except: + raise BugtrackerError, "Bug not found" + +# Introspection is quite cool +defined_bugtrackers = {} +v = vars() +for k in v.keys(): + if type(v[k]) == type(IBugtracker) and issubclass(v[k], IBugtracker) and not (v[k] == IBugtracker): + defined_bugtrackers[k.lower()] = v[k] + +# Let's add a few bugtrackers by default +registerBugtracker('mozilla', 'http://bugzilla.mozilla.org', 'Mozilla', 'bugzilla') +registerBugtracker('ubuntu', 'http://bugzilla.ubuntu.com', 'Ubuntu', 'bugzilla') +registerBugtracker('gnome', 'http://bugzilla.gnome.org', 'Gnome', 'bugzilla') +registerBugtracker('gnome2', 'http://bugs.gnome.org', 'Gnome', 'bugzilla') +registerBugtracker('kde', 'http://bugs.kde.org', 'KDE', 'bugzilla') +registerBugtracker('ximian', 'http://bugzilla.ximian.com', 'Ximian', 'bugzilla') +registerBugtracker('freedesktop', 'http://bugzilla.freedesktop.org', 'Freedesktop', 'bugzilla') +registerBugtracker('freedesktop2', 'http://bugs.freedesktop.org', 'Freedesktop', 'bugzilla') +# Given that there is only one, let's add it by default +registerBugtracker('openoffice', 'http://openoffice.org/issues', 'OpenOffice.org', 'issuezilla') +# Given that there is only one, let's add it by default +registerBugtracker('malone', 'http://launchpad.net/malone', 'Malone', 'malone') +# Given that there is only one, let's add it by default +registerBugtracker('debian', 'http://bugs.debian.org', 'Debian', 'debbugs') +# Let's add a few bugtrackers by default +registerBugtracker('trac', 'http://projects.edgewall.com/trac/ticket', 'Trac', 'trac') +registerBugtracker('django', 'http://code.djangoproject.com/ticket', 'Django', 'trac') +# Let's add a few bugtrackers by default +registerBugtracker('supybot', 'http://sourceforge.net/tracker/?group_id=58965&atid=489447', 'Supybot', 'sourceforge') +# Special one, do NOT disable/delete +registerBugtracker('sourceforge', 'http://sourceforge.net/tracker/', 'Sourceforge', 'sourceforge') + +Class = Bugtracker diff --git a/Bugtracker/test.py b/Bugtracker/test.py new file mode 100644 index 0000000..510a401 --- /dev/null +++ b/Bugtracker/test.py @@ -0,0 +1,3 @@ +from supybot.test import * +class BugtrackerTestCase(PluginTestCase): + plugins = ('Bugtracker',) diff --git a/Changuard/README.txt b/Changuard/README.txt new file mode 100644 index 0000000..d60b47a --- /dev/null +++ b/Changuard/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/Changuard/__init__.py b/Changuard/__init__.py new file mode 100644 index 0000000..cf59297 --- /dev/null +++ b/Changuard/__init__.py @@ -0,0 +1,21 @@ +""" +Various channel protections +""" + +import supybot +import supybot.world as world + +__version__ = "0.1" +__author__ = supybot.Author('Dennis Kaarsemaker', 'Seveas', 'dennis@kaarsemaker.net') +__contributors__ = {} +__url__ = 'http://bots.ubuntulinux.nl' + +import config +reload(config) +import plugin +reload(plugin) + +if world.testing: + import test +Class = plugin.Class +configure = config.configure diff --git a/Changuard/config.py b/Changuard/config.py new file mode 100644 index 0000000..c628c81 --- /dev/null +++ b/Changuard/config.py @@ -0,0 +1,10 @@ +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Changuard', True) + +Changuard = conf.registerPlugin('Changuard') +conf.registerChannelValue(conf.supybot.plugins.Changuard, 'enabled', + registry.Boolean(False,"""Enable the guard plugin""")) diff --git a/Changuard/plugin.py b/Changuard/plugin.py new file mode 100644 index 0000000..0c0babb --- /dev/null +++ b/Changuard/plugin.py @@ -0,0 +1,25 @@ +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +import supybot.ircmsgs as ircmsgs + +class Changuard(callbacks.PluginRegexp): + """Channel guard""" + regexps = ['theban','badwords'] + + def theban(self, irc, msg, match): + r"""((\S\S\S\S\S.*?)\2\2\2\2\2|nextpicturez)""" + if self.registryValue('enabled', msg.args[0]): + if msg.args[0][0] == "#": + irc.queueMsg(ircmsgs.IrcMsg(command='REMOVE', args=(msg.args[0], msg.nick, "No flooding please"), msg=msg)) + irc.queueMsg(ircmsgs.ban(msg.args[0], '*!*@%s' % msg.host)) + + def badwords(self, irc, msg, match): + r"""http.*(sex|porn)""" + if self.registryValue('enabled', msg.args[0]): + if msg.args[0][0] == "#": + irc.queueMsg(ircmsgs.IrcMsg(command='REMOVE', args=(msg.args[0], msg.nick, "Watch your language!"), msg=msg)) + +Class = Changuard diff --git a/Changuard/test.py b/Changuard/test.py new file mode 100644 index 0000000..989b128 --- /dev/null +++ b/Changuard/test.py @@ -0,0 +1,4 @@ +from supybot.test import * + +class ChanguardTestCase(PluginTestCase): + plugins = ('Changuard',) diff --git a/Encyclopedia/README.txt b/Encyclopedia/README.txt new file mode 100644 index 0000000..d60b47a --- /dev/null +++ b/Encyclopedia/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/Encyclopedia/__init__.py b/Encyclopedia/__init__.py new file mode 100644 index 0000000..fe41ad2 --- /dev/null +++ b/Encyclopedia/__init__.py @@ -0,0 +1,42 @@ +### +# Copyright (c) 2006, Dennis Kaarsemaker +# All rights reserved. +# +# +### + +""" +Add a description of the plugin (to be presented to the user inside the wizard) +here. This should describe *what* the plugin does. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.authors.unknown + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = '' # 'http://supybot.com/Members/yourname/Factoid plugin/download' + +import config +import plugin +reload(plugin) # In case we're being reloaded. +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Encyclopedia/config.py b/Encyclopedia/config.py new file mode 100644 index 0000000..8ebe274 --- /dev/null +++ b/Encyclopedia/config.py @@ -0,0 +1,28 @@ +### +# Copyright (c) 2006, Dennis Kaarsemaker +# All rights reserved. +# +# +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified himself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Encyclopedia', True) + + +Encyclopedia = conf.registerPlugin('Encyclopedia') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(Factoid, 'someConfigVariableName', +# registry.Boolean(False, """Help for someConfigVariableName.""")) +conf.registerChannelValue(Encyclopedia, 'database', + registry.String('', 'Name of database to use')) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Encyclopedia/factoids.cgi b/Encyclopedia/factoids.cgi new file mode 100755 index 0000000..64af980 --- /dev/null +++ b/Encyclopedia/factoids.cgi @@ -0,0 +1,85 @@ +#!/usr/bin/python + +import sqlite +import datetime +import cgi, cgitb +from math import ceil +import re +cgitb.enable + +NUM_PER_PAGE=50.0 + +buf = '' +def out(txt): + global buf + buf += str(txt) + +def link(match): + url = match.group('url') + txt = url + if len(txt) > 30: + txt = txt[:20] + '…' + txt[-10:] + return '<a href="%s">%s</a>' % (url, txt) + +def q(txt): + txt = str(txt).replace('&','&').replace('<','<').replace('>','>').replace('"','"').replace('\n','<br />') + # linkify + rx = re.compile('(?P<url>(https?://\S+|www\S+))') + return rx.sub(link, txt) + +database = 'ubuntu' + +form = cgi.FieldStorage() +try: + page = int(form['page'].value) +except: + page = 0 +order_by = 'added DESC' +try: + order_by = form['order'].value + if order_by not in ('added DESC', 'added ASC', 'name DESC', 'name ASC', 'popularity DESC','popularity ASC'): + order_by = 'added DESC' +except: + order_by = 'added DESC' + +con = sqlite.connect('/home/dennis/ubugtu/data/facts/%s.db' % database) +cur = con.cursor() + +cur.execute("""SELECT COUNT(*) FROM facts WHERE value NOT LIKE '<alias>%%'""") +num = cur.fetchall()[0][0] +npages = int(ceil(num / float(NUM_PER_PAGE))) +out('·') +for i in range(npages): + out(' <a href="factoids.cgi?order=%s&page=%s">%d</a> ·' % (order_by, i, i+1)) +out('<br />Order by<br />·') +out(' <a href="factoids.cgi?order=%s&page=%d">%s</a> ·' % ('name ASC', page, 'Name +')) +out(' <a href="factoids.cgi?order=%s&page=%d">%s</a> ·' % ('name DESC', page, 'Name -')) +out(' <a href="factoids.cgi?order=%s&page=%d">%s</a> ·' % ('popularity ASC', page, 'Popularity +')) +out(' <a href="factoids.cgi?order=%s&page=%d">%s</a> ·' % ('popularity DESC', page, 'Popularity -')) +out(' <a href="factoids.cgi?order=%s&page=%d">%s</a> ·' % ('added ASC', page, 'Date added +')) +out(' <a href="factoids.cgi?order=%s&page=%d">%s</a> ·' % ('added DESC', page, 'Date added -')) + + +out('<table cellspacing="0"><tr><th>Factoid</th><th>Value</th><th>Author</th></tr>') + +cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE value NOT LIKE '<alias>%%' ORDER BY %s LIMIT %d, %d" % (order_by, page*NUM_PER_PAGE, NUM_PER_PAGE)) +factoids = cur.fetchall() +i = 0 +for f in factoids: + cur.execute("SELECT name FROM facts WHERE value LIKE %s", '<alias> ' + f[0]) + f = list(f) + f[0] += '\n' + '\n'.join([x[0] for x in cur.fetchall()]) + out('<tr') + if i % 2: out(' class="bg2"') + i += 1 + out('><td>%s</td><td>%s</td><td>%s<br />Added on: %s<br />Requested %s times</td>' % tuple([q(x) for x in f])) + +out('</table>') + +print "Content-Type: text/html; charset=UTF-8" +print "" + +fd = open('factoids.tmpl') +tmpl = fd.read() +fd.close() +print tmpl % (buf) diff --git a/Encyclopedia/factoids.tmpl b/Encyclopedia/factoids.tmpl new file mode 100644 index 0000000..4ea389c --- /dev/null +++ b/Encyclopedia/factoids.tmpl @@ -0,0 +1,134 @@ +<html> +<head> +<title>Ubotu factoids + + + + + +
+

Ubotu factoids

+

+%s +

+

+©2006 Dennis Kaarsemaker +

+
+ + diff --git a/Encyclopedia/plugin.py b/Encyclopedia/plugin.py new file mode 100644 index 0000000..69a0588 --- /dev/null +++ b/Encyclopedia/plugin.py @@ -0,0 +1,442 @@ +### +# Copyright (c) 2006, Dennis Kaarsemaker +# All rights reserved. +# +# +### + +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.ircmsgs as ircmsgs +import supybot.callbacks as callbacks +import sqlite, datetime, time, apt_pkg, commands +import supybot.registry as registry +import supybot.ircdb as ircdb +from email import FeedParser +import re +import os +apt_pkg.init() + +fallback = ('ubuntu', '#ubuntu') +datadir = '/home/dennis/ubugtu/data/facts' + +def r(section): + if '/' in section: + return section[:section.find('/')] + return 'main' + +class Factoid: + def __init__(self, name, value, author, added, popularity): + self.name = name; self.value = value + self.author = author; self.added = added + self.popularity = popularity + +def get_factoid(db, name, channel): + cur = db.cursor() + cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name = %s", '%s-%s' % (name, channel)) + factoid = cur.fetchall() + if len(factoid): + f = factoid[0] + return Factoid(f[0],f[1],f[2],f[3],f[4]) + cur.execute("SELECT name, value, author, added, popularity FROM facts WHERE name = %s", name) + factoid = cur.fetchall() + if len(factoid): + f = factoid[0] + return Factoid(f[0],f[1],f[2],f[3],f[4]) + return None + +def resolve_alias(db,factoid,channel,loop=0): + if loop >= 10: + return Factoid('','Error: infinite loop detected','','',0) + if factoid.value.lower().startswith(''): + new_factoid = get_factoid(db,factoid.value[7:].lower().strip(),channel) + if not new_factoid: + return Factoid('','Error: unresolvable ','','',0) + else: + return resolve_alias(db, new_factoid, channel, loop+1) + else: + return factoid + +class Encyclopedia(callbacks.PluginRegexp): + """!factoid: show factoid""" + threaded = True + regexps = ['showfactoid', 'addfactoid', 'deletefactoid','info','find','editfactoid','searchfactoid','seen'] + + def __init__(self, irc): + callbacks.PluginRegexp.__init__(self, irc) + self.databases = {} + self.times = {} + self.seens = {} + + # Capability check + def _precheck(self, irc, msg, capability=None, timeout=None): + channel = msg.args[0].lower() + inchannel = channel.startswith('#') + excl = msg.args[1].startswith('!') + if inchannel and not excl: + return False + for c in irc.callbacks: + comm = msg.args[1].split()[0] + print c.isDisabled(comm) + if c.isCommandMethod(comm) and not c.isDisabled(comm): + return False + if capability: + try: + _ = ircdb.users.getUser(msg.prefix) + except KeyError: + irc.queueMsg(ircmsgs.privmsg('#ubuntu-ops', "In %s, %s said: %s" % (msg.args[0], msg.nick, msg.args[1]))) + irc.reply("Your edit request has been forwarded to #ubuntu-ops. Thank you for your attention to detail",private=True) + return False + if not ircdb.checkCapability(msg.prefix, capability): + irc.queueMsg(ircmsgs.privmsg('#ubuntu-ops', "In %s, %s said: %s" % (msg.args[0], msg.nick, msg.args[1]))) + irc.reply("Your edit request has been forwarded to #ubuntu-ops. Thank you for your attention to detail",private=True) + return False + if timeout: + for key in self.times.keys(): + if self.times[key] < time.time() - 15: + self.times.pop(key) + if timeout in self.times: + return False + self.times[timeout] = time.time() + db = self.registryValue('database',channel) + if not db: + db,channel = fallback + if channel not in self.databases: + self.databases[channel] = sqlite.connect(os.path.join(datadir, '%s.db' % db)) + return self.databases[channel] + + def searchfactoid(self, irc, msg, match): + r"^!?search\s+(?P.+)" + db = self._precheck(irc, msg, timeout=(msg.args[0],match.group('query'))) + if not db: return + cur = db.cursor() + query = '%%%s%%' % match.group('query').replace('%','').replace('*','%') + try: + cur.execute("SELECT name FROM facts WHERE (value LIKE %s OR name LIKE %s ) AND value NOT LIKE '%%'", (query, query)) + data = cur.fetchall() + all = [x[0] for x in data] + cur.execute("SELECT value FROM facts WHERE name LIKE %s AND value LIKE '%%'", query) + data = cur.fetchall() + all += [x[0][7:].strip() for x in data] + all = list(set(all)) + + if len(all) > 10: + irc.reply("Found: %s (and %d more)" % (', '.join(all[:10]), len(all)-10)) + elif len(all): + irc.reply("Found: %s" % ', '.join(all)) + else: + irc.reply("Found nothing") + except: + irc.error('An error occured (code 561)') + + def showfactoid(self, irc, msg, match): + r"^(!|ubotu\S?\s)?(?P-)?\s*(tell\s+(?P\S+)\s+about\s+)?(?P\S.*?)(>\s*(?P\S+))?$" + db = self._precheck(irc, msg, timeout=(msg.args[0], match.group('nick'), match.group('factoid'), match.group('nick2'))) + if not db: return + to = channel = msg.args[0] + if channel[0] != '#': + to = msg.nick + cur = db.cursor() + retmsg = '' + + noalias = match.group('noalias') + factoid = match.group('factoid').lower().strip() + if ' is ' in match.group(0) or \ + '=~' in match.group(0) or \ + '' in match.group(0) or \ + factoid.startswith('forget ') or \ + factoid.startswith('info ') or \ + factoid.startswith('find ') or \ + factoid.startswith('search ') or \ + factoid.startswith('seen'): + return + + #if channel.startswith('#'): + if True: + nick = match.group('nick') + if match.group('nick2'): nick = match.group('nick2') + if nick == 'me': nick = msg.nick + if nick: + for chan in irc.state.channels: + if nick in irc.state.channels[chan].users and\ + msg.nick in irc.state.channels[chan].users: + retmsg = '%s wants you to know: ' % msg.nick + to = nick + break + else: + irc.error("That person could not be found in any channel you're in") + return + + # Retrieve factoid + try: + factoid = get_factoid(db, factoid, channel) + if not factoid: + irc.reply('I know nothing about %s' % match.group('factoid')) + return + # Output factoid + if not noalias: + factoid = resolve_alias(db,factoid,channel) + else: + cur.execute("SELECT name FROM facts WHERE value = %s", ' ' + factoid.name) + data = cur.fetchall() + if(len(data)): + irc.queueMsg(ircmsgs.privmsg(to, "%s aliases: %s" % (factoid.name, ', '.join([x[0].strip() for x in data])))) + return + # Do timing + if not self._precheck(irc, msg, timeout=(to,factoid.name)): + return + cur.execute("UPDATE FACTS SET popularity = %d WHERE name = %s", factoid.popularity+1, factoid.name) + db.commit() + if factoid.value.startswith(''): + irc.queueMsg(ircmsgs.privmsg(to, '%s%s' % (retmsg, factoid.value[7:].strip()))) + else: + irc.queueMsg(ircmsgs.privmsg(to, '%s%s is %s' % (retmsg, factoid.name, factoid.value.strip()))) + except: + raise + irc.error('An error occured (code 813)') + + def addfactoid(self, irc, msg, match): + r"^!?(?Pno,?\s+)?(?P\S.*?)\s+is\s+(?P\S.*)" + db = self._precheck(irc, msg, capability='editfactoids', timeout=(msg.args[0],match.group(0))) + if not db: return + channel = msg.args[0] + cur = db.cursor() + + factoid = match.group('factoid').lower().strip() + fact = match.group('fact').strip() + if '' in match.group(0) or \ + '=~' in match.group(0) or \ + factoid.startswith('forget') or \ + factoid.startswith('info') or \ + factoid.startswith('find') or \ + factoid.startswith('search'): + return + + try: + # See if the alias exists and resolve it... + old_factoid = get_factoid(db, factoid, channel) + if old_factoid: + if not fact.startswith(''): + old_factoid = resolve_alias(db, old_factoid, channel) + # Unresolvable alias + if not old_factoid.name: + irc.reply(old_factoid.value) + return + if match.group('no'): + if fact.startswith(''): + cur.execute("SELECT COUNT(*) FROM facts WHERE value = %s", ' ' + factoid) + num = cur.fetchall()[0][0] + if num: + irc.reply("Can't turn factoid with aliases into an alias") + return + alias_factoid = get_factoid(db, fact[7:].lower().strip(), channel) + if not alias_factoid: + alias_factoid = Factoid('','Error: unresolvable ','','',0) + else: + alias_factoid = resolve_alias(db, alias_factoid, channel) + if not alias_factoid.name: + irc.reply(alias_factoid.value) + return + fact = ' %s' % alias_factoid.name + fact = fact.lower() + cur.execute("""UPDATE facts SET value=%s, author=%s, added=%s WHERE name=%s""", + (fact, msg.prefix, str(datetime.datetime.now()), old_factoid.name)) + db.commit() + irc.reply("I'll remember that") + else: + irc.reply('%s is already known...' % factoid) + else: + if fact.lower().startswith(''): + old_factoid = get_factoid(db, fact[7:].lower().strip(), channel) + if not old_factoid: + old_factoid = Factoid('','Error: unresolvable ','','',0) + else: + old_factoid = resolve_alias(db, old_factoid, channel) + if not old_factoid.name: + irc.reply(old_factoid.value) + return + fact = ' %s' % old_factoid.name + fact = fact.lower() + cur.execute("""INSERT INTO facts (name, value, author, added) VALUES + (%s, %s, %s, %s)""", (factoid, fact, msg.prefix, str(datetime.datetime.now()))) + db.commit() + irc.reply("I'll remember that") + except: + irc.error('An error occured (code 735)') + + def editfactoid(self, irc, msg, match): + r"^!?(?P.*?)\s*(=~|(\s+is\s*))\s*s?(?P.*)" + db = self._precheck(irc, msg, capability='editfactoids', timeout=(msg.args[0],match.group(0))) + if not db: return + channel = msg.args[0] + cur = db.cursor() + + factoid = match.group('factoid').lower().strip() + regex = match.group('regex').strip() + if factoid.startswith('forget') or \ + factoid.startswith('info') or \ + factoid.startswith('find') or \ + factoid.startswith('search'): return + # Store factoid if nonexistant or 'no' is given + try: + # See if the alias exists and resolve it... + factoid = get_factoid(db, factoid, channel) + if factoid: + factoid = resolve_alias(db, factoid, channel) + # Unresolvable alias + if not factoid.name: + irc.reply(old_factoid.value) + return + delim = regex[0] + if regex[-1] != delim: + irc.reply("Missing end delimiter") + return + data = regex.split(delim)[1:-1] + if len(data) != 2: + irc.reply("You used the delimiter too often. Maybe try another one?") + return + regex, change = data + if '' in change.lower(): + irc.reply("Can't turn factoids into aliases this way") + return + try: + regex = re.compile(regex) + except: + irc.reply("Malformed regex") + return + newval = regex.sub(change, factoid.value, 1) + if newval != factoid.value: + cur.execute("""UPDATE facts SET value=%s, author=%s, added=%s WHERE name=%s""", + (newval, msg.prefix, str(datetime.datetime.now()), factoid.name)) + db.commit() + irc.reply("I'll remember that") + else: + irc.reply("No changes, not saving") + else: + irc.reply('I know nothing about %s' % match.group('factoid')) + except: + irc.error('An error occured (code 735)') + + def deletefactoid(self, irc, msg, match): + r"^!?forget\s+(?P\S.*)" + db = self._precheck(irc, msg, capability='editfactoids', timeout=(msg.args[0],match.group('factoid'))) + if not db: return + channel = msg.args[0] + cur = db.cursor() + try: + cur.execute("SELECT COUNT(*) FROM facts WHERE value = %s", ' ' + match.group('factoid')) + num = cur.fetchall()[0][0] + if num: + irc.reply("Can't forget factoids with aliases") + else: + cur.execute("DELETE FROM facts WHERE name = %s", match.group('factoid')) + db.commit() + irc.reply("I've forgotten it") + except: + raise + irc.error('An error occured (code 124)') + + + aptcommand = """apt-cache\\ + -o"Dir::State::Lists=/home/dennis/ubugtu/data/%s"\\ + -o"Dir::etc::sourcelist=/home/dennis/ubugtu/data/%s.list"\\ + -o"Dir::State::status=/home/dennis/ubugtu/data/%s.status"\\ + -o"Dir::Cache=/home/dennis/ubugtu/data/cache"\\ + %s %s""" + def info(self, irc, msg, match): + r"^!?info\s+(?P\S+)(\s+(?P\S+))?" + if not self._precheck(irc, msg, timeout=(msg.args[0],match.group('package'), match.group('distro'))): + return + distro = 'dapper' + if (match.group('distro') in ('warty','hoary','breezy','dapper','edgy')): + distro = match.group('distro') + data = commands.getoutput(self.aptcommand % (distro, distro, distro, 'show', match.group('package'))) + if not data or 'E: No packages found' in data: + irc.reply('Package %s does not exist in %s' % (match.group('package'), distro)) + else: + maxp = {'Version': '0'} + packages = [x.strip() for x in data.split('\n\n')] + for p in packages: + if not p.strip(): + continue + parser = FeedParser.FeedParser() + parser.feed(p) + p = parser.close() + if apt_pkg.VersionCompare(maxp['Version'], p['Version']) < 0: + maxp = p + del parser + irc.reply("%s: %s. In repository %s, is %s. Version %s (%s), package size %s kB, installed size %s kB" % + (maxp['Package'], maxp['Description'].split('\n')[0], r(maxp['Section']), + maxp['Priority'], maxp['Version'], distro, int(maxp['Size'])/1024, maxp['Installed-Size'])) + + def find(self, irc, msg, match): + r"^!?find\s+(?P\S+)(\s+(?P\S+))?" + if not self._precheck(irc, msg, timeout=(msg.args[0],match.group('package'), match.group('distro'),2)): + return + distro = 'dapper' + if (match.group('distro') in ('warty','hoary','breezy','dapper','edgy')): + distro = match.group('distro') + data = commands.getoutput(self.aptcommand % (distro, distro, distro, 'search -n', match.group('package'))) + if not data: + irc.reply("No packages matching '%s' could be found" % match.group('package')) + else: + pkgs = [x.split()[0] for x in data.split('\n')] + if len(pkgs) > 5: + irc.reply("Found: %s (and %d others)" % (', '.join(pkgs[:5]), len(pkgs) -5)) + else: + irc.reply("Found: %s" % ', '.join(pkgs[:5])) + + def seen(self, irc, msg, match): + r"^!?seen\s+(?P\S+)" + if not self._precheck(irc, msg, timeout=(msg.args[0],match.group('nick'))): + return + to = msg.args[0] + if msg.args[0][0] != '#': + to = msg.nick + self.seens[match.group('nick')] = (to, time.time()) + irc.queueMsg(ircmsgs.privmsg('seenserv', "seen %s" % match.group('nick'))) + + def doNotice(self, irc, msg): + if msg.nick.lower() == 'seenserv': + resp = msg.args[1] + for n in self.seens.keys(): + if self.seens[n][1] < time.time() - 10: + self.seens.pop(n) + for n in self.seens.keys(): + if n.lower() in resp.lower(): + irc.queueMsg(ircmsgs.privmsg(self.seens[n][0], resp)) + self.seens.pop(n) + + def addeditor(self, irc, msg, args, name): + self._precheck(irc, msg, capability='addeditors') + try: + u = ircdb.users.getUser(name) + except: + irc.error('User %s is not registered' % name) + else: + u.addCapability('editfactoids') + addeditor = wrap(addeditor, ['text']) + + def editors(self, irc, msg, args): + irc.reply(', '.join([ircdb.users.getUser(u).name for u in ircdb.users.users \ + if 'editfactoids' in ircdb.users.getUser(u).capabilities])) + editors = wrap(editors) + def moderators(self, irc, msg, args): + irc.reply(', '.join([ircdb.users.getUser(u).name for u in ircdb.users.users \ + if 'addeditors' in ircdb.users.getUser(u).capabilities])) + moderators = wrap(moderators) + + def removeeditor(self, irc, msg, args, name): + self._precheck(irc, msg, capability='addeditors') + try: + u = ircdb.users.getUser(name) + except: + irc.error('User %s is not registered' % name) + else: + u.removeCapability('editfactoids') + removeeditor = wrap(removeeditor, ['text']) + +Class = Encyclopedia +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Encyclopedia/test.py b/Encyclopedia/test.py new file mode 100644 index 0000000..b754ab0 --- /dev/null +++ b/Encyclopedia/test.py @@ -0,0 +1,14 @@ +### +# Copyright (c) 2006, Dennis Kaarsemaker +# All rights reserved. +# +# +### + +from supybot.test import * + +class Factoid pluginTestCase(PluginTestCase): + plugins = ('Factoid plugin',) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4907008 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +ALL=$(shell find . -name '???*' -type d -printf '%p.tar.gz\n') + +default: $(ALL) + +%.tar.gz: % + tar zcf $@ $< + +clean: + rm -f $(ALL) diff --git a/Mess/README.txt b/Mess/README.txt new file mode 100644 index 0000000..d60b47a --- /dev/null +++ b/Mess/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/Mess/__init__.py b/Mess/__init__.py new file mode 100644 index 0000000..ee00ea2 --- /dev/null +++ b/Mess/__init__.py @@ -0,0 +1,21 @@ +""" +Random mess plugin +""" + +import supybot +import supybot.world as world + +__version__ = "0.5" +__author__ = supybot.Author('Dennis Kaarsemaker','Seveas','dennis@kaarsemaker.net') +__contributors__ = {} +__url__ = 'https://bots.ubuntulinux.nl' + +import config +reload(config) +import plugin +reload(plugin) + +if world.testing: + import test +Class = plugin.Class +configure = config.configure diff --git a/Mess/bofh.txt b/Mess/bofh.txt new file mode 100644 index 0000000..0a09d5d --- /dev/null +++ b/Mess/bofh.txt @@ -0,0 +1,453 @@ +clock speed +solar flares +electromagnetic radiation from satellite debris +static from nylon underwear +static from plastic slide rules +global warming +poor power conditioning +static buildup +doppler effect +hardware stress fractures +magnetic interference from money/credit cards +dry joints on cable plug +we're waiting for the phone company to fix that line +sounds like a Windows problem, try calling Microsoft support +temporary routing anomaly +somebody was calculating pi on the server +fat electrons in the lines +excess surge protection +floating point processor overflow +divide-by-zero error +POSIX compliance problem +monitor resolution too high +improperly oriented keyboard +network packets travelling uphill (use a carrier pigeon) +Decreasing electron flux +first Saturday after first full moon in Winter +radiosity depletion +CPU radiator broken +It works the way the Wang did, what's the problem +positron router malfunction +cellular telephone interference +techtonic stress +piezo-electric interference +(l)user error +working as designed +dynamic software linking table corrupted +heavy gravity fluctuation, move computer to floor rapidly +secretary plugged hairdryer into UPS +terrorist activities +not enough memory, go get system upgrade +interrupt configuration error +spaghetti cable cause packet failure +boss forgot system password +bank holiday - system operating credits not recharged +virus attack, luser responsible +waste water tank overflowed onto computer +Complete Transient Lockout +bad ether in the cables +Bogon emissions +Change in Earth's rotational speed +Cosmic ray particles crashed through the hard disk platter +Smell from unhygienic janitorial staff wrecked the tape heads +Little hamster in running wheel had coronary; waiting for replacement to be Fedexed from Wyoming +Evil dogs hypnotised the night shift +Plumber mistook routing panel for decorative wall fixture +Electricians made popcorn in the power supply +Groundskeepers stole the root password +high pressure system failure +failed trials, system needs redesigned +system has been recalled +not approved by the FCC +need to wrap system in aluminum foil to fix problem +not properly grounded, please bury computer +CPU needs recalibration +system needs to be rebooted +bit bucket overflow +descramble code needed from software company +only available on a need to know basis +knot in cables caused data stream to become twisted and kinked +nesting roaches shorted out the ether cable +The file system is full of it +Satan did it +Daemons did it +You're out of memory +There isn't any problem +Unoptimized hard drive +Typo in the code +Yes, yes, its called a design limitation +Look, buddy: Windows 3.1 IS A General Protection Fault. +That's a great computer you have there; have you considered how it would work as a BSD machine? +Please excuse me, I have to circuit an AC line through my head to get this database working. +Yeah, yo mama dresses you funny and you need a mouse to delete files. +Support staff hung over, send aspirin and come back LATER. +Someone is standing on the ethernet cable, causing a kink in the cable +Windows 95 undocumented "feature" +Runt packets +Password is too complex to decrypt +Boss' kid fucked up the machine +Electromagnetic energy loss +Budget cuts +Mouse chewed through power cable +Stale file handle (next time use Tupperware(tm)!) +Feature not yet implemented +Internet outage +Pentium FDIV bug +Vendor no longer supports the product +Small animal kamikaze attack on power supplies +The vendor put the bug there. +SIMM crosstalk. +IRQ dropout +Collapsed Backbone +Power company testing new voltage spike (creation) equipment +operators on strike due to broken coffee machine +backup tape overwritten with copy of system manager's favourite CD +UPS interrupted the server's power +The electrician didn't know what the yellow cable was so he yanked the ethernet out. +The keyboard isn't plugged in +The air conditioning water supply pipe ruptured over the machine room +The electricity substation in the car park blew up. +The rolling stones concert down the road caused a brown out +The salesman drove over the CPU board. +The monitor is plugged into the serial port +Root nameservers are out of sync +electro-magnetic pulses from French above ground nuke testing. +your keyboard's space bar is generating spurious keycodes. +the real ttys became pseudo ttys and vice-versa. +the printer thinks its a router. +the router thinks its a printer. +evil hackers from Serbia. +we just switched to FDDI. +halon system went off and killed the operators. +because Bill Gates is a Jehovah's witness and so nothing can work on St. Swithin's day. +user to computer ratio too high. +user to computer ration too low. +we just switched to Sprint. +it has Intel Inside +Sticky bits on disk. +Power Company having EMP problems with their reactor +The ring needs another token +new management +telnet: Unable to connect to remote host: Connection refused +SCSI Chain overterminated +It's not plugged in. +because of network lag due to too many people playing deathmatch +You put the disk in upside down. +Daemons loose in system. +User was distributing pornography on server; system seized by FBI. +BNC (brain not connected) +UBNC (user brain not connected) +LBNC (luser brain not connected) +disks spinning backwards - toggle the hemisphere jumper. +new guy cross-connected phone lines with ac power bus. +had to use hammer to free stuck disk drive heads. +Too few computrons available. +Flat tire on station wagon with tapes. +Communications satellite used by the military for star wars. +Party-bug in the Aloha protocol. +Insert coin for new game +Dew on the telephone lines. +Arcserve crashed the server again. +Some one needed the powerstrip, so they pulled the switch plug. +My pony-tail hit the on/off switch on the power strip. +Big to little endian conversion error +You can tune a file system, but you can't tune a fish +Dumb terminal +Zombie processes haunting the computer +Incorrect time synchronization +Defunct processes +Stubborn processes +non-redundant fan failure +monitor VLF leakage +bugs in the RAID +no "any" key on keyboard +root rot +Backbone Scoliosis +/pub/lunch +excessive collisions & not enough packet ambulances +le0: no carrier: transceiver cable problem? +broadcast packets on wrong frequency +popper unable to process jumbo kernel +NOTICE: alloc: /dev/null: filesystem full +pseudo-user on a pseudo-terminal +Recursive traversal of loopback mount points +Backbone adjustment +OS swapped to disk +vapors from evaporating sticky-note adhesives +sticktion +short leg on process table +multicasts on broken packets +ether leak +Atilla the Hub +endothermal recalibration +filesystem not big enough for Jumbo Kernel Patch +loop found in loop in redundant loopback +system consumed all the paper for paging +permission denied +Reformatting Page. Wait... +Either the disk or the processor is on fire. +SCSI's too wide. +Proprietary Information. +Just type 'mv * /dev/null'. +runaway cat on system. +Did you pay the new Support Fee? +We only support a 1200 bps connection. +We only support a 28000 bps connection. +Me no internet, only janitor, me just wax floors. +I'm sorry a pentium won't do, you need an SGI to connect with us. +Post-it Note Sludge leaked into the monitor. +the curls in your keyboard cord are losing electricity. +The monitor needs another box of pixels. +RPC_PMAP_FAILURE +kernel panic: write-only-memory (/dev/wom0) capacity exceeded. +Write-only-memory subsystem too slow for this machine. Contact your local dealer. +Just pick up the phone and give modem connect sounds. "Well you said we should get more lines so we don't have voice lines." +Quantum dynamics are affecting the transistors +Police are examining all internet packets in the search for a narco-net-trafficker +We are currently trying a new concept of using a live mouse. Unfortunately, one has yet to survive being hooked up to the computer.....please bear with us. +Your mail is being routed through Germany ... and they're censoring us. +Only people with names beginning with 'A' are getting mail this week (a la Microsoft) +We didn't pay the Internet bill and it's been cut off. +Lightning strikes. +Of course it doesn't work. We've performed a software upgrade. +Change your language to Finnish. +Fluorescent lights are generating negative ions. If turning them off doesn't work, take them out and put tin foil on the ends. +High nuclear activity in your area. +What office are you in? Oh, that one. Did you know that your building was built over the universities first nuclear research site? And wow, aren't you the lucky one, your office is right over where the core is buried! +The MGs ran out of gas. +The UPS doesn't have a battery backup. +Recursivity. Call back if it happens again. +Someone thought The Big Red Button was a light switch. +The mainframe needs to rest. It's getting old, you know. +I'm not sure. Try calling the Internet's head office -- it's in the book. +The lines are all busy (busied out, that is -- why let them in to begin with?). +Jan 9 16:41:27 huber su: 'su root' succeeded for .... on /dev/pts/1 +It's those computer people in London. They keep stuffing things up. +A star wars satellite accidently blew up the WAN. +Fatal error right in front of screen +That function is not currently supported, but Bill Gates assures us it will be featured in the next upgrade. +wrong polarity of neutron flow +Lusers learning curve appears to be fractal +We had to turn off that service to comply with the CDA Bill. +Ionization from the air-conditioning +TCP/IP UDP alarm threshold is set too low. +Someone is broadcasting pygmy packets and the router doesn't know how to deal with them. +The new frame relay network hasn't bedded down the software loop transmitter yet. +Fanout dropping voltage too much, try cutting some of those little traces +Plate voltage too low on demodulator tube +You did wha... oh _dear_.... +CPU needs bearings repacked +Too many little pins on CPU confusing it, bend back and forth until 10-20% are neatly removed. Do _not_ leave metal bits visible! +_Rosin_ core solder? But... +Software uses US measurements, but the OS is in metric... +The computer fleetly, mouse and all. +Your cat tried to eat the mouse. +The Borg tried to assimilate your system. Resistance is futile. +It must have been the lightning storm we had yesterday +Due to Federal Budget problems we have been forced to cut back on the number of users able to access the system at one time +Too much radiation coming from the soil. +Unfortunately we have run out of bits/bytes/whatever. Don't worry, the next supply will be coming next week. +Program load too heavy for processor to lift. +Processes running slowly due to weak power supply +Our ISP is having frame relay problems +We've run out of licenses +Interference from lunar radiation +Standing room only on the bus. +You need to install an RTFM interface. +That would be because the software doesn't work. +That's easy to fix, but I can't be bothered. +Someone's tie is caught in the printer, and if anything else gets printed, he'll be in it too. +We're upgrading /dev/null +The Usenet news is out of date +Our POP server was kidnapped by a weasel. +It's stuck in the Web. +Your modem doesn't speak English. +The mouse escaped. +All of the packets are empty. +The UPS is on strike. +Neutrino overload on the nameserver +Melting hard drives +Someone has messed up the kernel pointers +The kernel license has expired +Netscape has crashed +The cord jumped over and hit the power switch. +It was OK before you touched it. +Bit rot +U.S. Postal Service +Your Flux Capacitor has gone bad. +The Dilithium Crystals need to be rotated. +The static electricity routing is acting up... +Traceroute says that there is a routing problem in the backbone. It's not our problem. +The co-locator cannot verify the frame-relay gateway to the ISDN server. +High altitude condensation from U.S.A.F prototype aircraft has contaminated the primary subnet mask. Turn off your computer for 9 days to avoid damaging it. +Lawn mower blade in your fan need sharpening +Electrons on a bender +Telecommunications is upgrading. +Telecommunications is downgrading. +Telecommunications is downshifting. +Hard drive sleeping. Let it wake up on it's own... +Interference between the keyboard and the chair. +The CPU has shifted, and become decentralized. +Due to the CDA, we no longer have a root account. +We ran out of dial tone and we're and waiting for the phone company to deliver another bottle. +You must've hit the wrong any key. +PCMCIA slave driver +The Token fell out of the ring. Call us when you find it. +The hardware bus needs a new token. +Too many interrupts +Not enough interrupts +The data on your hard drive is out of balance. +Digital Manipulator exceeding velocity parameters +appears to be a Slow/Narrow SCSI-0 Interface problem +microelectronic Riemannian curved-space fault in write-only file system +fractal radiation jamming the backbone +routing problems on the neural net +IRQ-problems with the Un-Interruptible-Power-Supply +CPU-angle has to be adjusted because of vibrations coming from the nearby road +emissions from GSM-phones +CD-ROM server needs recalibration +firewall needs cooling +asynchronous inode failure +transient bus protocol violation +incompatible bit-registration operators +your process is not ISO 9000 compliant +You need to upgrade your VESA local bus to a MasterCard local bus. +The recent proliferation of Nuclear Testing +Elves on strike. (Why do they call EMAG Elf Magic) +Internet exceeded Luser level, please wait until a luser logs off before attempting to log back on. +Your EMAIL is now being delivered by the USPS. +Your computer hasn't been returning all the bits it gets from the Internet. +You've been infected by the Telescoping Hubble virus. +Scheduled global CPU outage +Your Pentium has a heating problem - try cooling it with ice cold water.(Do not turn of your computer, you do not want to cool down the Pentium Chip while he isn't working, do you?) +Your processor has processed too many instructions. Turn it off immediately, do not type any commands!! +Your packets were eaten by the terminator +Your processor does not develop enough heat. +We need a licensed electrician to replace the light bulbs in the computer room. +The POP server is out of Coke +Fiber optics caused gas main leak +Server depressed, needs Prozac +quantum decoherence +those damn raccoons! +suboptimal routing experience +A plumber is needed, the network drain is clogged +50% of the manual is in .pdf readme files +the AA battery in the wallclock sends magnetic interference +the xy axis in the trackball is coordinated with the summer solstice +the butane lighter causes the pincushioning +old inkjet cartridges emanate barium-based fumes +manager in the cable duct +Well fix that in the next (upgrade, update, patch release, service pack). +HTTPD Error 666 : BOFH was here +HTTPD Error 4004 : very old Intel cpu - insufficient processing power +The ATM board has run out of 10 pound notes. We are having a whip round to refill it, care to contribute ? +Network failure - call NBC +Having to manually track the satellite. +Your/our computer(s) had suffered a memory leak, and we are waiting for them to be topped up. +The rubber band broke +We're on Token Ring, and it looks like the token got loose. +Stray Alpha Particles from memory packaging caused Hard Memory Error on Server. +paradigm shift...without a clutch +PEBKAC (Problem Exists Between Keyboard And Chair) +The cables are not the same length. +Second-system effect. +Chewing gum on /dev/sd3c +Boredom in the Kernel. +the daemons! the daemons! the terrible daemons! +I'd love to help you -- it's just that the Boss won't let me near the computer. +struck by the Good Times virus +YOU HAVE AN I/O ERROR -> Incompetent Operator error +Your parity check is overdrawn and you're out of cache. +Communist revolutionaries taking over the server room and demanding all the computers in the building or they shoot the sysadmin. Poor misguided fools. +Plasma conduit breach +Out of cards on drive D: +Sand fleas eating the Internet cables +parallel processors running perpendicular today +ATM cell has no roaming feature turned on, notebooks can't connect +Webmasters kidnapped by evil cult. +Failure to adjust for daylight savings time. +Virus transmitted from computer to sysadmins. +Virus due to computers having unsafe sex. +Incorrectly configured static routes on the corerouters. +Forced to support NT servers; sysadmins quit. +Suspicious pointer corrupted virtual machine +It's the InterNIC's fault. +Root name servers corrupted. +Budget cuts forced us to sell all the power cords for the servers. +Someone hooked the twisted pair wires into the answering machine. +Operators killed by year 2000 bug bite. +We've picked COBOL as the language of choice. +Operators killed when huge stack of backup tapes fell over. +Robotic tape changer mistook operator's tie for a backup tape. +Someone was smoking in the computer room and set off the halon systems. +Your processor has taken a ride to Heaven's Gate on the UFO behind Hale-Bopp's comet. +it's an ID-10-T error +Dyslexics retyping hosts file on servers +The Internet is being scanned for viruses. +Your computer's union contract is set to expire at midnight. +Bad user karma. +/dev/clue was linked to /dev/null +Increased sunspot activity. +We already sent around a notice about that. +It's union rules. There's nothing we can do about it. Sorry. +Interference from the Van Allen Belt. +Jupiter is aligned with Mars. +Redundant ACLs. +Mail server hit by UniSpammer. +T-1's congested due to porn traffic to the news server. +Data for intranet got routed through the extranet and landed on the internet. +We are a 100% Microsoft Shop. +We are Microsoft. What you are experiencing is not a problem; it is an undocumented feature. +Sales staff sold a product we don't offer. +Secretary sent chain letter to all 5000 employees. +Sysadmin didn't hear pager go off due to loud music from bar-room speakers. +Sysadmin accidentally destroyed pager with a large hammer. +Sysadmins unavailable because they are in a meeting talking about why they are unavailable so much. +Bad cafeteria food landed all the sysadmins in the hospital. +Route flapping at the NAP. +Computers under water due to SYN flooding. +The vulcan-death-grip ping has been applied. +Electrical conduits in machine room are melting. +Traffic jam on the Information Superhighway. +Radial Telemetry Infiltration +Cow-tippers tipped a cow onto the server. +tachyon emissions overloading the system +Maintenance window broken +We're out of slots on the server +Computer room being moved. Our systems are down for the weekend. +Sysadmins busy fighting SPAM. +Repeated reboots of the system failed to solve problem +Feature was not beta tested +Domain controller not responding +Someone else stole your IP address, call the Internet detectives! +It's not RFC-822 compliant. +operation failed because: there is no message for this error (#1014) +stop bit received +internet is needed to catch the etherbunny +network down, IP packets delivered via UPS +Firmware update in the coffee machine +Temporal anomaly +Mouse has out-of-cheese-error +Borg implants are failing +Borg nanites have infested the server +error: one bad user found in front of screen +Please state the nature of the technical emergency +Internet shut down due to maintenance +Daemon escaped from pentagram +crop circles in the corn shell +sticky bit has come loose +Hot Java has gone cold +Cache miss - please take better aim next time +Hash table has woodworm +Trojan horse ran out of hay +Zombie processes detected, machine is haunted. +overflow error in /dev/null +Browser's cookie is corrupted -- someone's been nibbling on it. +Mailer-daemon is busy burning your message in hell. +According to Microsoft, it's by design +vi needs to be upgraded to vii +greenpeace free'd the mallocs +Terrorists crashed an airplane into the server room, have to remove /bin/laden. (rm -rf /bin/laden) +astropneumatic oscillations in the water-cooling +Somebody ran the operating system through a spelling checker. +Spider infestation in warm case parts diff --git a/Mess/config.py b/Mess/config.py new file mode 100644 index 0000000..50bf2a9 --- /dev/null +++ b/Mess/config.py @@ -0,0 +1,16 @@ +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Mess', True) + +Mess = conf.registerPlugin('Mess') +conf.registerChannelValue(conf.supybot.plugins.Mess, 'enabled', + registry.Boolean(False,"""Enable all mess that Ubugtu can spit out in the + channel""")) +conf.registerChannelValue(conf.supybot.plugins.Mess, 'offensive', + registry.Boolean(False,"""Enable all possibly offensive mess that Ubugtu can spit out in the + channel""")) +conf.registerChannelValue(conf.supybot.plugins.Mess, 'delay', + registry.Integer(10,""" Minimum number of seconds between mess """)) diff --git a/Mess/plugin.py b/Mess/plugin.py new file mode 100644 index 0000000..4fa1379 --- /dev/null +++ b/Mess/plugin.py @@ -0,0 +1,243 @@ +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +import random, re, time, commands, urllib2 +import supybot.ircmsgs as ircmsgs + +_bofhfile = '/home/dennis/ubugtu/plugins/Mess/bofh.txt' +_bofhdata = open(bofhfile).readlines() + +class Mess(callbacks.PluginRegexp): + """Random Mess plugin""" + threaded = True + regexps = ['hugme'] + hugs = ["hugs %s","gives %s a big hug","gives %s a sloppy wet kiss", + "huggles %s","squeezes %s","humps %s"] + regex = re.compile('

.*?

(.*?)

', re.DOTALL) + entre = re.compile('&(\S*?);') + jre1 = ('http://www.jackbauerfacts.com/index.php?rate_twenty_four', + re.compile('current-rating.*?width.*?(.*?)', re.DOTALL)) + jre2 = ('http://www.notrly.com/jackbauer/', + re.compile('

(.*?)

')) + badwords = ['sex','masturbate','fuck','rape','dick','pussy','prostitute','hooker', + 'orgasm','sperm','cunt','penis','shit','piss','urin','bitch','semen'] + i = 0 + time = {} + + # WARNING: depends on an alteration in supybot/callbacks.py - don't do + # str(s) if s is unicode! + def dice(self, irc, msg, args, count): + if not self.ok(msg.args[0]): return + if not count: count = 1 + if count > 5: count = 5 + t = u' '.join([x.__call__([u"\u2680",u"\u2681",u"\u2682",u"\u2683",u"\u2684",u"\u2685"]) for x in [random.choice]*count]) + print t + #print str(t) + irc.reply(t) + dice = wrap(dice, [additional('int')]) + + def hugme(self, irc, msg, match): + r""".*hug.*ubugtu""" + irc.queueMsg(ircmsgs.action(msg.args[0], self.hugs[random.randint(0,len(self.hugs)-1)] % msg.nick)) + + + def ok(self, channel, offensive = False): + if not channel.startswith('#'): + delay = 5 + else: + if not self.registryValue('enabled', channel): + return False + if offensive and not self.registryValue('offensive', channel): + return False + delay = self.registryValue('delay', channel) + if channel not in self.time.keys(): + self.time[channel] = time.time() + return True + if self.time[channel] < time.time() - delay: + self.time[channel] = time.time() + return True + return False + + def fact(self,who,count=0): + # The website is buggy, mysql errors rear their ugly head a lot. So we + # retry up to 5 times :) + if count > 5: + return + try: + fact = utils.web.getUrl('http://4q.cc/index.php?pid=fact&person=%s' % who) + reo = self.regex.search(fact) + val = reo.group(1) + while self.entre.search(val): + entity = self.entre.search(val).group(1) + if entity in entities: + val = self.entre.sub(entities[entity], val) + else: + val = self.entre.sub('?', val) + _val = val.lower() + for word in self.badwords: + if word in _val: + raise RuntimeError + return val + except: + time.sleep(1) + return self.fact(who,count+1) + + + def t(self, irc, msg, args): + """ Display a mr T. fact """ + if not self.ok(msg.args[0]): return + f = self.fact('mrt') + if f: irc.reply(f) + t = wrap(t) + + def chuck(self, irc, msg, args): + """ Display a Chuck Norris fact """ + if not self.ok(msg.args[0]): return + f = self.fact('chuck') + if f: irc.reply(f) + chuck = wrap(chuck) + + def vin(self, irc, msg, args): + """ Display a Vin Diesel fact """ + if not self.ok(msg.args[0]): return + f = self.fact('vin') + if f: irc.reply(f) + vin = wrap(vin) + + hre = re.compile('(.*?)',re.DOTALL) + hre2 = re.compile('<.*?>') + def hamster(self, irc, msg, args): + """ Bob sez! """ + if not self.ok(msg.args[0]): return + try: + data = utils.web.getUrl("http://hamsterrepublic.com/dyn/bobsez") + except: + return + # Find correct data + data = self.hre.search(data).group(1) + data = self.hre2.sub('',data) + irc.reply(data.strip()) + hamster = wrap(hamster) + + def fortune(self, irc, msg, args): + """ Display a fortune cookie """ + if not self.ok(msg.args[0]): return + f = commands.getoutput('fortune -s') + f.replace('\t',' ') + f = f.split('\n') + for l in f: + if l: + irc.reply(l) + fortune = wrap(fortune) + def ofortune(self, irc, msg, args): + """ Display a possibly offensive fortune cookie """ + if not self.ok(msg.args[0], True): return + f = commands.getoutput('fortune -so') + f.replace('\t',' ') + f = f.split('\n') + for l in f: + if l: + irc.reply(l) + ofortune = wrap(ofortune) + + #def bash(self, irc, msg, args): + # """ Display a bash.org quote """ + # if not self.ok(msg.args[0], True): return + # b = utils.web.getUrl('http://bash.org?random1') + # r = [] + # infirst = False + # for line in b.split('\n'): + # if '#' in line and 'X' in line: + # if infirst: + # if len(r) < 6: + # bw = False + # for w in self.badwords: + # if w in ''.join(r): + # bw = True + # break + # if not bw: + # for l in r: + # if l: + # irc.reply(l) + # return + # r = [] + # infirst = True + # elif infirst: + # r.append(line.strip()) + # irc.reply('hmm, weird') + #bash = wrap(bash) + + def bofh(self, irc, msg, args, num): + """ Display a BOFH excuse """ + if not self.ok(msg.args[0]): return + if num and num >= 1 and num <= len(_bofhdata): + i = num + else: + i = random.randint(0,len(_bofhdata)-1) + irc.reply("BOFH excuse #%d: %s" % (i, _bofhdata[i])) + bofh = wrap(bofh, [additional('int')]) + + def bauer(self, irc, msg, args, count=0): + """ Display a Jack Bauer fact """ + if not self.ok(msg.args[0]): return + f = self._bauer() + if f: + irc.reply(f) + bauer = wrap(bauer) + + def futurama(self, irc, msg, args): + """ Display a futurama quote """ + if not self.ok(msg.args[0]): return + u = urllib2.urlopen('http://slashdot.org') + h = [x for x in u.headers.headers if x.startswith('X') and not x.startswith('X-Powered-By')][0] + irc.reply(h[2:-2]) + futurama = wrap(futurama) + + def yourmom(self, irc, msg, args): + """ Your mom hates IRC """ + if not self.ok(msg.args[0], True): return + data = utils.web.getUrl('http://pfa.php1h.com/') + irc.reply(data[data.find('

')+3:data.find('

')].strip()) + yourmom = wrap(yourmom) + + def bush(self, irc,msg,args): + """Yes, bush needs help....""" + if not self.ok(msg.args[0], True): return + data = utils.web.getUrl('http://www.dubyaspeak.com/random.phtml') + data = data[data.find('')+1:] + irc.reply(data.replace("\n",'')) + bush = wrap(bush) + + def _bauer(self,count=0): +# if self.i % 2 == 0: +# (url, re) = self.jre1 +# else: +# (url, re) = self.jre2 +# self.i += 1 + (url, re) = self.jre2 + if count > 5: + return + try: + fact = utils.web.getUrl(url) + reo = re.search(fact) + val = reo.group(1) + while self.entre.search(val): + entity = self.entre.search(val).group(1) + if entity in entities: + val = self.entre.sub(entities[entity], val) + else: + val = self.entre.sub('?', val) + _val = val.lower() + for word in self.badwords: + if word in _val: + raise RuntimeError + return val + except: + time.sleep(1) + return self._bauer(count+1) + +Class = Mess diff --git a/Mess/test.py b/Mess/test.py new file mode 100644 index 0000000..3ae8a5f --- /dev/null +++ b/Mess/test.py @@ -0,0 +1,4 @@ +from supybot.test import * + +class MessTestCase(PluginTestCase): + plugins = ('Mess',) diff --git a/Webcal/README.txt b/Webcal/README.txt new file mode 100644 index 0000000..754296c --- /dev/null +++ b/Webcal/README.txt @@ -0,0 +1,5 @@ +This plugin can update a topic given an iCal schedule. It's made to work for +#ubuntu-meeting on Freenode (and iCal from fridge.ubuntu.com) but in theory it +should work with other iCal feeds too. It's not hard to understand. + +Requirements: included ical module and pytz diff --git a/Webcal/__init__.py b/Webcal/__init__.py new file mode 100644 index 0000000..5b87a16 --- /dev/null +++ b/Webcal/__init__.py @@ -0,0 +1,34 @@ +### +# Copyright (c) 2005,2006 Dennis Kaarsemaker +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +### + +""" +Update the topic according to an iCal schedule +""" + +import supybot +import supybot.world as world +__version__ = "0.2" +__author__ = supybot.Author("Dennis Kaarsemaker","Seveas","dennis@kaarsemaker.net") +__contributors__ = {} +__url__ = 'http://bugbot.ubuntulinux.nl/' + +import config +reload(config) +import plugin +reload(plugin) + +if world.testing: + import test +Class = plugin.Class +configure = config.configure diff --git a/Webcal/config.py b/Webcal/config.py new file mode 100644 index 0000000..7747305 --- /dev/null +++ b/Webcal/config.py @@ -0,0 +1,28 @@ +### +# Copyright (c) 2005,2006 Dennis Kaarsemaker +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Webcal', True) + +Webcal = conf.registerPlugin('Webcal') +conf.registerChannelValue(conf.supybot.plugins.Webcal, 'url', + registry.String('',"""Webcal URL for the channel""")) +conf.registerChannelValue(conf.supybot.plugins.Webcal, 'topic', + registry.String('',"""Topic template""")) +conf.registerGlobalValue(conf.supybot.plugins.Webcal, 'defaultChannel', + registry.String('',"""Default channel for /msg replies""")) diff --git a/Webcal/ical.py b/Webcal/ical.py new file mode 100644 index 0000000..89c7f06 --- /dev/null +++ b/Webcal/ical.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# Slightly modified version of the iCal module found at +# http://www.devoesquared.com/Software/iCal_Module + +import os +import os.path +import re +import datetime +import time +import pytz # pytz can be found on http://pytz.sourceforge.net + +SECONDS_PER_DAY=24*60*60 + +class ICalReader: + + def __init__(self, data): + self.events = [] + self.raw_data = data + self.readEvents() + + def readEvents(self): + self.events = [] + lines = self.raw_data.split('\n') + inEvent = False + eventLines = [] + stRegex = re.compile("^BEGIN:VEVENT") + enRegex = re.compile("^END:VEVENT") + for line in lines: + if stRegex.match(line): + inEvent = True + eventLines = [] + if inEvent: + eventLines.append(line) + if enRegex.match(line): + self.events.append(self.parseEvent(eventLines)) + + return self.events + + def parseEvent(self, lines): + event = ICalEvent() + event.raw_data = "\n".join(lines) + startDate = None + rule = None + endDate = None + + for line in lines: + if re.compile("^SUMMARY:(.*)").match(line): + event.summary = re.compile("^SUMMARY:(.*)").match(line).group(1) + elif re.compile("^DTSTART;.*:(.*).*").match(line): + startDate = self.parseDate(re.compile("^DTSTART;.*:(.*).*").match(line).group(1)) + elif re.compile("^DTEND;.*:(.*).*").match(line): + endDate = self.parseDate(re.compile("^DTEND;.*:(.*).*").match(line).group(1)) + elif re.compile("^EXDATE.*:(.*)").match(line): + event.addExceptionDate(parseDate(re.compile("^EXDATE.*:(.*)").match(line).group(1))) + elif re.compile("^RRULE:(.*)").match(line): + rule = re.compile("^RRULE:(.*)").match(line).group(1) + + event.startDate = startDate + event.endDate = endDate + if rule: + event.addRecurrenceRule(rule) + return event + + def parseDate(self, dateStr): + year = int(dateStr[0:4]) + if year < 1970: + year = 1970 + + month = int(dateStr[4:4+2]) + day = int(dateStr[6:6+2]) + try: + hour = int(dateStr[9:9+2]) + minute = int(dateStr[11:11+2]) + except: + hour = 0 + minute = 0 + + return datetime.datetime(year, month, day, hour, minute, tzinfo=pytz.UTC) + + def selectEvents(self, selectFunction): + note = datetime.datetime.today() + self.events.sort() + events = filter(selectFunction, self.events) + return events + + def todaysEvents(self, event): + return event.startsToday() + + def tomorrowsEvents(self, event): + return event.startsTomorrow() + + def eventsFor(self, date): + note = datetime.datetime.today() + self.events.sort() + ret = [] + for event in self.events: + if event.startsOn(date): + ret.append(event) + return ret + + +class ICalEvent: + def __init__(self): + self.exceptionDates = [] + self.dateSet = None + + def __str__(self): + return self.summary + + def __eq__(self, otherEvent): + return self.startDate == otherEvent.startDate + + def addExceptionDate(self, date): + self.exceptionDates.append(date) + + def addRecurrenceRule(self, rule): + self.dateSet = DateSet(self.startDate, self.endDate, rule) + + def startsToday(self): + return self.startsOn(datetime.datetime.today()) + + def startsTomorrow(self): + tomorrow = datetime.datetime.fromtimestamp(time.time() + SECONDS_PER_DAY) + return self.startsOn(tomorrow) + + def startsOn(self, date): + return (self.startDate.year == date.year and + self.startDate.month == date.month and + self.startDate.day == date.day or + (self.dateSet and self.dateSet.includes(date))) + + def startTime(self): + return self.startDate + +class DateSet: + def __init__(self, startDate, endDate, rule): + self.startDate = startDate + self.endDate = endDate + self.frequency = None + self.count = None + self.untilDate = None + self.byMonth = None + self.byDate = None + self.parseRecurrenceRule(rule) + + def parseRecurrenceRule(self, rule): + if re.compile("FREQ=(.*?);").match(rule) : + self.frequency = re.compile("FREQ=(.*?);").match(rule).group(1) + + if re.compile("COUNT=(\d*)").match(rule) : + self.count = int(re.compile("COUNT=(\d*)").match(rule).group(1)) + + if re.compile("UNTIL=(.*?);").match(rule) : + self.untilDate = DateParser.parse(re.compile("UNTIL=(.*?);").match(rule).group(1)) + + if re.compile("INTERVAL=(\d*)").match(rule) : + self.interval = int(re.compile("INTERVAL=(\d*)").match(rule).group(1)) + + if re.compile("BYMONTH=(.*?);").match(rule) : + self.byMonth = re.compile("BYMONTH=(.*?);").match(rule).group(1) + + if re.compile("BYDAY=(.*?);").match(rule) : + self.byDay = re.compile("BYDAY=(.*?);").match(rule).group(1) + + + def includes(self, date): + if date == self.startDate: + return True + + if self.untilDate and date > self.untilDate: + return False + + if self.frequency == 'DAILY': + increment = 1 + if self.interval: + increment = self.interval + d = self.startDate + counter = 0 + while(d < date): + if self.count: + counter += 1 + if counter >= self.count: + return False + + d = d.replace(day=d.day+1) + + if (d.day == date.day and + d.year == date.year and + d.month == date.month): + return True + + elif self.frequency == 'WEEKLY': + if self.startDate.weekday() == date.weekday(): + return True + else: + if self.endDate: + for n in range(0, self.endDate.day - self.startDate.day): + newDate = self.startDate.replace(day=self.startDate.day+n) + if newDate.weekday() == date.weekday(): + return True + + elif self.frequency == 'MONTHLY': + pass + + elif self.frequency == 'YEARLY': + pass + + return False + diff --git a/Webcal/plugin.py b/Webcal/plugin.py new file mode 100644 index 0000000..d08fd4a --- /dev/null +++ b/Webcal/plugin.py @@ -0,0 +1,213 @@ +### +# Copyright (c) 2005,2006 Dennis Kaarsemaker +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +### + +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks +import supybot.schedule as schedule +import supybot.ircmsgs as ircmsgs +import pytz +import ical +import datetime +reload(ical) + +def _event_to_string(event, timezone): + if not timezone: + return "%s UTC: %s" % (event.startDate.strftime("%d %b %H:%M"), event.summary) + return "%s: %s" % (event.startDate.astimezone(pytz.timezone(timezone)).strftime("%d %b %H:%M"), event.summary) + +def diff(delta): + s = '' + if delta.days: + if delta.days > 1: + s = 's' + return '%d day%s' % (delta.days, s) + h = '' + if delta.seconds > 7200: + s = 's' + if delta.seconds > 3600: + h = '%d hour%s ' % (int(delta.seconds/3600),s) + s = '' + seconds = delta.seconds % 3600 + if seconds > 120: + s = 's' + return '%s%d minute%s' % (h,(seconds/60),s) + +class Webcal(callbacks.Plugin): + """@schedule : display the schedule in your timezone""" + threaded = True + + def __init__(self, irc): + callbacks.Privmsg.__init__(self, irc) + self.irc = irc + schedule.addPeriodicEvent(self._refresh_cache, 60 * 20, name=self.name()) + schedule.addPeriodicEvent(self._autotopics, 60, name=self.name() + 'b') + self.cache = {} + + def die(self): + schedule.removeEvent(self.name()) + schedule.removeEvent(self.name() + 'b') + self.cache.clear() + + def reset(self): + self.cache.clear() + + def _filter(self, event, channel, now): + #channel = '#ubuntu-meeting' # Testing hack + if channel.lower() not in event.raw_data.lower(): + return False + delta = event.endDate - now + return delta.days >= 0 or (delta.days == -1 and abs(delta).seconds < 30 * 60) + + def _gettopic(self, url, channel, do_update=False, only_update=False, timezone = None, no_topic=False): + if do_update or url not in self.cache.keys(): + data = utils.web.getUrl(url) + parser = ical.ICalReader(data) + #parser = ical.ICalReader(ical.sample) + self.cache[url] = parser.events + if not only_update: + now = datetime.datetime.now(pytz.UTC) + events = filter(lambda x: self._filter(x,channel,now),self.cache[url])[:6] + preamble = '' + if len(events): + # The standard slack of 30 minutes after the meeting will be an + # error if there are 2 conscutive meetings. + if len(events) > 1 and events[1].startDate < now: + events = events[1:] + ev0 = events[0] + delta = abs(ev0.startDate - now) + if ev0.startDate < now or (delta.days == 0 and delta.seconds < 10 * 60): + preamble = 'Current meeting: %s | ' % ev0.summary.replace('Meeting','').strip() + events = events[1:] + events = map(lambda x: _event_to_string(x,timezone), events) + template = self.registryValue('topic', channel) + newtopic = ' | '.join(events).replace(' Meeting','') + if '%s' in template and not no_topic: + newtopic = template % str(newtopic) + return preamble + newtopic + + def _nextmeeting(self, url, channel, timezone): + if url not in self.cache.keys(): + data = utils.web.getUrl(url) + parser = ical.ICalReader(data) + #parser = ical.ICalReader(ical.sample) + self.cache[url] = parser.events + now = datetime.datetime.now(pytz.UTC) + events = filter(lambda x: self._filter(x,channel,now),self.cache[url])[:6] + preamble = '' + if len(events): + # The standard slack of 30 minutes after the meeting will be an + # error if there are 2 conscutive meetings. + if len(events) > 1 and events[1].startDate < now: + events = events[1:] + ev0 = events[0] + delta = abs(ev0.startDate - now) + if ev0.startDate < now or (delta.days == 0 and delta.seconds < 10 * 60): + return ' - Current meeting: %s ' % ev0.summary.replace('Meeting','').strip() + return ' - Next meeting: %s in %s' % (ev0.summary.replace('Meeting',''), diff(delta)) + return '' + + def _autotopics(self): + if not self.irc: + return + for c in self.irc.state.channels: + url = self.registryValue('url', c) + if url: + newtopic = self._gettopic(url, c) + if newtopic and not (newtopic.strip() == self.irc.state.getTopic(c).strip()): + self.irc.queueMsg(ircmsgs.topic(c, newtopic)) + + def _refresh_cache(self): + if not self.lastIrc: + return + for c in self.lastIrc.state.channels: + url = self.registryValue('url', c) + if url: + self._gettopic(url, c, True, true) + + def topic(self, irc, msg, args): + url = self.registryValue('url', msg.args[0]) + if not url: + return + newtopic = self._gettopic(url, msg.args[0], True) + if not (newtopic.strip() == irc.state.getTopic(msg.args[0]).strip()): + irc.queueMsg(ircmsgs.topic(msg.args[0], newtopic)) + topic = wrap(topic) + + def _tzfilter(self, tz, ud): + if tz == ud: + return True + pos = tz.find('/') + while not (pos == -1): + if tz[pos+1:] == ud: + return True + pos = tz.find('/',pos+1) + return False + + def schedule(self, irc, msg, args, tz): + """ Retrieve the date/time of scheduled meetings in a specific timezone """ + if not tz: + tz = 'utc' + if irc.isChannel(msg.args[0]): + c = msg.args[0] + else: + c = self.registryValue('defaultChannel') + if not c: + return + url = self.registryValue('url', c) + if not url: + return + tzs = filter(lambda x: self._tzfilter(x.lower(),tz.lower()), pytz.all_timezones) + if not tzs or 'gmt' in tz.lower(): + irc.error('Unknown timezone: %s - Full list: http://bugbot.ubuntulinux.nl/timezones.html' % tz) + else: + irc.reply('Schedule for %s: %s' % (tzs[0],self._gettopic(url, c, timezone=tzs[0], no_topic=True))) + schedule = wrap(schedule, [additional('text')]) + + def now(self, irc, msg, args, tz): + """ Display the current time """ + now = datetime.datetime.now(pytz.UTC) + if not tz: + tz = 'utc' + #irc.reply('Current time in UTC: %s' % now.strftime("%B %d %Y, %H:%M:%S")) + tzs = filter(lambda x: self._tzfilter(x.lower(),tz.lower()), pytz.all_timezones) + if not tzs or 'gmt' in tz.lower(): + irc.error('Unknown timezone: %s - Full list: http://bugbot.ubuntulinux.nl/timezones.html' % tz) + else: + if irc.isChannel(msg.args[0]): + c = msg.args[0] + else: + c = self.registryValue('defaultChannel') + if not c: + return + #c = "#ubuntu-meeting" + url = self.registryValue('url', c) + print c, url + if not url: + meeting = '' + else: + meeting = self._nextmeeting(url, c, tzs[0]) + irc.reply('Current time in %s: %s%s' % (tzs[0],now.astimezone(pytz.timezone(tzs[0])).strftime("%B %d %Y, %H:%M:%S"),meeting)) + now = wrap(now, [additional('text')]) + time = now + + def doTopic(self, irc, msg): + url = self.registryValue('url', msg.args[0]) + if not url: + return + irc.queueMsg(ircmsgs.privmsg(msg.nick, "The topic of %s is managed by me and filled with the contents of %s - please don't change manually" % (msg.args[0],url))) + +Class = Webcal diff --git a/Webcal/test.py b/Webcal/test.py new file mode 100644 index 0000000..3dc9015 --- /dev/null +++ b/Webcal/test.py @@ -0,0 +1,3 @@ +from supybot.test import * +class WebcalTestCase(PluginTestCase): + plugins = ('Webcal',) diff --git a/Webcal/timezones.html b/Webcal/timezones.html new file mode 100644 index 0000000..e9deb8a --- /dev/null +++ b/Webcal/timezones.html @@ -0,0 +1,592 @@ + + + Ubugtu and Ubotu + + + +
+

Timezones Hi!

+

+ These are all timezones known by Ubugtu. You don't need to use the full + zone name, the last part is enough. When this is ambiguous, the + alphabetic first timezone will be used. Timezones are not case sensitive +

+

+

+ Examples:
+
+ For Europe/Amsterdam:
+ @schedule amsterdam
+ For America/North_Dakota/Center:
+ @schedule North_Dakota/Center
+

+

+

All timezones

+

+ Africa/Abidjan
+ Africa/Accra
+ Africa/Addis_Ababa
+ Africa/Algiers
+ Africa/Asmera
+ Africa/Bamako
+ Africa/Bangui
+ Africa/Banjul
+ Africa/Bissau
+ Africa/Blantyre
+ Africa/Brazzaville
+ Africa/Bujumbura
+ Africa/Cairo
+ Africa/Casablanca
+ Africa/Ceuta
+ Africa/Conakry
+ Africa/Dakar
+ Africa/Dar_es_Salaam
+ Africa/Djibouti
+ Africa/Douala
+ Africa/El_Aaiun
+ Africa/Freetown
+ Africa/Gaborone
+ Africa/Harare
+ Africa/Johannesburg
+ Africa/Kampala
+ Africa/Khartoum
+ Africa/Kigali
+ Africa/Kinshasa
+ Africa/Lagos
+ Africa/Libreville
+ Africa/Lome
+ Africa/Luanda
+ Africa/Lubumbashi
+ Africa/Lusaka
+ Africa/Malabo
+ Africa/Maputo
+ Africa/Maseru
+ Africa/Mbabane
+ Africa/Mogadishu
+ Africa/Monrovia
+ Africa/Nairobi
+ Africa/Ndjamena
+ Africa/Niamey
+ Africa/Nouakchott
+ Africa/Ouagadougou
+ Africa/Porto-Novo
+ Africa/Sao_Tome
+ Africa/Timbuktu
+ Africa/Tripoli
+ Africa/Tunis
+ Africa/Windhoek +

+

+

+ America/Adak
+ America/Anchorage
+ America/Anguilla
+ America/Antigua
+ America/Araguaina
+ America/Argentina/Buenos_Aires
+ America/Argentina/Catamarca
+ America/Argentina/ComodRivadavia
+ America/Argentina/Cordoba
+ America/Argentina/Jujuy
+ America/Argentina/La_Rioja
+ America/Argentina/Mendoza
+ America/Argentina/Rio_Gallegos
+ America/Argentina/San_Juan
+ America/Argentina/Tucuman
+ America/Argentina/Ushuaia
+ America/Aruba
+ America/Asuncion
+ America/Atka
+ America/Bahia
+ America/Barbados
+ America/Belem
+ America/Belize
+ America/Boa_Vista
+ America/Bogota
+ America/Boise
+ America/Buenos_Aires
+ America/Cambridge_Bay
+ America/Campo_Grande
+ America/Cancun
+ America/Caracas
+ America/Catamarca
+ America/Cayenne
+ America/Cayman
+ America/Chicago
+ America/Chihuahua
+ America/Coral_Harbour
+ America/Cordoba
+ America/Costa_Rica
+ America/Cuiaba
+ America/Curacao
+ America/Danmarkshavn
+ America/Dawson
+ America/Dawson_Creek
+ America/Denver
+ America/Detroit
+ America/Dominica
+ America/Edmonton
+ America/Eirunepe
+ America/El_Salvador
+ America/Ensenada
+ America/Fort_Wayne
+ America/Fortaleza
+ America/Glace_Bay
+ America/Godthab
+ America/Goose_Bay
+ America/Grand_Turk
+ America/Grenada
+ America/Guadeloupe
+ America/Guatemala
+ America/Guayaquil
+ America/Guyana
+ America/Halifax
+ America/Havana
+ America/Hermosillo
+ America/Indiana/Indianapolis
+ America/Indiana/Knox
+ America/Indiana/Marengo
+ America/Indiana/Vevay
+ America/Indianapolis
+ America/Inuvik
+ America/Iqaluit
+ America/Jamaica
+ America/Jujuy
+ America/Juneau
+ America/Kentucky/Louisville
+ America/Kentucky/Monticello
+ America/Knox_IN
+ America/La_Paz
+ America/Lima
+ America/Los_Angeles
+ America/Louisville
+ America/Maceio
+ America/Managua
+ America/Manaus
+ America/Martinique
+ America/Mazatlan
+ America/Mendoza
+ America/Menominee
+ America/Merida
+ America/Mexico_City
+ America/Miquelon
+ America/Monterrey
+ America/Montevideo
+ America/Montreal
+ America/Montserrat
+ America/Nassau
+ America/New_York
+ America/Nipigon
+ America/Nome
+ America/Noronha
+ America/North_Dakota/Center
+ America/Panama
+ America/Pangnirtung
+ America/Paramaribo
+ America/Phoenix
+ America/Port-au-Prince
+ America/Port_of_Spain
+ America/Porto_Acre
+ America/Porto_Velho
+ America/Puerto_Rico
+ America/Rainy_River
+ America/Rankin_Inlet
+ America/Recife
+ America/Regina
+ America/Rio_Branco
+ America/Rosario
+ America/Santiago
+ America/Santo_Domingo
+ America/Sao_Paulo
+ America/Scoresbysund
+ America/Shiprock
+ America/St_Johns
+ America/St_Kitts
+ America/St_Lucia
+ America/St_Thomas
+ America/St_Vincent
+ America/Swift_Current
+ America/Tegucigalpa
+ America/Thule
+ America/Thunder_Bay
+ America/Tijuana
+ America/Toronto
+ America/Tortola
+ America/Vancouver
+ America/Virgin
+ America/Whitehorse
+ America/Winnipeg
+ America/Yakutat
+ America/Yellowknife +

+

+

+ Antarctica/Casey
+ Antarctica/Davis
+ Antarctica/DumontDUrville
+ Antarctica/Mawson
+ Antarctica/McMurdo
+ Antarctica/Palmer
+ Antarctica/Rothera
+ Antarctica/South_Pole
+ Antarctica/Syowa
+ Antarctica/Vostok +

+

+

+ Arctic/Longyearbyen +

+

+

+ Asia/Aden
+ Asia/Almaty
+ Asia/Amman
+ Asia/Anadyr
+ Asia/Aqtau
+ Asia/Aqtobe
+ Asia/Ashgabat
+ Asia/Ashkhabad
+ Asia/Baghdad
+ Asia/Bahrain
+ Asia/Baku
+ Asia/Bangkok
+ Asia/Beirut
+ Asia/Bishkek
+ Asia/Brunei
+ Asia/Calcutta
+ Asia/Choibalsan
+ Asia/Chongqing
+ Asia/Chungking
+ Asia/Colombo
+ Asia/Dacca
+ Asia/Damascus
+ Asia/Dhaka
+ Asia/Dili
+ Asia/Dubai
+ Asia/Dushanbe
+ Asia/Gaza
+ Asia/Harbin
+ Asia/Hong_Kong
+ Asia/Hovd
+ Asia/Irkutsk
+ Asia/Istanbul
+ Asia/Jakarta
+ Asia/Jayapura
+ Asia/Jerusalem
+ Asia/Kabul
+ Asia/Kamchatka
+ Asia/Karachi
+ Asia/Kashgar
+ Asia/Katmandu
+ Asia/Krasnoyarsk
+ Asia/Kuala_Lumpur
+ Asia/Kuching
+ Asia/Kuwait
+ Asia/Macao
+ Asia/Macau
+ Asia/Magadan
+ Asia/Makassar
+ Asia/Manila
+ Asia/Muscat
+ Asia/Nicosia
+ Asia/Novosibirsk
+ Asia/Omsk
+ Asia/Oral
+ Asia/Phnom_Penh
+ Asia/Pontianak
+ Asia/Pyongyang
+ Asia/Qatar
+ Asia/Qyzylorda
+ Asia/Rangoon
+ Asia/Riyadh
+ Asia/Saigon
+ Asia/Sakhalin
+ Asia/Samarkand
+ Asia/Seoul
+ Asia/Shanghai
+ Asia/Singapore
+ Asia/Taipei
+ Asia/Tashkent
+ Asia/Tbilisi
+ Asia/Tehran
+ Asia/Tel_Aviv
+ Asia/Thimbu
+ Asia/Thimphu
+ Asia/Tokyo
+ Asia/Ujung_Pandang
+ Asia/Ulaanbaatar
+ Asia/Ulan_Bator
+ Asia/Urumqi
+ Asia/Vientiane
+ Asia/Vladivostok
+ Asia/Yakutsk
+ Asia/Yekaterinburg
+ Asia/Yerevan +

+

+

+ Atlantic/Azores
+ Atlantic/Bermuda
+ Atlantic/Canary
+ Atlantic/Cape_Verde
+ Atlantic/Faeroe
+ Atlantic/Jan_Mayen
+ Atlantic/Madeira
+ Atlantic/Reykjavik
+ Atlantic/South_Georgia
+ Atlantic/St_Helena
+ Atlantic/Stanley +

+

+

+ Australia/ACT
+ Australia/Adelaide
+ Australia/Brisbane
+ Australia/Broken_Hill
+ Australia/Canberra
+ Australia/Currie
+ Australia/Darwin
+ Australia/Hobart
+ Australia/LHI
+ Australia/Lindeman
+ Australia/Lord_Howe
+ Australia/Melbourne
+ Australia/NSW
+ Australia/North
+ Australia/Perth
+ Australia/Queensland
+ Australia/South
+ Australia/Sydney
+ Australia/Tasmania
+ Australia/Victoria
+ Australia/West
+ Australia/Yancowinna +

+

+

+ Brazil/Acre
+ Brazil/DeNoronha
+ Brazil/East
+ Brazil/West +

+

+

+ CET
+ CST6CDT +

+

+

+ Canada/Atlantic
+ Canada/Central
+ Canada/East-Saskatchewan
+ Canada/Eastern
+ Canada/Mountain
+ Canada/Newfoundland
+ Canada/Pacific
+ Canada/Saskatchewan
+ Canada/Yukon
+ Chile/Continental
+ Chile/EasterIsland +

+

+

+ Cuba
+ EET
+ EST
+ EST5EDT
+ Egypt
+ Eire +

+

+

+ Etc/Greenwich
+ Etc/UCT
+ Etc/UTC
+ Etc/Universal
+ Etc/Zulu +

+

+ Europe/Amsterdam
+ Europe/Andorra
+ Europe/Athens
+ Europe/Belfast
+ Europe/Belgrade
+ Europe/Berlin
+ Europe/Bratislava
+ Europe/Brussels
+ Europe/Bucharest
+ Europe/Budapest
+ Europe/Chisinau
+ Europe/Copenhagen
+ Europe/Dublin
+ Europe/Gibraltar
+ Europe/Helsinki
+ Europe/Istanbul
+ Europe/Kaliningrad
+ Europe/Kiev
+ Europe/Lisbon
+ Europe/Ljubljana
+ Europe/London
+ Europe/Luxembourg
+ Europe/Madrid
+ Europe/Malta
+ Europe/Mariehamn
+ Europe/Minsk
+ Europe/Monaco
+ Europe/Moscow
+ Europe/Nicosia
+ Europe/Oslo
+ Europe/Paris
+ Europe/Prague
+ Europe/Riga
+ Europe/Rome
+ Europe/Samara
+ Europe/San_Marino
+ Europe/Sarajevo
+ Europe/Simferopol
+ Europe/Skopje
+ Europe/Sofia
+ Europe/Stockholm
+ Europe/Tallinn
+ Europe/Tirane
+ Europe/Tiraspol
+ Europe/Uzhgorod
+ Europe/Vaduz
+ Europe/Vatican
+ Europe/Vienna
+ Europe/Vilnius
+ Europe/Warsaw
+ Europe/Zagreb
+ Europe/Zaporozhye
+ Europe/Zurich +

+

+

+ GB
+ GB-Eire
+ Greenwich
+ HST
+ Hongkong
+ Iceland +

+

+

+ Indian/Antananarivo
+ Indian/Chagos
+ Indian/Christmas
+ Indian/Cocos
+ Indian/Comoro
+ Indian/Kerguelen
+ Indian/Mahe
+ Indian/Maldives
+ Indian/Mauritius
+ Indian/Mayotte
+ Indian/Reunion +

+

+

+ Iran
+ Israel
+ Jamaica
+ Japan
+ Kwajalein
+ Libya
+ MET
+ MST
+ MST7MDT +

+

+

+ Mexico/BajaNorte
+ Mexico/BajaSur
+ Mexico/General +

+

+

+ NZ
+ NZ-CHAT
+ Navajo
+ PRC
+ PST8PDT +

+

+

+ Pacific/Apia
+ Pacific/Auckland
+ Pacific/Chatham
+ Pacific/Easter
+ Pacific/Efate
+ Pacific/Enderbury
+ Pacific/Fakaofo
+ Pacific/Fiji
+ Pacific/Funafuti
+ Pacific/Galapagos
+ Pacific/Gambier
+ Pacific/Guadalcanal
+ Pacific/Guam
+ Pacific/Honolulu
+ Pacific/Johnston
+ Pacific/Kiritimati
+ Pacific/Kosrae
+ Pacific/Kwajalein
+ Pacific/Majuro
+ Pacific/Marquesas
+ Pacific/Midway
+ Pacific/Nauru
+ Pacific/Niue
+ Pacific/Norfolk
+ Pacific/Noumea
+ Pacific/Pago_Pago
+ Pacific/Palau
+ Pacific/Pitcairn
+ Pacific/Ponape
+ Pacific/Port_Moresby
+ Pacific/Rarotonga
+ Pacific/Saipan
+ Pacific/Samoa
+ Pacific/Tahiti
+ Pacific/Tarawa
+ Pacific/Tongatapu
+ Pacific/Truk
+ Pacific/Wake
+ Pacific/Wallis
+ Pacific/Yap +

+

+

+ Poland
+ Portugal
+ ROC
+ ROK
+ Singapore
+ Turkey
+ UCT +

+

+

+ US/Alaska
+ US/Aleutian
+ US/Arizona
+ US/Central
+ US/East-Indiana
+ US/Eastern
+ US/Hawaii
+ US/Indiana-Starke
+ US/Michigan
+ US/Mountain
+ US/Pacific
+ US/Pacific-New
+ US/Samoa +

+

+

+ UTC
+ Universal
+ W-SU
+ WET
+ Zulu
+ posixrules +

+
+ + diff --git a/bot.css b/bot.css new file mode 100644 index 0000000..174c2d0 --- /dev/null +++ b/bot.css @@ -0,0 +1,93 @@ +body { + font-family: verdana, sans; + font-weight: bold; + background-color: #d9bb7a; + color: #980101; + font-size: 10px; +} + +div.home { + margin: 20px auto; + width: 300px; + border: 2px solid #980101; + padding: 0px 10px; + background-color: #fdd99b; + text-align: center; +} +.submit { + font-size: 10px; + border: solid 1px #980101; + color: #980101; + font-weight: bold; + background-color: #fdff99; +} +.input { + font-size: 10px; + border: solid 1px #980101; + color: #980101; + font-weight: bold; + background-color: white; +} +th { + border-bottom: solid 1px #980101; + text-align: center; +} +td,th { + font-size: 10px; + text-align: left; + vertical-align: top; + white-space: nowrap; +} +td.comment { + white-space: normal; + padding-left: 20px; +} +li { + list-style: none; +} +table { + width: 100%%; + padding-top: 1em; + clear: both; +} + +div.main { + margin: 20px; + border: 2px solid #980101; + padding: 0px; + background-color: #fdd99b; + text-align: center; +} +div.pdata { + text-align: right; + padding-right: 10px; + float: right; +} +div.search { + float: left; + text-align: left; + padding-left: 10px; +} +a, span.pseudolink { + color: #d40000; + text-decoration: underline; + cursor: pointer; +} +span.removal { + color: #6699cc; +} +tr.bg2 { + background-color: #fdff99; +} +div.invisible { + display: none; +} +div.log { + display: none; + color: black; + width: 100%%; + font-family: monospace; + white-space: normal; + text-align: left; + font-weight: normal; +} diff --git a/commoncgi.py b/commoncgi.py new file mode 100644 index 0000000..4d24a05 --- /dev/null +++ b/commoncgi.py @@ -0,0 +1,33 @@ +import cgi, cgitb, re, sys, math, os, md5, sqlite, random, time, datetime, pytz, Cookie, StringIO +import cPickle as pickle +cgitb.enable() + +form = cgi.FieldStorage() +cookie = Cookie.SimpleCookie() +if os.environ.has_key('HTTP_COOKIE'): + cookie.load(os.environ['HTTP_COOKIE']) + +if cookie.has_key('sess'): + cookie['sess']['max-age'] = 2592000 + cookie['sess']['version'] = 1 +if cookie.has_key('tz'): + cookie['tz']['max-age'] = 2592000 + cookie['tz']['version'] = 1 +sys.stdout = StringIO.StringIO() + +def send_page(template): + data = sys.stdout.getvalue() + sys.stdout = sys.__stdout__ + print "Content-Type: text/html" + print cookie + print "" + + fd = open(template) + tmpl = fd.read() + fd.close() + print tmpl % data + sys.exit(0) + +def q(txt): + return txt.replace('&','&').replace('<','<').replace('>','>').replace('"','"') +