Commits

Chris Mulligan  committed 62c27c2

Initial public checkin

  • Participants

Comments (0)

Files changed (11)

+Copyright (c) 2010, YouGov Plc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of YouGov Plc.  nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL YOUGOV PLC BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-*- restructuredtext -*-
+
+======
+pmxbot
+======
+
+pmxbot is an IRC bot written in python. Originally built for internal use,
+it's been sanitized and set free upon the world.
+
+
+Commands
+========
+pmxbot listens to commands prefixed by a '!'
+If it's a command it knows it will reply, take an action, etc. 
+It can search the web, quote you, track karma, make decisions,
+and do just about anything else you could want. It logs text in a sqlite3
+database, and eventually we'll write a web interface to it.
+
+Contains
+========
+pmxbot will respond to things you say if it detects words and phrases it's
+been told to recognize. For example, mention sql on rails.
+
+Requirements
+============
+pmxbot requires python, of course. Probably 2.5+. It also requires a few python packages:
+
+* python-irclib
+* pyyaml
+* simplejson or json
+* feedparser
+* httplib2
+* sqlite3
+
+
+Configuration
+=============
+Configuration is based on very easy YAML files. Check out config.yaml in the
+source tree for an example.
+
+Usage
+=====
+Once you've setup a config file, you just need to call ``pmxbot config.yaml``
+and it will join and connect. We recommend running pmxbot under djb's
+daemontools to make it automatically restart if it crashes. 
+
+
+
+Writing a new feature
+=====================
+Adding new features is very easy, you just need to add them to pmxbot.py
+following the established convention. A command gets the @command deocator::
+
+  @command("tinytear", aliases=('tt', 'tear', 'cry'), doc="I cry a tiny tear for you.")
+  def tinytear(client, event, channel, nick, rest):
+  	if rest:
+  		return "/me sheds a single tear for %s" % rest
+  	else:
+  		return "/me sits and cries as a single tear slowly trickles down its cheek"
+
+A response uses the @contains decorator::
+
+  @contains("sqlonrails")
+  def yay_sor(client, event, channel, nick, rest):
+  	karmaChange(botbase.logger.db, 'sql on rails', 1)
+  	return "Only 76,417 lines..."
+
+server_host: "irc.freenode.net"
+server_port: 6667
+bot_nickname: pmxbot
+log_channels: 
+    - "#botone"
+other_channels:
+    - "#bottwo"
+inane_channel: "#botone"
+database_dir: "./bot_database"
+places: ["Palo Alto, CA", "Canton, OH", "Washington, DC", "London, England"]
+lunch_choices:
+    PA: ["Pasta?", "Thaiphoon", "Pluto's", "Penninsula Creamery", "Kan Zeman"]
+feed_interval: 15 #15 minutes
+feeds:
+    - name    : "pmxbot bitbucket"
+      channel : "#botone"
+      linkurl : "http://bitbucket.org/yougov/pmxbot"
+      url     : "http://bitbucket.org/yougov/pmxbot"
+    - name    : "pmxbot bitbucket"
+      channel : "#bottwo"
+      linkurl : "http://bitbucket.org/yougov"
+      url     : "http://bitbucket.org/yougov/rss"

File pmxbot/__init__.py

Empty file added.

File pmxbot/botbase.py

+# vim:ts=4:sw=4:noexpandtab
+
+import sys
+import ircbot
+import datetime
+from sqlite3 import dbapi2 as sqlite
+import os
+import traceback
+import time
+import re
+import feedparser
+import socket
+
+exists = os.path.exists
+pjoin = os.path.join
+
+LOGWARN_EVERY = 60 # seconds
+LOGWARN_MESSAGE = \
+'''PRIVACY INFORMATION: LOGGING IS ENABLED!!
+  
+The following channels are logged are being logged to provide a 
+convenient, searchable archive of conversation histories:
+%s
+'''
+warn_history = {}
+
+class NoLog(object): pass
+
+class LoggingCommandBot(ircbot.SingleServerIRCBot):
+	def __init__(self, repo, server, port, nickname, channels, nolog_channels=None, feed_interval=60, feeds=[]):
+		ircbot.SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname)
+		nolog_channels = nolog_channels or []
+		self.nickname = nickname
+		self._channels = channels + nolog_channels
+		self._nolog = set(('#' + c if not c.startswith('#') else c) for c in nolog_channels)
+		setup_repo(repo)
+		self._repo = repo
+		self._nickname = nickname
+        self._feed_interval = feed_interval
+        self._feeds = feeds
+
+	def on_welcome(self, c, e):
+		for channel in self._channels:
+			if not channel.startswith('#'):
+				channel = '#' + channel
+			c.join(channel)
+		c.execute_delayed(30, self.feed_parse, arguments=(c, e, self._feed_interval, self._feeds))
+
+	def on_join(self, c, e):
+		nick = e.source().split('!', 1)[0]
+		channel = e.target()
+		if channel in self._nolog or nick == self._nickname:
+			return
+		if time.time() - warn_history.get(nick, 0) > LOGWARN_EVERY:
+			warn_history[nick] = time.time()
+			msg = LOGWARN_MESSAGE % (', '.join(['#'+chan for chan in self._channels if '#'+chan not in self._nolog]))
+			for line in msg.splitlines():
+				c.notice(nick, line)
+	
+	def on_pubmsg(self, c, e):
+		msg = ''.join(e.arguments())
+		nick = e.source().split('!', 1)[0]
+		channel = e.target()
+		if msg == '':
+			pass
+		else:
+			if channel not in self._nolog:
+				logger.message(channel, nick, msg)
+			self.handle_action(c, e, channel, nick, msg)
+
+	def on_invite(self, c, e):
+		nick = e.source().split('!', 1)[0]
+		channel = e.arguments()[0]
+		if channel.startswith('#'):
+			channel = channel[1:]
+		self._channels.append(channel)
+		channel = '#' + channel
+		self._nolog.add(channel)
+		time.sleep(1)
+		c.join(channel)
+		time.sleep(1)
+		c.privmsg(channel, "You summoned me, master %s?" % nick)
+		
+
+	def handle_action(self, c, e, channel, nick, msg):
+		lc_msg = msg.lower()
+		lc_cmd = msg.split()[0]
+		res = None
+		secret = False
+		for typ, name, f, doc in sorted(_handler_registry, key=lambda x: (_handler_sort_order[x[0]], 0-len(x[1]), x[1])):
+			if typ in ('command', 'alias') and lc_cmd == '!%s' % name:
+				try:
+					if ' ' in msg:
+						msg = msg.split(' ', 1)[-1].lstrip()
+					else:
+						msg = ''
+					res = f(c, e, channel, nick, msg)
+				except Exception, e:
+					res = "DO NOT TRY TO BREAK PMXBOT!!!"
+					traceback.print_exc()
+				break
+			elif typ in('contains', '#') and name in lc_msg:
+				try:
+					res = f(c, e, channel, nick, msg)
+				except Exception, e:
+					res = "DO NOT TRY TO BREAK PMXBOT!!!"
+					res += '\n%s' % e
+					traceback.print_exc()
+				break
+		def out(s):
+			if s.startswith('/me '):
+				c.action(channel, s.split(' ', 1)[-1].lstrip())
+			else:
+				c.privmsg(channel, s)
+				if channel not in self._nolog and not secret:
+					logger.message(channel, self._nickname, s)
+		if res:
+			if isinstance(res, basestring):
+				out(res)
+			else:
+				for item in res:
+					if item == NoLog:
+						secret = True
+					else:
+						out(item)
+
+	def feed_parse(self, c, e, interval, feeds):
+		socket.setdefaulttimeout(20)
+		db = logger.db
+		try:
+			res = db.execute('select key from feed_seen')
+			FEED_SEEN = [x[0] for x in res]
+		except:
+			db.execute('CREATE TABLE feed_seen (key varchar)')
+			db.commit()
+			FEED_SEEN = []
+		NEWLY_SEEN = []
+		for this_feed in feeds:
+			outputs = []
+			try:
+				feed = feedparser.parse(this_feed['url'])
+			except:
+				pass
+			for entry in feed['entries']:
+				if entry.has_key('id'):
+					id = entry['id']
+				elif entry.has_key('link'):
+					id = entry['link']
+				elif entry.has_key('title'):
+					id = entry['title']
+				else:
+					continue #this is bad...
+				#If this is google let's overwrite
+				if 'google.com' in this_feed['url'].lower():
+					GNEWS_RE = re.compile(r'[?&]url=(.+?)[&$]', re.IGNORECASE)
+					try:
+						id = GNEWS_RE.findall(entry['link'])[0]
+					except:
+						pass
+				if id in FEED_SEEN:
+					continue
+				FEED_SEEN.append(id)
+				NEWLY_SEEN.append(id)
+				if ' by ' in entry['title']: #We don't need to add the author
+					out = '%s' % entry['title']
+				else:
+					try:
+						out = '%s by %s' % (entry['title'], entry['author'])
+					except KeyError:
+						out = '%s' % entry['title']
+				outputs.append(out)
+			if outputs:
+				txt = 'News from %s %s : %s' % (this_feed['name'], this_feed['linkurl'], ' || '.join(outputs[:10]))
+				txt = txt.encode('utf-8')
+				c.privmsg(this_feed['channel'], txt)
+			if NEWLY_SEEN:
+				db.executemany('INSERT INTO feed_seen (key) values (?)', [(x,) for x in NEWLY_SEEN])
+				db.commit()
+		c.execute_delayed(interval, self.feed_parse, arguments=(c, e, interval, feeds))
+
+
+_handler_registry = []
+_handler_sort_order = {'command' : 1, 'alias' : 2, 'contains' : 3}
+
+def contains(s, doc=None):
+	def deco(f):
+		if s == '#':
+			_handler_registry.append(('#', s.lower(), f, doc))
+		else:
+			_handler_registry.append(('contains', s.lower(), f, doc))
+		return f
+	return deco
+
+def command(s, aliases=None, doc=None):
+	def deco(f):
+		_handler_registry.append(('command', s.lower(), f, doc))
+		if aliases:
+			for a in aliases:
+				if not a.endswith(' '):
+					pass
+					#a += ' '
+				_handler_registry.append(('alias', a.lower(), f, doc))
+		return f
+	return deco
+
+class Logger(object):
+
+	def __init__(self, repo):
+		self.repo = repo
+		self.dbfn = pjoin(self.repo, 'pmxbot.sqlite')
+		self.db = sqlite.connect(self.dbfn)
+		LOG_CREATE_SQL = '''
+		CREATE TABLE IF NOT EXISTS logs (
+			id INTEGER NOT NULL,
+			datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+			channel VARCHAR NOT NULL,
+			nick VARCHAR NOT NULL,
+			MESSAGE TEXT,
+			PRIMARY KEY (id) )
+		'''
+		INDEX_CREATE_SQL = 'CREATE INDEX IF NOT EXISTS ix_logs_datetime_channel ON logs (datetime, channel)'
+		self.db.execute(LOG_CREATE_SQL)
+		self.db.execute(INDEX_CREATE_SQL)
+		self.db.commit()
+		
+	def message(self, channel, nick, msg):
+		INSERT_LOG_SQL = 'INSERT INTO logs (datetime, channel, nick, message) VALUES (?, ?, ?, ?)'
+		now = datetime.datetime.now()
+		channel = channel.replace('#', '')
+		self.db.execute(INSERT_LOG_SQL, [now, channel, nick, msg.encode('utf-8')])
+		self.db.commit()
+
+	def last_seen(self, nick):
+		FIND_LAST_SQL = 'SELECT datetime, channel FROM logs WHERE nick = ? ORDER BY datetime DESC LIMIT 1'
+		res = list(self.db.execute(FIND_LAST_SQL, [nick]))
+		self.db.commit()
+		if not res:
+			return None
+		else:
+			return res[0]
+
+	def strike(self, channel, nick, count):
+		count += 1 # let's get rid of 'the last !strike' too!
+		if count > 20:
+			count = 20
+		LAST_N_IDS_SQL = '''select channel, nick, id from logs where channel = ? and nick = ? and date(datetime) = date('now','localtime') order by datetime desc limit ?'''
+		DELETE_LINE_SQL = '''delete from logs where channel = ? and nick = ? and id = ?'''
+		channel = channel.replace('#', '')
+		
+		ids_to_delete = self.db.execute(LAST_N_IDS_SQL, [channel, nick, count]).fetchall()
+		if ids_to_delete:
+			deleted = self.db.executemany(DELETE_LINE_SQL, ids_to_delete)
+			self.db.commit()
+			rows_deleted = deleted.rowcount - 1
+		else:
+			rows_deleted = 0
+		rows_deleted = deleted.rowcount - 1
+		self.db.commit()
+		return rows_deleted
+		
+
+logger = None
+
+def setup_repo(path):
+	global logger
+	logger = Logger(path)

