diff --git a/.gitignore b/.gitignore index 83658ec..a79e2ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.pyc *.log +/lolbot.db +/lolbot.conf diff --git a/botcommon.py b/botcommon.py deleted file mode 100644 index 1476ca5..0000000 --- a/botcommon.py +++ /dev/null @@ -1,53 +0,0 @@ -"""\ -Common bits and pieces used by the various bots. -""" - -import sys -import os -import time -from threading import Thread, Event - - -class OutputManager(Thread): - def __init__(self, connection, delay=.5): - Thread.__init__(self) - self.setDaemon(1) - self.connection = connection - self.delay = delay - self.event = Event() - self.queue = [] - - def run(self): - while 1: - self.event.wait() - while self.queue: - msg,target = self.queue.pop(0) - self.connection.privmsg(target, msg) - time.sleep(self.delay) - self.event.clear() - - def send(self, msg, target): - self.queue.append((msg.strip(),target)) - self.event.set() - - -def trivial_bot_main(klass): - if len(sys.argv) != 4: - botname = os.path.basename(sys.argv[0]) - print "Usage: %s " % botname - sys.exit(1) - - s = sys.argv[1].split(":", 1) - server = s[0] - if len(s) == 2: - try: - port = int(s[1]) - except ValueError: - print "Error: Erroneous port." - sys.exit(1) - else: - port = 6667 - channel = sys.argv[2] - nickname = sys.argv[3] - - klass(channel, nickname, server, port).start() diff --git a/ircbot.py b/ircbot.py index ad47981..6463396 100644 --- a/ircbot.py +++ b/ircbot.py @@ -31,6 +31,30 @@ from irclib import SimpleIRCClient from irclib import nm_to_n, irc_lower, all_events from irclib import parse_channel_modes, is_channel from irclib import ServerConnectionError +import threading +import time + +class OutputManager(threading.Thread): + def __init__(self, connection, delay=.5): + threading.Thread.__init__(self) + self.setDaemon(1) + self.connection = connection + self.delay = delay + self.event = threading.Event() + self.queue = [] + + def run(self): + while 1: + self.event.wait() + while self.queue: + msg,target = self.queue.pop(0) + self.connection.privmsg(target, msg) + time.sleep(self.delay) + self.event.clear() + + def send(self, msg, target): + self.queue.append((msg.strip(),target)) + self.event.set() class SingleServerIRCBot(SimpleIRCClient): """A single-server IRC bot class. diff --git a/lolbot b/lolbot deleted file mode 100755 index d621979..0000000 --- a/lolbot +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -/usr/bin/python lolbot.py irc.freenode.net "#zomglol" lolbot diff --git a/lolbot.conf.sample b/lolbot.conf.sample new file mode 100644 index 0000000..1181e42 --- /dev/null +++ b/lolbot.conf.sample @@ -0,0 +1,6 @@ +irc.server = irc.freenode.net +irc.channel = #lolbottest + +# db.url can be any URL that works with SQLAlchemy +db.url = sqlite:///lolbot.db + diff --git a/lolbot.py b/lolbot.py old mode 100644 new mode 100755 index 8ca0d68..a83bf69 --- a/lolbot.py +++ b/lolbot.py @@ -11,14 +11,18 @@ Logs a channel and collects URLs for later. """ import sys, string, random, time -from ircbot import SingleServerIRCBot +from ircbot import SingleServerIRCBot, OutputManager from irclib import nm_to_n, nm_to_h, irc_lower -import botcommon import os from datetime import datetime from mechanize import Browser +import getopt +from sqlalchemy import MetaData, Table, Column, String, Text, Integer, DateTime, engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + # Exclamations - wrong input exclamations = [ "Zing!", @@ -38,37 +42,170 @@ ponderings = [ "No it's a week night 8pm is past my bedtime.", ] -class LolBot(SingleServerIRCBot): - def __init__(self, channel, nickname, server, port): - SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname) +SqlBase = declarative_base() - self.channel = channel +class Log(SqlBase): + """ + This class represents an event in the log table and inherits from a SQLAlchemy + convenience ORM class. + """ + + __tablename__ = "log" + + id = Column(Integer, primary_key=True) + timestamp = Column(DateTime) + nickname = Column(String(20)) + text = Column(Text) + + def __init__(self, nickname, text, timestamp=None): + if timestamp is None: + timestamp = datetime.now() + self.timestamp = timestamp self.nickname = nickname - self.urls = {} + self.text = text + + def __repr__(self): + return "(%s) %s: %s" % (self.timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.nickname, self.text) + +class Url(SqlBase): + """ + This class represents a saved URL and inherits from a SQLAlchemy convenience + ORM class. + """ + + __tablename__ = "url" + + id = Column(Integer, primary_key=True) + timestamp = Column(DateTime) + nickname = Column(String(20)) + url = Column(String(200)) + title = Column(Text) + + def __init__(self, nickname, url, title=None, timestamp=None): + if timestamp is None: + timestamp = datetime.now() + self.timestamp = timestamp + self.nickname = nickname + self.url = url + self.title = title + + # populate the title from the URL if not given. + if title is None: + try: + br = Browser() + br.open(self.url) + self.title = br.title() + except Exception as ex: + self.title = '' + + def __repr__(self): + if not self.title: + return "%s: %s" % (self.nickname, self.url) + else: + return "%s: %s - %s" % (self.nickname, self.url, self.title) + +class LolBot(SingleServerIRCBot): + """ + The Lolbot itself. + """ + + def __init__(self, config_path=''): + self.get_config(config_path) + SingleServerIRCBot.__init__(self, [(self.server, self.port)], self.nickname, self.nickname) + + # connect to the database + self.dbengine = engine_from_config(self.config, prefix="db.") + SqlBase.metadata.bind = self.dbengine + SqlBase.metadata.create_all() + self.get_session = sessionmaker(bind=self.dbengine) self.helptext = "Adds URLs to a list. Commands: list - prints a bunch of URLs; clear - clears the list; lol - say something funny; - adds the URL to the list; help - this message." - self.queue = botcommon.OutputManager(self.connection) + self.queue = OutputManager(self.connection) self.queue.start() self.start() - def save_url(self, url): + def get_config(self, config_path): + """ + This method loads configuration options from a lolbot.conf file. The file + should look like this: + + irc.server = irc.freenode.net + irc.port = 6667 + irc.channel = #lolbottest + irc.nickname = lolbot + db.url = sqlite:///lolbot.db + + """ + + if not config_path: + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lolbot.conf") + if not os.path.exists(config_path): + print "Error: configuration file not found. By default lolbot will look for a lolbot.conf file in the same directory as the lolbot script, but you can override this by specifying a path on the command line with the --config option." + usage() + sys.exit(1) + + # open the configuration file and grab all param=value declarations. + config = {} + with open(config_path) as f: + for line in f: + # skip comments + if line.strip().startswith("#") or line.strip() == "": + continue + + # collect up param = value + try: + (param, value) = line.strip().split("=", 1) + if param.strip() != "": + config[param.strip()] = value.strip() + except ValueError: + continue + + # validate IRC host + if "irc.server" not in config.keys(): + print "Error: the IRC server was not specified. Use --help for more information." + sys.exit(1) + self.server = config["irc.server"] + + # validate IRC port + if "irc.port" not in config.keys(): + config["irc.port"] = "6667" try: - br = Browser() - br.open(url) - title = br.title() - except Exception as ex: - title = '' - self.urls[url] = title - return title + self.port = int(config["irc.port"]) + except ValueError: + print "Error: the IRC port must be an integer. If not specified, lolbot will use the default IRC port value 6667. Use --help for more information." + sys.exit(1) + + # validate IRC channel + if "irc.channel" not in config.keys() or not config["irc.channel"].startswith("#"): + print "Error: the IRC channel is not specified or incorrect. It must begin with a # - e.g. #mychatchannel. Use --help for more information." + sys.exit(1) + self.channel = config["irc.channel"] + + # validate bot nickname + if "irc.nickname" not in config.keys(): + config["irc.nickname"] = "lolbot" + self.nickname = config["irc.nickname"] + + self.config = config def now(self): return datetime.today().strftime("%Y-%m-%d %H:%M:%S") - def log_event(self, event): - nick = nm_to_n(event.source()) - text = event.arguments()[0] - print "(%s) %s: %s" % (self.now(), nick, text) + def save_url(self, nickname, url): + theurl = Url(nickname, url) + db = self.get_session() + db.add(theurl) + db.commit() + print theurl + return theurl.title + + def log_event(self, nick, text): + entry = Log(nick, text) + db = self.get_session() + db.add(entry) + db.commit() + print entry def on_nicknameinuse(self, connection, event): self.nickname = connection.get_nickname() + "_" @@ -86,9 +223,9 @@ class LolBot(SingleServerIRCBot): "Deal with a public message in a channel." # log it - self.log_event(event) - from_nick = nm_to_n(event.source()) + self.log_event(from_nick, event.arguments()[0]) + args = string.split(event.arguments()[0], ":", 1) if len(args) > 1 and irc_lower(args[0]) == irc_lower(self.nickname): self.do_command(event, string.strip(args[1]), from_nick) @@ -97,13 +234,13 @@ class LolBot(SingleServerIRCBot): words = event.arguments()[0].split(" ") for w in words: if w.startswith('http://') or w.startswith('https://'): - title = self.save_url(w) + title = self.save_url(from_nick, w) self.say_public(title) def say_public(self, text): "Print TEXT into public channel, for all to see." self.queue.send(text, self.channel) - print "(%s) %s: %s" % (self.now(), self.nickname, text) + self.log_event(self.nickname, text) def say_private(self, nick, text): "Send private message of TEXT to NICK." @@ -142,30 +279,72 @@ class LolBot(SingleServerIRCBot): self.reply(self.ponder(), target) elif cmd == 'urls' or cmd == 'list': - for url, title in self.urls.items(): - line = "%s %s" % (url, title) + db = self.get_session() + for url in db.query(Url).order_by(Url.timestamp): + line = "%s %s" % (url.url, url.title) self.reply(line, target) time.sleep(1) elif cmd.startswith('http:') or cmd.startswith('https:'): - title = self.save_url(cmd) + title = self.save_url(from_private, cmd) if title == '': self.reply('URL added.', target) if title != '': self.reply('URL added: %s' % title, target) - elif cmd == 'clear': - del self.urls - self.urls = {} - self.reply('URLs cleared.', target) - else: self.reply(self.exclaim(), target) +def usage(): + print """Run a lolbot. + + -h, --help + This message. + + -c, --config= + Specify a configuration file. Defaults to lolbot.conf in the same + directory as the script. + +Configuration: + + irc.server = + The IRC server, e.g. irc.freenode.net + + irc.port = + The IRC server port. Default: 6667 + + irc.channel = + The chat channel to join, e.g. #trainspotting + + irc.nickname = + The nickname for your lolbot. Default: "lolbot" + + db.url = + The URL to your lolbot database. This can be any valid URL accepted + by SQL Alchemy. If not specified, lolbot will attempt to use a + SQLite database called lolbot.db in the same directory as the + script. +""" + if __name__ == "__main__": try: - botcommon.trivial_bot_main(LolBot) + (options, args) = getopt.getopt(sys.argv[1:], "hc:", ["help", "config=", ]) + except getopt.GetoptError, err: + print str(err) + usage() + sys.exit(2) + + config_path = "" + for option,value in options: + if option in ("-h", "--help"): + usage() + sys.exit(2) + if option in ("-c", "--config"): + config_path = value + + try: + LolBot(config_path).start() except KeyboardInterrupt: print "Shutting down." diff --git a/runbot b/runbot index acbeb78..e65753c 100755 --- a/runbot +++ b/runbot @@ -1,12 +1,20 @@ #!/bin/sh PATH=/bin:/usr/bin -SERVERS=`ps -ef|grep 'lolbot.py'|grep -v grep|awk '{print $2}'` -if [ "$SERVERS" != "" ]; then - kill $SERVERS + +NOW=`date +%Y-%m-%d_%H%M` +if [ "x$1" = "x" ]; then + CONFIG="" +else + CONFIG="--config $1" fi -APP_PATH=/home/johnno/projects/lolbot -cd /tmp -nohup /usr/bin/python $APP_PATH/lolbot.py irc.freenode.net "#zomglol" lolbot > $APP_PATH/server.log 2> $APP_PATH/error.log & +LOLBOTS=`ps -ef|grep 'lolbot.py'|grep -v grep|awk '{print $2}'` +if [ "$LOLBOTS" != "" ]; then + kill $LOLBOTS +fi + +cd `dirname $0` +APP_PATH=`pwd` +/usr/bin/python $APP_PATH/lolbot.py $CONFIG > server-$NOW.log 2> error-$NOW.log &