File pmxbot/cleanhtml.py

+# vim:ts=4:sw=4:expandtab
+"""Exposes several SGMLParser subclasses.
+
+This work, including the source code, documentation
+and related data, is placed into the public domain.
+
+The orginal author is Robert Brewer.
+
+THIS SOFTWARE IS PROVIDED AS-IS, WITHOUT WARRANTY
+OF ANY KIND, NOT EVEN THE IMPLIED WARRANTY OF
+MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE
+ASSUMES _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE
+RESULTING FROM THE USE, MODIFICATION, OR
+REDISTRIBUTION OF THIS SOFTWARE.
+
+If you don't need thread-safety, you might create a single instance of the
+parser you want, and feed it yourself. You also might use the classes
+directly if you need to customize them in some way; for example, you may
+need to alter the list of unsafe_tags in the Sanitizer class, either
+per-instance or by subclassing it.
+
+If you need thread-safe parsing, you should use the functions provided.
+They create a new instance each time, so you get a *small* performance
+hit, but by the same token, each thread can work on its own instance.
+"""
+
+import re
+import sgmllib
+import htmlentitydefs
+from xml.sax.saxutils import quoteattr
+
+interesting = re.compile('[&<]')
+incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|'
+                           '<([a-zA-Z][^<>]*|'
+                              '/([a-zA-Z][^<>]*)?|'
+                              '![^<>]*)?')
+
+entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
+charref = re.compile('&#([0-9]+)[^0-9]')
+
+starttagopen = re.compile('<[>a-zA-Z]')
+
+
+class MoreReasonableSGMLParser(sgmllib.SGMLParser):
+    """Just like an SGML Parser, but with more information passed
+    to the handle_ methods. For example, handle_entityref passes
+    the whole match, ampersand, name, and trailer."""
+    
+    # Internal -- handle data as far as reasonable.  May leave state
+    # and data to be processed by a subsequent call.  If 'end' is
+    # true, force handling all data as if followed by EOF marker.
+    def goahead(self, end):
+        rawdata = self.rawdata
+        i = 0
+        n = len(rawdata)
+        while i < n:
+            if self.nomoretags:
+                self.handle_data(rawdata[i:n])
+                i = n
+                break
+            match = interesting.search(rawdata, i)
+            if match: j = match.start()
+            else: j = n
+            if i < j:
+                self.handle_data(rawdata[i:j])
+            i = j
+            if i == n: break
+            if rawdata[i] == '<':
+                if starttagopen.match(rawdata, i):
+                    if self.literal:
+                        self.handle_data(rawdata[i])
+                        i = i+1
+                        continue
+                    k = self.parse_starttag(i)
+                    if k < 0: break
+                    i = k
+                    continue
+                if rawdata.startswith("</", i):
+                    k = self.parse_endtag(i)
+                    if k < 0: break
+                    i = k
+                    self.literal = 0
+                    continue
+                if self.literal:
+                    if n > (i + 1):
+                        self.handle_data("<")
+                        i = i+1
+                    else:
+                        # incomplete
+                        break
+                    continue
+                if rawdata.startswith("<!--", i):
+                        # Strictly speaking, a comment is --.*--
+                        # within a declaration tag <!...>.
+                        # This should be removed,
+                        # and comments handled only in parse_declaration.
+                    k = self.parse_comment(i)
+                    if k < 0: break
+                    i = k
+                    continue
+                if rawdata.startswith("<?", i):
+                    k = self.parse_pi(i)
+                    if k < 0: break
+                    i = i+k
+                    continue
+                if rawdata.startswith("<!", i):
+                    # This is some sort of declaration; in "HTML as
+                    # deployed," this should only be the document type
+                    # declaration ("<!DOCTYPE html...>").
+                    k = self.parse_declaration(i)
+                    if k < 0: break
+                    i = k
+                    continue
+            elif rawdata[i] == '&':
+                if self.literal:
+                    self.handle_data(rawdata[i])
+                    i = i+1
+                    continue
+                match = charref.match(rawdata, i)
+                if match:
+                    name = match.group(1)
+                    self.handle_charref(name)
+                    i = match.end(0)
+                    if rawdata[i-1] != ';': i = i-1
+                    continue
+                match = entityref.match(rawdata, i)
+                if match:
+                    name = match.group(1)
+                    i = match.end(0)
+                    trailer = rawdata[i-1]
+                    self.handle_entityref(name, trailer)
+                    if trailer != ';': i = i-1
+                    continue
+            else:
+                self.error('neither < nor & ??')
+            # We get here only if incomplete matches but
+            # nothing else
+            match = incomplete.match(rawdata, i)
+            if not match:
+                self.handle_data(rawdata[i])
+                i = i+1
+                continue
+            j = match.end(0)
+            if j == n:
+                break # Really incomplete
+            self.handle_data(rawdata[i:j])
+            i = j
+        # end while
+        if end and i < n:
+            self.handle_data(rawdata[i:n])
+            i = n
+        self.rawdata = rawdata[i:]
+        # XXX if end: check for empty stack
+
+
+class Plaintext(MoreReasonableSGMLParser):
+    """Strips all HTML from content.
+    Entities are translated to their Unicode equivalents where possible."""
+    
+    def handle_data(self, data):
+        self.result.append(data)
+    
+    def handle_charref(self, ref):
+        try:
+            self.result.append(unichr(int(ref)))
+        except ValueError:
+            self.result.append(u"?")
+        
+    def handle_entityref(self, ref, trailer):
+        try:
+            cp = htmlentitydefs.name2codepoint[ref]
+            self.result.append(unichr(cp))
+            if trailer != ";":
+                self.result.append(trailer)
+        except KeyError:
+            self.result.append("&" + ref + trailer)
+        except ValueError:
+            self.result.append("?")
+            if trailer != ";":
+                self.result.append(trailer)
+
+def plaintext(content):
+    """Strips all HTML from content.
+    Entities are translated to their Unicode equivalents where possible."""
+    s = Plaintext()
+    s.result = []
+    s.feed(content)
+    s.close()
+    return u"".join(s.result)
+
+
+class StripTags(MoreReasonableSGMLParser):
+    """Strips HTML tags from content. Entities are retained."""
+    
+    def handle_data(self, data):
+        self.result.append(data)
+    
+    def handle_charref(self, ref):
+        self.result.append('&#' + ref + ';')
+    
+    def handle_entityref(self, ref, trailer):
+        self.result.append('&' + ref + trailer)
+
+def striptags(content):
+    """Strips HTML tags from content. Entities are retained."""
+    s = StripTags()
+    s.result = []
+    s.feed(content)
+    s.close()
+    return u"".join(s.result)
+
+
+class Sanitizer(MoreReasonableSGMLParser):
+    """Strips specific HTML tags from content. Entities are retained."""
+    
+    unsafe_tags = [u'!doctype', u'applet', u'base', u'basefont', u'bgsound',
+                   u'blink', u'body', u'button', u'comment', u'embed',
+                   u'fieldset', u'fn', u'form', u'frame', u'frameset',
+                   u'head', u'html', u'iframe', u'ilayer', u'input',
+                   u'isindex', u'keygen', u'label', u'layer', u'legend',
+                   u'link', u'meta', u'noembed', u'noframes', u'noscript',
+                   u'object', u'optgroup', u'option', u'param', u'plaintext',
+                   u'select', u'script', u'style', u'textarea', u'title',
+                   ]
+    replacement = u"<!-- Prohibited Content -->"
+    javascript = r"""(?i)href\w*=['"]javascript:"""
+    unsafe_attributes = [u'abort', u'blur', u'change', u'click', 'dblclick',
+                         u'error', u'focus', u'keydown', u'keypress', u'keyup',
+                         u'load', u'mousedown', u'mouseout', u'mouseover',
+                         u'mouseup', u'reset', u'resize', u'submit', u'unload',
+                         ]
+    empty_tags = [u'area', u'base', u'basefont', u'br', u'hr', u'img',
+                  u'input', u'link', u'meta', u'param',
+                  ]
+    
+    def handle_data(self, data):
+        self.result.append(data)
+    
+    def handle_charref(self, ref):
+        self.result.append('&#' + ref + ';')
+    
+    def handle_entityref(self, ref, trailer):
+        self.result.append('&' + ref + trailer)
+    
+    def handle_decl(self, data):
+        tag = data.split(" ")[0].lower()
+        if ("!" + tag) in self.unsafe_tags:
+            self.result.append(self.replacement)
+        else:
+            self.result.append(u'<!' + data + '>')
+    
+    def unknown_starttag(self, tag, attributes):
+        if tag in self.unsafe_tags:
+            self.result.append(self.replacement)
+        else:
+            attrs = []
+            for name, value in attributes:
+                if name not in self.unsafe_attributes:
+                    attrs.append(' ' + name + '=' + quoteattr(value))
+            if tag in self.empty_tags:
+                tail = ' />'
+            else:
+                tail = '>'
+            self.result.append('<' + tag + ''.join(attrs) + tail)
+    
+    def unknown_endtag(self, tag):
+        if tag in self.unsafe_tags:
+            self.result.append(self.replacement)
+        else:
+            if tag not in self.empty_tags:
+                self.result.append('</' + tag + '>')
+
+def sanitize(content):
+    """Strips specific HTML tags from content. Entities are retained."""
+    s = Sanitizer()
+    s.result = []
+    s.feed(content)
+    s.close()
+    return u"".join(s.result)
+
+

File pmxbot/pmxbot.py

+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:noexpandtab
+from botbase import command, contains,  _handler_registry, NoLog, LoggingCommandBot
+import botbase
+import time
+import sys, re, urllib, random,  csv
+from datetime import date, timedelta
+from util import *
+from cStringIO import StringIO
+try:
+	import simplejson as json
+except ImportError:
+	import json # one last try-- python 2.6?
+from saysomething import FastSayer
+from cleanhtml import plaintext
+from xml.etree import ElementTree
+from sqlite3 import dbapi2 as sqlite
+
+QUOTE_PATH = os.path.join(os.path.dirname(__file__), "popquotes.sqlite")
+popular_quote_db = sqlite.connect(QUOTE_PATH)
+
+sayer = FastSayer()
+
+@command("google", aliases=('g',), doc="Look a phrase up on google")
+def google(client, event, channel, nick, rest):
+	BASE_URL = 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0&'
+	url = BASE_URL + urllib.urlencode({'q' : rest.strip()})
+	raw_res = urllib.urlopen(url).read()
+	results = json.loads(raw_res)
+	hit1 = results['responseData']['results'][0]
+	return ' - '.join((urllib.unquote(hit1['url']), hit1['titleNoFormatting']))
+
+@command("googlecalc", aliases=('gc',), doc="Calculate something using google")
+def googlecalc(client, event, channel, nick, rest):
+	query = rest
+	gcre = re.compile('<h2 class=r style="font-size:138%"><b>(.+?)</b>')
+	html = get_html('http://www.google.com/search?%s' % urllib.urlencode({'q' : query}))
+	return plaintext(gcre.search(html).group(1))
+
+@command("time", doc="What time is it in.... Similar to !weather")
+def googletime(client, event, channel, nick, rest):
+	rest = rest.strip()
+	if rest == 'all':
+		places = config.places
+	elif '|' in rest:
+		places = [x.strip() for x in rest.split('|')]
+	else:
+		places = [rest]
+	for place in places:
+		if not place.startswith('time'):
+			query = 'time ' + place
+		else:
+			query = place
+		timere = re.compile('<td valign=[a-z]+><em>(.+?)(?=<br>|</table>)')
+		html = get_html('http://www.google.com/search?%s' % urllib.urlencode({'q' : query}))
+		try:
+			time = plaintext(timere.search(html).group(1))
+			yield time
+		except AttributeError:
+			continue
+
+@command('weather', aliases=('w'), doc='Get weather for a place. All offices with "all", or a list of places separated by pipes.')
+def weather(client, event, channel, nick, rest):
+	rest = rest.strip()
+	if rest == 'all':
+		places = config.places
+	elif '|' in rest:
+		places = [x.strip() for x in rest.split('|')]
+	else:
+		places = [rest]
+	for place in places:
+		try:
+			url = "http://www.google.com/ig/api?" + urllib.urlencode({'weather' : place})
+			wdata = ElementTree.parse(urllib.urlopen(url))
+			city = wdata.find('weather/forecast_information/city').get('data')
+			tempf = wdata.find('weather/current_conditions/temp_f').get('data')
+			tempc = wdata.find('weather/current_conditions/temp_c').get('data')
+			conds = wdata.find('weather/current_conditions/condition').get('data')
+			conds = conds.replace('Snow Showers', '\xe2\x98\x83')
+			conds = conds.replace('Snow', '\xe2\x98\x83') # Fix snow description
+			future_day = wdata.find('weather/forecast_conditions/day_of_week').get('data')
+			future_highf = wdata.find('weather/forecast_conditions/high').get('data')
+			future_highc = int((int(future_highf) - 32) / 1.8)
+			future_conds = wdata.find('weather/forecast_conditions/condition').get('data')
+			future_conds = conds.replace('Snow Showers', '\xe2\x98\x83')
+			future_conds = conds.replace('Snow', '\xe2\x98\x83') # Fix snow description
+			weather = u"%s. Currently: %sF/%sC, %s.	%s: %sF/%sC, %s" % (city, tempf, tempc, conds, future_day, future_highf, future_highc, future_conds)
+			yield weather
+		except:
+			pass
+
+@command("translate", aliases=('trans', 'googletrans', 'googletranslate'), doc="Translate a phrase using Google Translate. First argument should be the language[s]. It is a 2 letter abbreviation. It will auto detect the orig lang if you only give one; or two languages joined by a |, for example 'en|de' to trans from English to German. Follow this by the phrase you want to translate.")
+def translate(client, event, channel, nick, rest):
+	rest = rest.strip()
+	langpair, meh, rest = rest.partition(' ')
+	if '|' not in langpair:
+		langpair = '|' + langpair
+	BASE_URL = 'http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&format=text&'
+	url = BASE_URL + urllib.urlencode({'q' : rest, 'langpair' : langpair})
+	raw_res = urllib.urlopen(url).read()
+	results = json.loads(raw_res)
+	translation = results['responseData']['translatedText']
+	return translation
+
+
+@command("boo", aliases=("b"), doc="Boo someone")
+def boo(client, event, channel, nick, rest):
+	slapee = rest
+	karmaChange(botbase.logger.db, slapee, -1)
+	return "/me BOOO %s!!! BOOO!!!" % slapee
+		
+@command("troutslap", aliases=("slap", "ts"), doc="Slap some(one|thing) with a fish")
+def troutslap(client, event, channel, nick, rest):
+	slapee = rest
+	karmaChange(botbase.logger.db, slapee, -1)
+	return "/me slaps %s around a bit with a large trout" % slapee
+
+@command("keelhaul", aliases=("kh",), doc="Inflict great pain and embarassment on some(one|thing)")
+def keelhaul(client, event, channel, nick, rest):
+	keelee = rest
+	karmaChange(botbase.logger.db, keelee, -1)
+	return "/me straps %s to a dirty rope, tosses 'em overboard and pulls with great speed. Yarrr!" % keelee
+
+@command("annoy", aliases=("a",), doc="Annoy everyone with meaningless banter")
+def annoy(client, event, channel, nick, rest):
+	def a1():
+		yield 'OOOOOOOHHH, WHAT DO YOU DO WITH A DRUNKEN SAILOR'
+		yield 'WHAT DO YOU DO WITH A DRUNKEN SAILOR'
+		yield "WHAT DO YOU DO WITH A DRUNKEN SAILOR, EARLY IN THE MORNIN'?"
+	def a2():
+		yield "I'M HENRY THE EIGHTH I AM"
+		yield "HENRY THE EIGHTH I AM I AM"
+		yield "I GOT MARRIED TO THE GIRL NEXT DOOR; SHE'S BEEN MARRIED SEVEN TIMES BEFORE"
+	def a3():
+		yield "BOTHER!"
+		yield "BOTHER BOTHER BOTHER!"
+		yield "BOTHER BOTHER BOTHER BOTHER!"
+	def a4():
+		yield "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+		yield "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
+		yield "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+	def a5():
+		yield "YOUR MOTHER WAS A HAMSTER!"
+		yield "AND YOUR FATHER SMELLED OF ELDERBERRIES!"
+	def a6():
+		yield "My Tallest! My Tallest! Hey! Hey My Tallest! My Tallest? My Tallest! Hey! Hey! Hey! My Taaaaaaallist! My Tallest? My Tallest! Hey! Hey My Tallest! My Tallest? It's me! My Tallest? My Tallest!"
+	return random.choice([a1, a2, a3, a4, a5, a6])()
+
+@command("dance", aliases=("d",), doc="Do a little dance")
+def dance(client, event, channel, nick, rest):
+	yield 'O-\-<'
+	yield 'O-|-<'
+	yield 'O-/-<'
+
+@command("panic", aliases=("pc",), doc="Panic!")
+def panic(client, event, channel, nick, rest):
+	yield 'O-|-<'
+	yield 'O-<-<'
+	yield 'O->-<'
+	yield 'AAAAAAAHHHH!!!  HEAD FOR THE HILLS!'
+
+@command("rubberstamp",  aliases=('approve',), doc="Approve something")
+def rubberstamp(client, event, channel, nick, rest):
+	parts = ["Bad credit? No credit? Slow credit?"]
+	rest = rest.strip()
+	if rest:
+		parts.append("%s is" % rest)
+		karmaChange(botbase.logger.db, rest, 1)
+	parts.append("APPROVED!")
+	return " ".join(parts)
+
+@command("cheer", aliases=("c",), doc="Cheer for something")
+def cheer(client, event, channel, nick, rest):
+	if rest:
+		karmaChange(botbase.logger.db, rest, 1)
+		return "/me cheers for %s!" % rest
+	karmaChange(botbase.logger.db, 'the day', 1)
+	return "/me cheers!"
+
+@command("golfclap", aliases=("clap",), doc="Clap for something")
+def golfclap(client, event, channel, nick, rest):
+	clapv = random.choice(clapvl)
+	adv = random.choice(advl)
+	adj = random.choice(adjl)
+	if rest:
+		clapee = rest.strip()
+		karmaChange(botbase.logger.db, clapee, 1)
+		return "/me claps %s for %s, %s %s." % (clapv, rest, adv, adj)
+	return "/me claps %s, %s %s." % (clapv, adv, adj)
+
+@command('featurecreep', aliases=('fc',), doc='Generate feature creep (P+C http://www.dack.com/web/bullshit.html)')
+def featurecreep(client, event, channel, nick, rest):
+	verb = random.choice(fcverbs).capitalize()
+	adjective = random.choice(fcadjectives)
+	noun = random.choice(fcnouns)
+	return '%s %s %s!' % (verb, adjective, noun)
+
+@command('job', aliases=('card',), doc='Generate a job title, http://www.cubefigures.com/job.html')
+def job(client, event, channel, nick, rest):
+	j1 = random.choice(jobs1)
+	j2 = random.choice(jobs2)
+	j3 = random.choice(jobs3)
+	return '%s %s %s' % (j1, j2, j3)
+
+@command('hire', doc="When all else fails, pmxbot delivers the perfect employee.")
+def hire(client, event, channel, nick, rest):
+	title = job(client, event, channel, nick, rest)
+	task = featurecreep(client, event, channel, nick, rest)
+	return "/me finds a new %s to %s" % (title, task.lower())
+
+@command('oregontrail', aliases=('otrail',), doc='It\'s edutainment!')
+def oregontrail(client, event, channel, nick, rest):
+	rest = rest.strip()
+	if rest:
+		who = rest.strip()
+	else:
+		who = random.choice([nick, channel, 'pmxbot'])
+	action = random.choice(otrail_actions)
+	if action in ('has', 'has died from'):
+		issue = random.choice(otrail_issues)
+		text = '%s %s %s.' % (who, action, issue)
+	else:
+		text = '%s %s' % (who, action)
+	return text
+
+#popquotes
+@command('bender', aliases=('bend',), doc='Quote Bender, a la http://en.wikiquote.org/wiki/Futurama')
+def bender(client, event, channel, nick, rest):
+	qt = bartletts(popular_quote_db, 'bender', nick, rest)
+	if qt:	return qt
+
+@command('zoidberg', aliases=('zoid',), doc='Quote Zoidberg, a la http://en.wikiquote.org/wiki/Futurama')
+def zoidberg(client, event, channel, nick, rest):
+	qt = bartletts(popular_quote_db, 'zoid', nick, rest)
+	if qt:	return qt
+
+@command('simpsons', aliases=('simp',), doc='Quote the Simpsons, a la http://snpp.com/')
+def simpsons(client, event, channel, nick, rest):
+	qt = bartletts(popular_quote_db, 'simpsons', nick, rest)
+	if qt:	return qt
+
+@command('hal', aliases=('2001',), doc='HAL 9000')
+def hal(client, event, channel, nick, rest):
+	qt = bartletts(popular_quote_db, 'hal', nick, rest)
+	if qt:	return qt
+
+@command('grail', aliases=(), doc='I'' questing baby')
+def grail(client, event, channel, nick, rest):
+	qt = bartletts(popular_quote_db, 'grail', nick, rest)
+	if qt:	return qt
+
+@command('anchorman', aliases=(), doc='Quote Anchorman.')
+def anchorman(client, event, channel, nick, rest):
+	qt = bartletts(popular_quote_db, 'anchorman', nick, rest)
+	if qt:	return qt
+
+#Added quotes
+@command('quote', aliases=('q',), doc='If passed with nothing then get a random quote. If passed with some string then search for that. If prepended with "add:" then add it to the db, eg "!quote add: drivers: I only work here because of pmxbot!"')
+def quote(client, event, channel, nick, rest):
+	qs = Quotes(botbase.logger.db, 'pmx')
+	rest = rest.strip()
+	if rest.startswith('add: ') or rest.startswith('add '):
+		quoteToAdd = rest.split(' ', 1)[1]
+		qs.quoteAdd(quoteToAdd)
+		qt = False
+		return 'Quote added!'
+	else:
+		qt, i, n = qs.quoteLookupWNum(rest)
+		if qt:
+			return '(%s/%s): %s' % (i, n, qt)
+
+@command('zinger', aliases=('zing',), doc='ZING!')
+def zinger(client, event, channel, nick, rest):
+	name = 'you'
+	if rest:
+		name = rest.strip()
+		karmaChange(botbase.logger.db, name, -1)
+	#qt = bartletts(botbase.logger.db, 'simpsons', nick, 'pardon my zinger')
+	#if qt:	return qt
+	return "OH MAN!!! %s TOTALLY GOT ZING'D!" % (name.upper())
+
+@command("motivate", aliases=("m", "appreciate", "thanks", "thank"), doc="Motivate someone")
+def motivate(client, event, channel, nick, rest):
+	if rest:
+		r = rest.strip()
+		karmaChange(botbase.logger.db, r, 1)
+	else:
+		r = channel
+	return "you're doing good work, %s!" % r
+
+@command("imotivate", aliases=("im", 'ironicmotivate',), doc='''Ironically "Motivate" someone''')
+def imotivate(client, event, channel, nick, rest):
+	if rest:
+		r = rest.strip()
+		karmaChange(botbase.logger.db, r, -1)
+	else:
+		r = channel
+	return '''you're "doing" "good" "work", %s!''' % r
+
+@command("demotivate", aliases=("dm",), doc="Demotivate someone")
+def demotivate(client, event, channel, nick, rest):
+	if rest:
+		r = rest.strip()
+		karmaChange(botbase.logger.db, r, -1)
+	else:
+		r = channel
+	return "you're doing horrible work, %s!" % r
+
+@command("8ball", aliases=("8",), doc="Ask the magic 8ball a question")
+def eball(client, event, channel, nick, rest):
+	return wchoice(ball8_opts)
+
+@command("klingon", aliases=('klingonism',), doc="Ask the magic klingon a question")
+def klingon(client, event, channel, nick, rest):
+	return random.choice(klingonisms)
+
+@command("roll", aliases=(), doc="Roll a die, default = 100.")
+def roll(client, event, channel, nick, rest):
+	if rest:
+		rest = rest.strip()
+		die = int(rest)
+	else:
+		die = 100
+	myroll = random.randint(1, die)
+	return "%s rolls %s" % (nick, myroll)
+		
+@command("flip", aliases=(), doc="Flip a coin")
+def flip(client, event, channel, nick, rest):
+	myflip = random.choice(('Heads', 'Tails'))
+	return "%s gets %s" % (nick, myflip)
+
+@command("deal", aliases=(), doc="Deal or No Deal?")
+def deal(client, event, channel, nick, rest):
+	mydeal = random.choice(('Deal!', 'No Deal!'))
+	return "%s gets %s" % (nick, mydeal)
+
+@command("ticker", aliases=("t",), doc="Look up a ticker symbol's current trading value")
+def ticker(client, event, channel, nick, rest):
+	ticker = rest.upper()
+	# let's use Yahoo's nifty csv facility, and pull last time/price both
+	stockInfo = csv.reader(urllib.urlopen('http://finance.yahoo.com/d/quotes.csv?s=%s&f=sl' % ticker))
+	lastTrade = stockInfo.next() 
+	if lastTrade[2] == 'N/A':
+		return "d'oh... could not find information for symbol %s" % ticker
+	else:
+		change = str(round((float(lastTrade[4]) / (float(lastTrade[1]) - float(lastTrade[4]))) * 100, 1))
+		return '%s at %s (ET): %s (%s%%)' % (ticker, lastTrade[3], lastTrade[1], change) 
+
+@command("pick", aliases=("p", 'p:', "pick:"), doc="Pick between a few options")
+def pick(client, event, channel, nick, rest):
+	question = rest.strip()
+	choices = splitem(question)
+	if len(choices) == 1:
+		return "I can't pick if you give me only one choice!"
+	else:
+		pick = random.choice(choices)
+		certainty = random.sample(certainty_opts, 1)[0]
+		return "%s... %s %s" % (pick, certainty, pick)
+
+@command("lunch", aliases=("lunchpick", "lunchpicker"), doc="Pick where to go to lunch")
+def lunch(client, event, channel, nick, rest):
+	rs = rest.strip()
+	if not rs:
+		return "Give me an area and I'll pick a place: (%s)" % (', '.join(list(config.lunch_choices)))
+	if rs not in config.lunch_choices:
+		return "I didn't recognize that area; here's what i have: (%s)" % (', '.join(list(config.lunch_choices)))
+	choices = config.lunch_choices[rs]
+	return random.choice(choices)
+
+@command("password", aliases=("pw", "passwd",), doc="Generate a random password, similar to http://www.pctools.com/guides/password")
+def password(client, event, channel, nick, rest):
+	chars = '32547698ACBEDGFHKJMNQPSRUTWVYXZacbedgfhkjmnqpsrutwvyxz'
+	passwd = []
+	for i in range(8):
+		passwd.append(random.choice(chars))
+	return ''.join(passwd)
+
+@command("insult", aliases=(), doc="Generate a random insult from http://www.webinsult.com/index.php")
+def insult(client, event, channel, nick, rest):
+	instype = random.randrange(4)
+	insurl = "http://www.webinsult.com/index.php?style=%s&r=0&sc=1" % instype
+	insre = re.compile('<div class="insult" id="insult">(.*?)</div>')
+	html = get_html(insurl)
+	insult = insre.search(html).group(1)
+	if insult:
+		if rest:
+			insultee = rest.strip()
+			karmaChange(botbase.logger.db, insultee, -1)
+			if instype in (0, 2):
+				cinsre = re.compile(r'\b(your)\b', re.IGNORECASE)
+				insult = cinsre.sub("%s's" % insultee, insult)
+			elif instype in (1, 3):
+				cinsre = re.compile(r'^([TY])')
+				insult = cinsre.sub(lambda m: "%s, %s" % (insultee, m.group(1).lower()), insult)
+		return insult
+
+@command("compliment", aliases=('surreal',), doc="Generate a random compliment from http://www.madsci.org/cgi-bin/cgiwrap/~lynn/jardin/SCG")
+def compliment(client, event, channel, nick, rest):
+	compurl = 'http://www.madsci.org/cgi-bin/cgiwrap/~lynn/jardin/SCG'
+	comphtml = ''.join([i for i in urllib.urlopen(compurl)])
+	compmark1 = '<h2>\n\n'
+	compmark2 = '\n</h2>'
+	compliment = comphtml[comphtml.find(compmark1) + len(compmark1):comphtml.find(compmark2)]
+	if compliment:
+		compliment = re.compile(r'\n').sub('%s' % ' ', compliment)
+		compliment = re.compile(r'  ').sub('%s' % ' ', compliment)
+		if rest:
+			complimentee = rest.strip()
+			karmaChange(botbase.logger.db, complimentee, 1)
+			compliment = re.compile(r'\b(your)\b', re.IGNORECASE).sub('%s\'s' % complimentee, compliment)
+			compliment = re.compile(r'\b(you are)\b', re.IGNORECASE).sub('%s is' % complimentee, compliment)
+			compliment = re.compile(r'\b(you have)\b', re.IGNORECASE).sub('%s has' % complimentee, compliment)
+		return compliment
+ 
+
+@command("karma", aliases=("k",), doc="Return or change the karma value for some(one|thing)")
+def karma(client, event, channel, nick, rest):
+	karmee = rest.strip('++').strip('--').strip('~~')
+	if '++' in rest: 
+		karmaChange(botbase.logger.db, karmee, 1)
+	elif '--' in rest: 
+		karmaChange(botbase.logger.db, karmee, -1)
+	elif '~~' in rest:
+		change = random.choice([-1, 0, 1])
+		karmaChange(botbase.logger.db, karmee, change)
+		if change == 1:
+			return "%s karma++" % karmee
+		elif change == 0:
+			return "%s karma shall remain the same" % karmee
+		elif change == -1:
+			return "%s karma--" % karmee
+	elif '==' in rest:
+		t1, t2 = rest.split('==')
+		karmaLink(botbase.logger.db, t1, t2)
+		score = karmaLookup(botbase.logger.db, t1)
+		return "%s and %s are now linked and have a score of %s" % (t1, t2, score)
+	else:
+		karmee = rest or nick
+		score = karmaLookup(botbase.logger.db, karmee)
+		return "%s has %s karmas" % (karmee, score)
+
+@command("top10", aliases=("top",), doc="Return the top n (default 10) highest entities by Karmic value. Use negative numbers for the bottom N.")
+def top10(client, event, channel, nick, rest):
+	if rest:
+		topn = int(rest)
+	else:
+		topn = 10
+	selection = karmaList(botbase.logger.db, topn)
+	res = ' '.join('(%s: %s)' % (', '.join(n), k) for n, k in selection)
+	return res
+
+@command("bottom10", aliases=("bottom",), doc="Return the bottom n (default 10) lowest entities by Karmic value. Use negative numbers for the bottom N.")
+def top10(client, event, channel, nick, rest):
+	if rest:
+		topn = -int(rest)
+	else:
+		topn = -10
+	selection = karmaList(botbase.logger.db, topn)
+	res = ' '.join('(%s: %s)' % (', '.join(n), k) for n, k in selection)
+	return res
+
+
+@command("excuse", aliases=("e ",), doc="Provide a convenient excuse")
+def excuse(client, event, channel, nick, rest):
+	args = "/".join(rest.split(' ')[:2])
+	if args:
+		args = "/" + args
+		url = 'http://www.dowski.com/excuses/new%s' % args
+	else:
+		url = 'http://www.dowski.com/excuses/new'
+	excuse = get_html(url)
+	return excuse
+
+@command("curse", doc="Curse the day!")
+def curse(client, event, channel, nick, rest):
+	if rest:
+		cursee = rest
+	else:
+		cursee = 'the day'
+	karmaChange(botbase.logger.db, cursee, -1)
+	return "/me curses %s!" % cursee
+
+@command("tinytear", aliases=('tt', 'tear', 'cry'), doc="I cry a tiny tear for you.")
+def tinytear(client, event, channel, nick, rest):
+	if rest:
+		return "/me sheds a single tear for %s" % rest
+	else:
+		return "/me sits and cries as a single tear slowly trickles down its cheek"
+
+@command("stab", aliases=("shank", "shiv",),doc="Stab, shank or shiv some(one|thing)!")
+def stab(client, event, channel, nick, rest):
+	if rest:
+		stabee = rest
+	else:
+		stabee = 'wildly at anything'
+	if random.random() < 0.9:
+		karmaChange(botbase.logger.db, stabee, -1)
+		weapon = random.choice(weapon_opts)
+		weaponadj = random.choice(weapon_adjs)
+		violentact = random.choice(violent_acts)
+		return "/me grabs a %s %s and %s %s!" % (weaponadj, weapon, violentact, stabee)
+	elif random.random() < 0.6:
+		karmaChange(botbase.logger.db, stabee, -1)
+		return "/me is going to become rich and famous after i invent a device that allows you to stab people in the face over the internet"
+	else:
+		karmaChange(botbase.logger.db, nick, -1)
+		return "/me turns on its master and shivs %s. This is reality man, and you never know what you're going to get!" % nick
+
+@command("disembowel", aliases=("dis", "eviscerate"),doc="Disembowel some(one|thing)!")
+def disembowel(client, event, channel, nick, rest):
+	if rest:
+		stabee = rest
+		karmaChange(botbase.logger.db, stabee, -1)
+	else:
+		stabee = "someone nearby"
+	return "/me takes %s, brings them down to the basement, ties them to a leaky pipe, and once bored of playing with them mercifully ritually disembowels them..." % stabee
+
+@command("embowel", aliases=("reembowel",), doc="Embowel some(one|thing)!")
+def embowel(client, event, channel, nick, rest):
+	if rest:
+		stabee = rest
+		karmaChange(botbase.logger.db, stabee, 1)
+	else:
+		stabee = "someone nearby"
+	return "/me (wearing a bright pink cape and yellow tights) swoops in through an open window, snatches %s, races out of the basement, takes them to the hospital with entrails on ice, and mercifully embowels them, saving the day..." % stabee
+
+@command("chain", aliases=(),doc="Chain some(one|thing)down.")
+def chain(client, event, channel, nick, rest):
+	if rest:
+		chainee = rest
+	else:
+		chainee = "someone nearby"
+	if chainee == 'cperry':
+		return "/me ties the chains extra tight around %s" % chainee
+	elif random.randint(1,10) != 1:
+		return "/me chains %s to the nearest desk.  you ain't going home, buddy." % chainee
+	else:
+		karmaChange(botbase.logger.db, nick, -1)
+		return "/me spins violently around and chains %s to the nearest desk.  your days of chaining people down and stomping on their dreams are over!  get a life, you miserable beast." % nick
+
+@command("bless", doc="Bless the day!")
+def bless(client, event, channel, nick, rest):
+	if rest:
+		blesse = rest
+	else:
+		blesse = 'the day'
+	karmaChange(botbase.logger.db, blesse, 1)
+	return "/me blesses %s!" % blesse
+
+@command("blame", doc="Pass the buck!")
+def blame(client, event, channel, nick, rest):
+	if rest:
+		blamee = rest
+	else:
+		blamee = channel
+	karmaChange(botbase.logger.db, nick, -1)
+	if random.randint(1,10) == 1:
+		yield "/me jumps atop the chair and points back at %s." % nick
+		yield "stop blaming the world for your problems, you bitter, two-faced sissified monkey!"
+	else:
+		yield "I blame %s for everything!  it's your fault!  it's all your fault!!" % blamee
+		yield "/me cries and weeps in despair"
+
+@contains('pmxbot')
+def rand_bot(client, event, channel, nick, rest):
+	if (channel == config.inane_channel and random.random() < .2):
+		normal_functions = [featurecreep, insult, motivate, compliment, cheer,
+			golfclap, excuse, nastygram, curse, bless, job, hire, 
+			bakecake, cutcake, oregontrail, chain, tinytear, blame,
+			reweight, panic, rubberstamp, dance, annoy, klingon, 
+			storytime, murphy]
+		quote_functions = [quote, falconer, gir, zim, zoidberg, simpsons, bender, hal, grail]
+		ftype = random.choice('n'*len(normal_functions) + 'q'*len(quote_functions))
+		if ftype == 'n':
+			func = random.choice(normal_functions)
+			res = func(client, event, channel, 'pmxbot', nick)
+		elif ftype == 'q':
+			func = random.choice(quote_functions)
+			res = func(client, event, channel, 'pmxbot', '')
+		return res
+		
+@contains("sqlonrails")
+def yay_sor(client, event, channel, nick, rest):
+	karmaChange(botbase.logger.db, 'sql on rails', 1)
+	return "Only 76,417 lines..."
+
+@contains("sql on rails")
+def other_sor(*args):
+	return yay_sor(*args)
+
+calc_exp = re.compile("^[0-9 \*/\-\+\)\(\.]+$")
+@command("calc", doc="Perform a basic calculation")
+def calc(client, event, channel, nick, rest):
+	mo = calc_exp.match(rest)
+	if mo:
+		try:
+			return str(eval(rest))
+		except:
+			return "eval failed... check your syntax"
+	else:
+		return "misformatted arithmetic!"
+
+@command("define", aliases=("def",), doc="Define a word")
+def defit(client, event, channel, nick, rest):
+	word = rest.strip()
+	res = lookup(word)
+	if res is None:
+		return "Arg!  I didn't find a definition for that."
+	else:
+		return 'Wikipedia says: ' + res
+
+
+@command("acronym", aliases=("ac",), doc="Look up an acronym")
+def acit(client, event, channel, nick, rest):
+	word = rest.strip()
+	res = lookup_acronym(word)
+	if res is None:
+		return "Arg!  I couldn't expand that..."
+	else:
+		return ' | '.join(res)
+
+@command("fight", doc="Pit two sworn enemies against each other")
+def fight(client, event, channel, nick, rest):
+	if rest:
+		vtype = random.choice(fight_victories)
+		fdesc = random.choice(fight_descriptions)
+		bad_protocol = False
+		if 'vs.' not in rest:
+			bad_protocol = True
+		contenders = [c.strip() for c in rest.split('vs.')]
+		if len(contenders) > 2:
+			bad_protocol = True
+		if bad_protocol:
+			karmaChange(botbase.logger.db, nick.lower(), -1)
+			args = (vtype, nick, fdesc)
+			return "/me %s %s in %s for bad protocol." % args
+		random.shuffle(contenders)
+		winner,loser = contenders
+		karmaChange(botbase.logger.db, winner, 1)
+		karmaChange(botbase.logger.db, loser, -1)
+		return "%s %s %s in %s." % (winner, vtype, loser, fdesc)
+
+@command("progress", doc="Display the progress of something: start|end|percent")
+def progress(client, event, channel, nick, rest):
+	if rest:
+		left, right, amount = [piece.strip() for piece in rest.split('|')]
+		ticks = min(int(round(float(amount) / 10)), 10)
+		bar = "=" * ticks
+		return "%s [%-10s] %s" % (left, bar, right) 
+
+@command("nastygram", aliases=('nerf', 'passive', 'bcc'), doc="A random passive-agressive comment, optionally directed toward some(one|thing).")
+def nastygram(client, event, channel, nick, rest):
+	recipient = ""
+	if rest:
+		recipient = rest.strip()
+		karmaChange(botbase.logger.db, recipient, -1)
+	return passagg(recipient, nick.lower())
+
+@command("therethere", aliases=('poor', 'comfort'), doc="Sympathy for you.")
+def therethere(client, event, channel, nick, rest):
+	if rest:
+		karmaChange(botbase.logger.db, rest, 1)
+		return "There there %s... There there." % rest
+	else:
+		return "/me shares its sympathy."
+
+@command("saysomething", aliases=(), doc="Generate a Markov Chain response based on past logs. Seed it with a starting word by adding that to the end, eg '!saysomething dowski:'")
+def saysomething(client, event, channel, nick, rest):
+	sayer.startup(botbase.logger.db)
+	if rest:
+		return sayer.saysomething(rest)
+	else:	
+		return sayer.saysomething()
+
+@command("tgif", doc="Thanks for the words of wisdow, Mike.")
+def tgif(client, event, channel, nick, rest):
+	return "Hey, it's Friday! Only two more days left in the work week!"
+
+@command("fml", aliases=(), doc="A SFW version of fml.")
+def fml(client, event, channel, nick, rest):
+	return "indeed"
+
+@command("storytime", aliases=('story',), doc="A story is about to be told.")
+def storytime(client, event, channel, nick, rest):
+	if rest:
+		return "Come everyone, gather around the fire. %s is about to tell us a story!" % rest.strip()
+	else:
+		return "Come everyone, gather around the fire. A story is about to be told!"
+
+@command("murphy", aliases=('law',), doc="Look up one of Murphy's laws")
+def murphy(client, event, channel, nick, rest):
+	return random.choice(murphys_laws)
+
+@command("meaculpa", aliases=('apology', 'apologize',), doc="Sincerely apologize")
+def meaculpa(client, event, channel, nick, rest):
+	if rest:
+		rest = rest.strip()
+
+	if rest:
+		return random.choice(direct_apologies) % dict(a=nick, b=rest)
+	else:
+		return random.choice(apologies) % dict(a=nick)
+
+
+#Below is system junk
+@command("help", aliases=('h',), doc="Help (this command)")
+def help(client, event, channel, nick, rest):
+	rs = rest.strip()
+	if rs:
+		for typ, name, f, doc in _handler_registry:
+			if name == rs:
+				yield '!%s: %s' % (name, doc)
+				break
+		else:
+			yield "command not found"
+	else:
+		def mk_entries():
+			for typ, name, f, doc in sorted(_handler_registry, key=lambda x: x[1]):
+				if typ == 'command':
+					aliases = sorted([x[1] for x in _handler_registry if x[0] == 'alias' and x[2] == f])
+					res =  "!%s" % name
+					if aliases:
+						res += " (%s)" % ', '.join(aliases)
+					yield res
+		o = StringIO("|".join(mk_entries()))
+		more = o.read(160)
+		while more:
+			yield more
+			time.sleep(0.3)
+			more = o.read(160)
+
+@command("ctlaltdel", aliases=('controlaltdelete', 'controlaltdelete', 'cad', 'restart', 'quit',), doc="Quits pmxbot. Daemontools should automatically restart it.")
+def ctlaltdel(client, event, channel, nick, rest):
+	if 'real' in rest.lower():
+		sys.exit()
+	else:
+		return "Really?"
+
+@command("hgup", aliases=('hg', 'update', 'hgpull'), doc="Update with the latest from mercurial")
+def svnup(client, event, channel, nick, rest):
+	svnres = os.popen('hg pull -u')
+	svnres = svnres.read()
+	svnres = svnres.splitlines()
+	return svnres
+
+@command("strike", aliases=(), doc="Strike last <n> statements from the record")
+def strike(client, event, channel, nick, rest):
+	yield NoLog
+	rest = rest.strip()
+	if not rest:
+		count = 1
+	else:
+		if not rest.isdigit():
+			yield "Strike how many?  Argument must be a positive integer."
+			raise StopIteration
+		count = int(rest)
+	try:
+		struck = botbase.logger.strike(channel, nick, count)
+		yield ("Isn't undo great?  Last %d statement%s by %s were stricken from the record." %
+		(struck, 's' if struck > 1 else '', nick))
+	except:
+		yield "Hmm.. I didn't find anything of yours to strike!"
+
+@command("where", aliases=('last', 'seen', 'lastseen'), doc="When did pmxbot last see <nick> speak?")
+def where(client, event, channel, nick, rest):
+	onick = rest.strip()
+	last = botbase.logger.last_seen(onick)
+	if last:
+		tm, chan = last
+		return "I last saw %s speak at %s in channel #%s" % (
+		onick, tm, chan)
+	else:
+		return "Sorry!  I don't have any record of %s speaking" % onick
+
+
+global config
+
+def run():
+	global config
+	import sys, yaml
+	if len(sys.argv) < 2:
+		sys.stderr.write("error: need config file as first argument")
+		raise SystemExit(1)
+	
+	config_file = sys.argv[1]
+	class O(object): 
+		def __init__(self, d):
+			for k, v in d.iteritems(): setattr(self, k, v)
+			
+	config = O(yaml.load(open(config_file)))
+
+	@contains(config.bot_nickname)
+	def rand_bot2(*args):
+		return rand_bot(*args)
+
+	bot = LoggingCommandBot(config.database_dir, config.server_host, config.server_port, 
+		config.bot_nickname, config.log_channels, config.other_channels,
+		config.feed_interval*60, config.feeds)
+	bot.start()

File pmxbot/popquotes.sqlite

Binary file added.

File pmxbot/saysomething.py

+# vim:ts=4:sw=4:noexpandtab
+import random
+
+nlnl = '\n', '\n'
+
+def new_key(key, word):
+	if word == '\n': return nlnl
+	else: return (key[1], word)
+
+def markov_data_from_words(words):
+	data = {}
+	key = nlnl
+	for word in words:
+		data.setdefault(key, []).append(word)
+		key = new_key(key, word)
+	return data
+
+def words_from_markov_data(data, initial_word='\n'):
+	key = '\n', initial_word
+	if initial_word != '\n':
+		yield initial_word
+	while 1:
+		word = random.choice(data.get(key, nlnl))
+		key = new_key(key, word)
+		yield word
+
+def words_from_file(f):
+	for line in f:
+		words = line.split()
+		if len(words):
+			for word in words:
+				yield word
+		else:
+			yield '\n'
+	yield '\n'
+
+def words_from_db(db):
+	db.text_factory = str
+	WORDS_SQL = '''SELECT message FROM logs UNION SELECT quote FROM quotes where library = 'pmx' '''
+	lines = db.execute(WORDS_SQL)
+	for line in lines:
+		words = line[0].strip().lower().split()
+		for word in words:
+			yield word
+		yield '\n'
+	yield '\n'
+
+def paragraph_from_words(words):
+	result = []
+	for word in words:
+		if word == '\n': break
+		result.append(word)
+	return ' '.join(result)
+
+def saysomething_db(db, initial_word='\n'):
+	return paragraph_from_words(
+			words_from_markov_data(
+				markov_data_from_words(
+					words_from_db(db)),
+				initial_word))
+
+class FastSayer(object):
+	def __init__(self):
+		self.started = False
+
+	def startup(self, db):
+		if not self.started:
+			self.db = db
+			self.markovdata = markov_data_from_words(
+						words_from_db(db))
+			self.started = True
+
+	def saysomething(self, initial_word='\n'):
+		return paragraph_from_words(words_from_markov_data(self.markovdata, initial_word))

File pmxbot/util.py

+# vim:ts=4:sw=4:noexpandtab
+import os
+import random
+import re
+import urllib
+import httplib2
+
+ball8_opts = { 
+"Signs point to yes." : 21,
+"Yes." : 21,
+"Most likely." : 21,
+"Without a doubt." : 21,
+"Yes - definitely." : 21,
+"As I see it, yes." : 21,
+"You may rely on it." : 18,
+"Outlook good." : 18,
+"It is certain." : 18,
+"It is decidedly so." : 18,
+"Reply hazy, try again." : 1,
+"Better not tell you now." : 1,
+"Ask again later." : 1,
+"Concentrate and ask again." : 1,
+"Cannot predict now." : 1,
+"Six of one, half-dozen of another." : 1,
+"Hold on, let me check the latest polls." : 1,
+"Would you like fries with that?" : 1,
+"Will you go to lunch? Go to lunch. Will you go to lunch?" : 1,
+"I grow weary of your questions." : 1,
+"Get away from me, kid, you bother me." : 1,
+"Go, and never darken my towels again." : 1,
+"Your question fills a much-needed gap." : 1,
+"I have heard your question and much like it." : 1,
+"Only reading entrails can answer that." : 1,
+"That is alas a mystery.  Unless you ask again." : 1,
+"Your question makes my circuits hurt." : 1,
+"You say 'tom ay to,' I say 'no way, Joe.'" : 1,
+"Doubt it." : 10,
+"No." : 10,
+"Indeed." : 10,
+"Yes, please." : 10,
+"Inevitably." : 10,
+"Probably." : 10,
+"Unquestionably." : 10,
+"Nay." : 1,
+"When pigs fly." : 1,
+"Whatever happened to crossing my palms with silver?" : 1,
+"Ask the NSA." : 1,
+"Not likely before the heat death of the universe." : 1,
+"Not if wishes were horses.  Neigh." : 1,
+"A little birdie told me no." : 1,
+"Pipe down in the peanut gallery, willya?" : 1,
+"My sources say no." : 18,
+"Very doubtful." : 18,
+"My reply is no." : 18,
+"Outlook not so good." : 18,
+"Don't count on it." : 18,
+"Not a chance." : 18,
+"Highly unlikely." : 18,
+"NFW." : 25,
+"You're more likely to get rich off you.l." : 18,
+"Not a chance, buddy." : 18,
+"p < .05" : 18,
+"Sample size too small." : 18,
+"Tentatively confirmed.": 18,
+}
+
+certainty_opts = [
+"definitely",
+"probably",
+"maybe",
+"possibly",
+"perhaps",
+"certainly",
+"questionably",
+"unquestionably",
+"without doubt",
+"absolutely",
+]
+
+weapon_opts = [
+'shiv',
+'shank',
+'knife',
+'spork',
+'razor',
+'cocktail sword',
+'spatula',
+'scimitar',
+]
+
+weapon_adjs = [
+'rusty',
+'dirty',
+'broken',
+'gleaming',
+'serrated',
+'dull',
+'sharp',
+'bent',
+'flaming',
+]
+
+violent_acts = [
+'stabs',
+'cuts',
+'maims',
+'sticks',
+'shivs',
+'shanks',
+'disembowels',
+'minces',
+'perforates',
+'lobotomizes',
+'slashes',
+'furiously pokes',
+]
+
+fight_victories = [
+'defeats',
+'destroys',
+'dominates',
+'edges',
+'pwns',
+'crushes',
+'obliviates',
+'trounces',
+'sneaks by',
+'hires a hitman to take care of',
+]
+
+fight_descriptions = [
+'a barroom brawl',
+'a cagematch',
+'a fight in the octagon',
+'a fight to the death',
+'a fight to the pain',
+'a slapfest',
+'a thumbwrestling match',
+'a battle with dull, flaming scimitars',
+'a gentlemanly game of chess',
+'a bat fight',
+'a stickbat fight',
+'with a crowbar to the kneee.'
+]
+
+dowski_praises = [
+"and I defer to your distinguished discernment",
+"and I honor your legacy",
+"and I'm always astounded by your acumen",
+"cursing those who would deny you deference",
+"greeting your grand gloriousness with groveling",
+"humbled by your unbelievable understanding",
+"long live your long-leggedness",
+"may all respect you and your recognized renowned remarkableness",
+"may all of your successes be glorious",
+"may your burdens be light upon your back",
+"may the road rise up to meet you",
+"may the wind be always at your back",
+"may the sun shine warm upon your face",
+"may all of your enemies be !shivved",
+"oh great ruler of all things IS",
+"oh wondrous wonderboy of the midwest",
+"oh phenomenal prodigy of python programming",
+"oh loquacious liege",
+"oh happy harbinger of happiness",
+"saluting your supreme superiority",
+"with my obeisance to your ohioness",
+"viva la dowski",
+"(everyone knows you're the best)",
+"you're much more splendorous than windowsill to me",
+"chowski or dowski, your prowess is unparalleled",
+"dowski manor may be in canton, but it must be heavenly",
+]
+
+holger_dawg = [
+"WAZZUP DAWG?",
+"wassup, my dawg brotha?",
+"you be trippin', my homey dawg",
+"you be comin' to liberate, holger the dane dawg?",
+"you can put my dawg holger into da hood, but you'll never put da hood into my dawg holger",
+]
+
+yuckvl = ['grinds teeth', 'shudders', 'gags', 'blinks away tears', 'coughs', 'stares', 'drops to the ground curled in the fetal position', 'spits', 'wards off the evil eye', 'dies a little more inside', 'contemplates suicide', 'cackles maniacally and slowly goes mad', 'starts to reread Ecclesiastes', 'blames this fiasco on Carl Friedrich Gauss', 'ponders a career change', 'twitches', 'considers alcoholism', 'remains very, very still', 'hisses', 'groans']
+
+clapvl = ['slowly', 'sadly', 'quietly', 'briefly', 'halfheartedly']
+
+advl = ['clearly', 'likely', 'utterly', 'deeply', 'profoundly']
+
+adjl = ['unimpressed', 'overwhelmed', 'verklemmt', 'distracted', 'awed']
+
+fcverbs = ['aggregate', 'architect', 'benchmark', 'brand', 'cultivate', 'deliver', 'deploy', 'disintermediate', 'drive', 'e-enable', 'embrace', 'empower', 'enable', 'engage', 'engineer', 'enhance', 'envisioneer', 'evolve', 'expedite', 'exploit', 'extend', 'facilitate', 'generate', 'grow', 'harness', 'implement', 'incentivize', 'incubate', 'innovate', 'integrate', 'iterate', 'leverage', 'matrix', 'maximize', 'mesh', 'monetize', 'morph', 'optimize', 'orchestrate', 'productize', 'recontextualize', 'redefine', 'reintermediate', 'reinvent', 'repurpose', 'revolutionize', 'scale', 'seize', 'strategize', 'streamline', 'syndicate', 'synergize', 'synthesize', 'target', 'transform', 'transition', 'unleash', 'utilize', 'visualize', 'whiteboard']
+
+fcadjectives = ['24/365', '24/7', 'B2B', 'B2C', 'back-end', 'best-of-breed', 'bleeding-edge', 'bricks-and-clicks', 'clicks-and-mortar', 'collaborative', 'compelling', 'cross-platform', 'cross-media', 'customized', 'cutting-edge', 'distributed', 'dot-com', 'dynamic', 'e-business', 'efficient', 'end-to-end', 'enterprise', 'extensible', 'frictionless', 'front-end', 'global', 'granular', 'holistic', 'impactful', 'innovative', 'integrated', 'interactive', 'intuitive', 'killer', 'leading-edge', 'magnetic', 'mission-critical', 'next-generation', 'one-to-one', 'open-source', 'out-of-the-box', 'plug-and-play', 'proactive', 'real-time', 'revolutionary', 'rich', 'robust', 'scalable', 'seamless', 'sexy', 'sticky', 'strategic', 'synergistic', 'transparent', 'turn-key', 'ubiquitous', 'user-centric', 'value-added', 'vertical', 'viral', 'virtual', 'visionary', 'web-enabled', 'wireless', 'world-class']
+
+fcnouns = ['action-items', 'applications', 'architectures', 'bandwidth', 'channels', 'communities', 'content', 'convergence', 'deliverables', 'e-business', 'e-commerce', 'e-markets', 'e-services', 'e-tailers', 'experiences', 'eyeballs', 'functionalities', 'infomediaries', 'infrastructures', 'initiatives', 'interfaces', 'markets', 'methodologies', 'metrics', 'mindshare', 'models', 'networks', 'niches', 'paradigms', 'partnerships', 'platforms', 'portals', 'relationships', 'ROI', 'synergies', 'web-readiness', 'schemas', 'solutions', 'supply-chains', 'systems', 'technologies', 'users', 'portals', 'web services']
+
+jobs1 = ["Assistant", "Internal", "External", "Foreign", "Domestic", "Deputy", "Junior", "Senior", "Regional", "Level B", "Level C", "Inter", "Intra", "YouGov", "Executive", "Special", "Polimetrix", "Primary", "Lead", "Backup", "Chief"]
+
+jobs2 = ["Project", "Systems", "Marketing", "Purchasing", "Communications", "Sales", "Financial", "Accounting", "Personnel", "Engineering", "Information", "Customer Service", "Operations", "Development", "Surveys", "Panel", "Database", "Projects", "Analytics"]
+
+jobs3 = ["Manager", "Specialist", "Coordinator", "Administrator", "Analyst", "Planner", "Processor", "Consultant", "Clerk", "Officer", "Monitor", "Associate", "Trainee", "I", "II", "III", "IV", "V", "VP", "EVP", "SVP", "Director", "Developer", "Trainer", "Contractor", "Consultant", "Executive"]
+
+otrail_actions = ['has']*6 + ['has died from'] * 6 + ['lost the trail. Lose 3 days.', 'took the ferry across the river safely.', 'forded the river safely.', 'capsized while floating across the river.', 'drowned.', 'killed a bear.', 'killed a buffalo.', 'lost an ox.', 'made it to oregon. Time to party with schmichael.', 'finds wild fruit.', 'traded with Indians.', 'passes a gravesite.', 'had no trouble floating the wagon across.', 'is taking the rapids.', 'is attacked by ninjas. Lose 8 days.', 'is attacked by reavers and dies.']
+
+otrail_issues = ['a fever', 'dysentery', 'measles', 'cholera', 'typhoid', 'exhaustion', 'a snake bite', 'a broken leg', 'a broken arm', 'swine flu']
+
+klingonisms = [
+	"I have challenged the entire ISO-9000 review team to a round of Bat-Leth practice on the holodeck. They will not concern us again.",
+	"A TRUE Klingon warrior does not comment his code!",
+	"Behold, the keyboard of Kalis! The greatest Klingon code warrior that ever lived!",
+	"By filing this bug report you have challenged the honour of my family. Prepare to die! ",
+	"C++? That is for children. A Klingon Warrior uses only machine code, keyed in on the front panel switches in raw binary.",
+	"Debugging? Klingons do not debug. Bugs are good for building character in the user.",
+	"Debugging? Klingons do not debug. Our software does not coddle the weak.",
+	"Defensive programming? Never! Klingon programs are always on the offense. Yes, Offensive programming is what we do best.",
+	"I am without honor...my children are without honor... My father coded at the Battle of Kittimer...and...and...he... HE ALLOWED HIMSELF TO BE MICROMANAGED. <shudder>",
+	"I have challenged the entire testing team to a Bat-Leth contest. They will not concern us again.",
+	"Indentation?! - I will show you how to indent when I indent your skull!",
+	"Klingon function calls do not have 'parameters' - they have 'arguments' - and they ALWAYS WIN THEM.",
+	"Klingon multitasking systems do not support 'time-sharing'. When a Klingon program wants to run, it challenges the scheduler in hand-to-hand combat and owns the machine.",
+	"Klingon programs don't do accountancy. For that, you need a Farengi programmer.",
+	"Klingon software does NOT have BUGS. It has FEATURES, and those features are too sophisticated for a Romulan pig like you to understand.",
+	"Klingons do not believe in indentation - except perhaps in the skulls of their project managers.",
+	"Klingons do not make software 'releases'. Our software 'escapes'. Typically leaving a trail of wounded programmers in it's wake.",
+	"Microsoft is actually a secret Farengi-Klingon alliance designed to cripple the Federation. The Farengi are doing the marketing and the Klingons are writing the code.",
+	"My program has just dumped Stova Core!",
+	"Our competitors are without honor!",
+	"Our users will know fear and cower before our software! Ship it! Ship it and let them flee like the dogs they are! ",
+	"Perhaps it IS a good day to Die! I say we ship it!",
+	"Python? That is for children. A Klingon Warrior uses only machine code, keyed in on the front panel switches in raw binary. ",
+	"Specs are for the weak and timid!",
+	"This code is a piece of crap! You have no honor!",
+	"This machine is a piece of GAGH! I need dual Pentium processors if I am to do battle with this code!",
+	"What is this talk of 'release'? Klingons do not make software 'releases'. Our software 'escapes' leaving a bloody trail of designers and quality assurance people in its wake.",
+	"You cannot truly appreciate Dilbert unless you've read it in the original Klingon.",
+	"You humans call this thing a 'cursor' and you move it with 'mouse'! Bah! A Klingon would not use such a device. We have a Karaghht-Gnot - which is best translated as 'An Aiming Daggar of 16x16 pixels' and we move it using a Gshnarrrf which is a creature from the Klingon homeworld which posesses just one, (disproportionately large) testicle...which it rubs along the ground.....uh do we really need to talk about this?",
+	"You question the worthiness of my code? I should kill you where you stand!",
+]
+
+murphys_laws = [
+	"In any field of scientific endeavor, anything that can go wrong, will.",
+	"If the possibility exists of several things going wrong, the one that will go wrong is the one that will do the most damage.",
+	"Everything will go wrong at one time - That time is always when you least expect it.",
+	"If nothing can go wrong, something will.",
+	"Nothing is as easy as it looks.",
+	"Everything takes longer than you think.",
+	"Left to themselves, things always go from bad to worse.",
+	"Nature always sides with the hidden flaw.",
+	"Given the most inappropriate time for something to go wrong, that's when it will occur.",
+	"Mother Nature is a bitch.",
+	"The universe is not indifferent to intelligence, it is actively hostile to it.",
+	"If everything seems to be going well, you have obviously overlooked something.",
+	"If in any problem you find yourself doing an immense amount of work, the answer can be obtained by simple inspection.",
+	"Never make anything simple and efficient when a way can be found to make it complex and wonderful.",
+	"If it doesn't fit, use a bigger hammer.",
+	"In an instrument or device characterized by a number of plus-or-minus errors, the total error will be the sum of all the errors adding in the same direction.",
+	"In any given calculation, the fault will never be placed if more than one person is involved.",
+	"In any given discovery, the credit will never be properly placed if more than one person is involved.",
+	"All warranty and guarantee clauses become invalid upon payment of the final invoice.",
+	"If there are two or more ways to do something, and one of those ways can result in a catastrophe, then someone will do it.",
+	"Never attribute to malice that which can be adequately explained by stupidity.",
+	"Sufficiently advanced incompetence is indistinguishable from malice.",
+	"Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law.",
+	"Ninety percent of everything is crap",
+]
+
+def wchoice(d):
+	l = []
+	for item, num in d.iteritems():
+		l.extend([item] * (num*100))
+	return random.choice(l)
+
+def splitem(s):
+	s = s.rstrip('?.!')
+	if ':' in s:
+		question, choices = s.rsplit(':', 1)
+	else:
+		choices = s
+	
+	c = choices.split(',')
+	if ' or ' in c[-1]:
+		c = c[:-1] + c[-1].split(' or ')
+	c = map(str.strip, c)
+	c = filter(None, c)
+	return c
+
+class Karma():
+	def __init__(self, db):
+		self.db = db
+		CREATE_KARMA_VALUES_TABLE = '''
+			CREATE TABLE IF NOT EXISTS karma_values (karmaid INTEGER NOT NULL, karmavalue INTEGER, primary key (karmaid))
+		'''
+		CREATE_KARMA_KEYS_TABLE = '''
+			CREATE TABLE IF NOT EXISTS karma_keys (karmakey varchar, karmaid INTEGER, primary key (karmakey))
+		'''
+		CREATE_KARMA_LOG_TABLE = '''
+			CREATE TABLE IF NOT EXISTS karma_log (karmakey varchar, logid INTEGER, change INTEGER)
+		'''
+		db.execute(CREATE_KARMA_VALUES_TABLE)
+		db.execute(CREATE_KARMA_KEYS_TABLE)
+		db.execute(CREATE_KARMA_LOG_TABLE)
+		db.commit()
+		
+
+	def karmaLookup(self, thing):
+		thing = thing.strip().lower()
+		LOOKUP_SQL = 'SELECT karmavalue from karma_keys k join karma_values v on k.karmaid = v.karmaid where k.karmakey = ?'
+		try:
+			karma = self.db.execute(LOOKUP_SQL, [thing]).fetchone()[0] 
+		except:
+			karma = 0
+		if karma == None:
+			karma = 0
+		return karma
+
+	def karmaSet(self, thing, value):
+		thing = thing.strip().lower()
+		value = int(value)
+		UPDATE_SQL = 'UPDATE karma_values SET karmavalue = ? where karmaid = (select karmaid from karma_keys where karmakey = ?)'
+		res = self.db.execute(UPDATE_SQL, (value, thing))
+		if res.rowcount == 0:
+			INSERT_VALUE_SQL = 'INSERT INTO karma_values (karmavalue) VALUES (?)'
+			INSERT_KEY_SQL = 'INSERT INTO karma_keys (karmakey, karmaid) VALUES (?, ?)'
+			ins = self.db.execute(INSERT_VALUE_SQL, [value])
+			self.db.execute(INSERT_KEY_SQL, (thing, ins.lastrowid))
+		self.db.commit()
+
+	def karmaChange(self, thing, change):
+		thing = thing.strip().lower()
+		value = int(self.karmaLookup(thing)) + int(change)
+		log_id, log_message = self.db.execute('SELECT id, message FROM LOGS order by datetime desc limit 1').fetchone()
+		UPDATE_SQL = 'UPDATE karma_values SET karmavalue = ? where karmaid = (select karmaid from karma_keys where karmakey = ?)'
+		res = self.db.execute(UPDATE_SQL, (value, thing))
+		if res.rowcount == 0:
+			INSERT_VALUE_SQL = 'INSERT INTO karma_values (karmavalue) VALUES (?)'
+			INSERT_KEY_SQL = 'INSERT INTO karma_keys (karmakey, karmaid) VALUES (?, ?)'
+			ins = self.db.execute(INSERT_VALUE_SQL, [value])
+			self.db.execute(INSERT_KEY_SQL, (thing, ins.lastrowid))
+		if thing in log_message.lower():
+			self.db.execute('INSERT INTO karma_log (karmakey, logid, change) VALUES (?, ?, ?)', (thing, log_id, change))
+		self.db.commit()
+
+	def karmaList(self, select=0):
+		KARMIC_VALUES_SQL = 'SELECT karmaid, karmavalue from karma_values order by karmavalue desc'
+		KARMA_KEYS_SQL= 'SELECT karmakey from karma_keys where karmaid = ?'
+
+		karmalist = self.db.execute(KARMIC_VALUES_SQL).fetchall()
+		karmalist.sort(key=lambda x: int(x[1]), reverse=True)
+		if select > 0:
+			selected = karmalist[:select]
+		elif select < 0:
+			selected = karmalist[select:]
+		else:
+			selected = karmalist
+		keysandkarma = []
+		for karmaid, value in selected:
+			keys = [x[0] for x in self.db.execute(KARMA_KEYS_SQL, [karmaid])]
+			keysandkarma.append((keys, value))
+		return keysandkarma
+
+	def karmaLink(self, thing1, thing2):
+		t1 = thing1.strip().lower()
+		t2 = thing2.strip().lower()
+		GET_KARMAID_SQL = 'SELECT karmaid FROM karma_keys WHERE karmakey = ?'
+		try:
+			t1id = self.db.execute(GET_KARMAID_SQL, [t1]).fetchone()[0]
+		except TypeError:
+			raise KeyError
+		t1value = self.karmaLookup(t1)
+		try:
+			t2id = self.db.execute(GET_KARMAID_SQL, [t2]).fetchone()[0]
+		except TypeError:
+			raise KeyError
+		t2value = self.karmaLookup(t2)
+
+		newvalue = t1value + t2value
+		self.db.execute('UPDATE karma_keys SET karmaid = ? where karmaid = ?', (t1id, t2id)) #update the keys so t2 points to t1s value
+		self.db.execute('DELETE FROM karma_values WHERE karmaid = ?', (t2id,)) #drop the old value row for neatness
+		self.db.execute('UPDATE karma_values SET karmavalue = ? where karmaid = ?', (newvalue, t1id)) #set the new combined value
+		self.db.commit()
+
+		
+
+
+def karmaLookup(db, thing):
+	k = Karma(db)
+	return k.karmaLookup(thing)
+
+def karmaChange(db, thing, change):
+	k = Karma(db)
+	return k.karmaChange(thing, change)
+
+def karmaList(db, select=0):
+	k = Karma(db)
+	return k.karmaList(select)
+
+def karmaLink(db, thing1, thing2):
+	k = Karma(db)
+	return k.karmaLink(thing1, thing2)
+
+class Quotes():
+	def __init__(self, db, lib):
+		self.db = db
+		self.lib = lib
+		CREATE_QUOTES_TABLE = '''
+			CREATE TABLE IF NOT EXISTS quotes (quoteid INTEGER NOT NULL, library VARCHAR, quote TEXT, PRIMARY KEY (quoteid))
+		'''
+		CREATE_QUOTES_INDEX = '''CREATE INDEX IF NOT EXISTS ix_quotes_library on quotes(library)'''
+		CREATE_QUOTE_LOG_TABLE = '''
+			CREATE TABLE IF NOT EXISTS quote_log (quoteid varchar, logid INTEGER)
+		'''
+		db.execute(CREATE_QUOTES_TABLE)
+		db.execute(CREATE_QUOTES_INDEX)
+		db.execute(CREATE_QUOTE_LOG_TABLE)
+		db.commit()
+
+	def quoteLookupWNum(self, rest=''):
+		lib = self.lib
+		rest = rest.strip()
+		if rest:
+			if rest.split()[-1].isdigit():
+				num = rest.split()[-1]
+				query = ' '.join(rest.split()[:-1])
+				qt, i, n = self.quoteLookup(query, num)
+			else:
+				qt, i, n = self.quoteLookup(rest)
+		else:
+			qt, i, n = self.quoteLookup()
+		return qt, i, n
+
+	def quoteLookup(self, thing='', num=0):
+		lib = self.lib
+		BASE_SEARCH_SQL = 'SELECT quoteid, quote FROM quotes WHERE library = ? %s order by quoteid'
+		thing = thing.strip().lower()
+		num = int(num)
+		if thing:
+			SEARCH_SQL = BASE_SEARCH_SQL % (' AND %s' % (' AND '.join(["quote like '%%%s%%'" % x for x in thing.split()])))
+		else:
+			SEARCH_SQL = BASE_SEARCH_SQL % ''
+		results = [x[1] for x in self.db.execute(SEARCH_SQL, (lib,)).fetchall()]
+		n = len(results)
+		if n > 0:
+			if num:
+				i = num-1
+			else:
+				i = random.randrange(n)
+			quote = results[i]
+		else:
+			i = 0
+			quote = ''
+		return (quote, i+1, n)
+
+	def quoteAdd(self, quote):
+		lib = self.lib
+		quote = quote.strip()
+		ADD_QUOTE_SQL = 'INSERT INTO quotes (library, quote) VALUES (?, ?)'
+		res = self.db.execute(ADD_QUOTE_SQL, (lib, quote,))
+		quoteid = res.lastrowid
+		log_id, log_message = self.db.execute('SELECT id, message FROM LOGS order by datetime desc limit 1').fetchone()
+		if quote in log_message:
+			self.db.execute('INSERT INTO quote_log (quoteid, logid) VALUES (?, ?)', (quoteid, log_id))
+		self.db.commit()
+		
+def bartletts(db, lib, nick, qsearch):
+	qs = Quotes(db, lib)
+	qsearch = qsearch.strip()
+	if nick == 'pmxbot':
+		qt, i, n = qs.quoteLookup()
+		if qt:
+			if qt.find(':', 0, 15) > -1:
+				qt = qt.split(':', 1)[1].strip()
+			return qt
+	else:
+		qt, i, n = qs.quoteLookupWNum(qsearch)
+		if qt:
+			return '(%s/%s): %s' % (i, n, qt)
+
+
+
+def get_html(url):
+	h = httplib2.Http()
+	resp, html = h.request(url,
+	headers={'User-Agent' : 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b1) Gecko/20091014 Firefox/3.6b1 GTB5'})
+	assert 200 <= resp.status < 300
+	return html
+
+wiki_exp = re.compile(r"(.*?)en\.wikipedia\.org\/wiki\/", re.MULTILINE | re.DOTALL)
+def_exp = re.compile(r"<li>([^<]+)", re.MULTILINE)
+
+def lookup(word):
+	'''Gets a wikipedia summary for a word.
+	'''
+	word = urllib.quote_plus(word)
+	html = get_html('http://www.google.com/search?hl=en&client=firefox-a&q=define:%s' % word)
+	mo = wiki_exp.search(html)
+	if not mo:
+		return None
+	defs_sec = mo.group(1)
+	all_defs = list(def_exp.finditer(defs_sec))
+	show_def = all_defs[-1].group(1)
+	return show_def.strip()
+
+
+html_strip = re.compile(r'<[^>]+?>')
+NUM_ACS = 3
+
+def lookup_acronym(acronym):
+	acronym = acronym.strip().upper()
+	html = get_html('http://www.acronymfinder.com/%s.html' % acronym)
+	idx = html.find('<th>Meaning</th>')
+	if idx == -1:
+		return None
+	all = []
+	for x in xrange(NUM_ACS):
+		idx = html.find('%s</a>' % acronym, idx)
+		idx = html.find('<td>', idx)
+		edx = html.find('</td>', idx)
+		ans = html[idx+4:edx]
+		ans = html_strip.sub('', ans)
+		all.append(ans)
+		
+	return all
+
+# passive-aggresive statement generator
+adj_intros = [
+	'your %s is legendary',
+	'I love how your %s shows up in your work',
+	'dream big, because %s is going to pay off for you big-time',
+	'somehow your %s always manages to shine through',
+	'if only we all possessed your %s',
+	'you have rare %s',
+	'even if I tried, I couldn\'t replicate your %s',
+	'few can compete with your epic %s',
+	'I always stop and smile at the telltale %s when correcting your mistakes',
+]
+
+adjs = [
+	'incompetence',
+	'laziness',
+	'ignorance',
+	'frailty',
+	'lack of attention to detail',
+	'BO',
+	'stupidity',
+	'lack of personality',
+	'clever decision',
+	'ability to "introcude a failure"',
+]
+
+farewells = [
+	'Hugs and kisses',
+	'Keep up the good work',
+	'Chin up',
+	'I hope you rot',
+	'Have a great day',
+	'Must get back to work',
+	'Thanks for everything',
+	'Your BFF',
+	'Don\'t ever change',
+	'Have a nice life',
+	'Don\' stop bragplaining',
+]
+
+direct_apologies = [
+	"%(a)s profusely apologizes to %(b)s",
+	"%(a)s sincerely apologizes to %(b)s",
+	"%(a)s would like to apologize to %(b)s for any physical, emotional, or mental anguish %(a)s's action, justified as they may have been, caused.",
+	"%(a)s would like to apologize to %(b)s for any physical, emotional, or mental anguish %(a)s's action, caused.",
+	"%(b)s: %(a)s is like sorry or something.",
+]
+
+apologies = [
+	"%(a)s is sorry.",
+	"%(a)s would like to tearfully apologize to everyone in a widely publicized press conference.",
+	"%(a)s profusely apologizes.",
+	"%(a)s sincerely apologizes.",
+	"%(a)s would like to apologize for any physical, emotional, or mental anguish that %(a)s's actions may have caused.",
+	"%(a)s apologizes and would like to point out there is no reason legal action to be taken.",
+	"%(a)s is sorry or something.",
+]
+
+def passagg(recipient='', sender=''):
+	adj = random.choice(adjs)
+	if random.randint(0,1):
+		lead = ""
+		trail=recipient if not recipient else ", %s" % recipient
+	else:
+		lead=recipient if not recipient else "%s, " % recipient
+		trail=""
+	start = "%s%s%s." % (lead, random.choice(adj_intros) % adj, trail)