Commit eb6357a3 authored by Jonathan Harker's avatar Jonathan Harker

Now uses sqlalchemy for persistent storage and logging.

 * Uses sqlalchemy declarative schema for the log and saved URLs.
 * runbot now not using nohup. Will need to be daemonised somehow.
 * Refactored botcommon.py out of existence.
 * Now reads from a config file which can optionally be specified at the
   command line with --config option.
 * Prints usage with --help or if no config file is found.
parent b7da0dae
*.pyc *.pyc
*.log *.log
/lolbot.db
/lolbot.conf
"""\
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 <server[:port]> <channel> <nickname>" % 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()
...@@ -31,6 +31,30 @@ from irclib import SimpleIRCClient ...@@ -31,6 +31,30 @@ from irclib import SimpleIRCClient
from irclib import nm_to_n, irc_lower, all_events from irclib import nm_to_n, irc_lower, all_events
from irclib import parse_channel_modes, is_channel from irclib import parse_channel_modes, is_channel
from irclib import ServerConnectionError 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): class SingleServerIRCBot(SimpleIRCClient):
"""A single-server IRC bot class. """A single-server IRC bot class.
......
#!/bin/sh
/usr/bin/python lolbot.py irc.freenode.net "#zomglol" lolbot
irc.server = irc.freenode.net
irc.channel = #lolbottest
# db.url can be any URL that works with SQLAlchemy
db.url = sqlite:///lolbot.db
...@@ -11,14 +11,18 @@ Logs a channel and collects URLs for later. ...@@ -11,14 +11,18 @@ Logs a channel and collects URLs for later.
""" """
import sys, string, random, time 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 from irclib import nm_to_n, nm_to_h, irc_lower
import botcommon
import os import os
from datetime import datetime from datetime import datetime
from mechanize import Browser 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 - wrong input
exclamations = [ exclamations = [
"Zing!", "Zing!",
...@@ -38,37 +42,170 @@ ponderings = [ ...@@ -38,37 +42,170 @@ ponderings = [
"No it's a week night 8pm is past my bedtime.", "No it's a week night 8pm is past my bedtime.",
] ]
class LolBot(SingleServerIRCBot): SqlBase = declarative_base()
def __init__(self, channel, nickname, server, port):
SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname) 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)
self.channel = channel def __init__(self, nickname, text, timestamp=None):
if timestamp is None:
timestamp = datetime.now()
self.timestamp = timestamp
self.nickname = nickname 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; <url> - adds the URL to the list; help - this message." self.helptext = "Adds URLs to a list. Commands: list - prints a bunch of URLs; clear - clears the list; lol - say something funny; <url> - adds the URL to the list; help - this message."
self.queue = botcommon.OutputManager(self.connection) self.queue = OutputManager(self.connection)
self.queue.start() self.queue.start()
self.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: try:
br = Browser() self.port = int(config["irc.port"])
br.open(url) except ValueError:
title = br.title() 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."
except Exception as ex: sys.exit(1)
title = ''
self.urls[url] = title # validate IRC channel
return title 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): def now(self):
return datetime.today().strftime("%Y-%m-%d %H:%M:%S") return datetime.today().strftime("%Y-%m-%d %H:%M:%S")
def log_event(self, event): def save_url(self, nickname, url):
nick = nm_to_n(event.source()) theurl = Url(nickname, url)
text = event.arguments()[0] db = self.get_session()
print "(%s) %s: %s" % (self.now(), nick, text) 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): def on_nicknameinuse(self, connection, event):
self.nickname = connection.get_nickname() + "_" self.nickname = connection.get_nickname() + "_"
...@@ -86,9 +223,9 @@ class LolBot(SingleServerIRCBot): ...@@ -86,9 +223,9 @@ class LolBot(SingleServerIRCBot):
"Deal with a public message in a channel." "Deal with a public message in a channel."
# log it # log it
self.log_event(event)
from_nick = nm_to_n(event.source()) from_nick = nm_to_n(event.source())
self.log_event(from_nick, event.arguments()[0])
args = string.split(event.arguments()[0], ":", 1) args = string.split(event.arguments()[0], ":", 1)
if len(args) > 1 and irc_lower(args[0]) == irc_lower(self.nickname): if len(args) > 1 and irc_lower(args[0]) == irc_lower(self.nickname):
self.do_command(event, string.strip(args[1]), from_nick) self.do_command(event, string.strip(args[1]), from_nick)
...@@ -97,13 +234,13 @@ class LolBot(SingleServerIRCBot): ...@@ -97,13 +234,13 @@ class LolBot(SingleServerIRCBot):
words = event.arguments()[0].split(" ") words = event.arguments()[0].split(" ")
for w in words: for w in words:
if w.startswith('http://') or w.startswith('https://'): if w.startswith('http://') or w.startswith('https://'):
title = self.save_url(w) title = self.save_url(from_nick, w)
self.say_public(title) self.say_public(title)
def say_public(self, text): def say_public(self, text):
"Print TEXT into public channel, for all to see." "Print TEXT into public channel, for all to see."
self.queue.send(text, self.channel) 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): def say_private(self, nick, text):
"Send private message of TEXT to NICK." "Send private message of TEXT to NICK."
...@@ -142,30 +279,72 @@ class LolBot(SingleServerIRCBot): ...@@ -142,30 +279,72 @@ class LolBot(SingleServerIRCBot):
self.reply(self.ponder(), target) self.reply(self.ponder(), target)
elif cmd == 'urls' or cmd == 'list': elif cmd == 'urls' or cmd == 'list':
for url, title in self.urls.items(): db = self.get_session()
line = "%s %s" % (url, title) for url in db.query(Url).order_by(Url.timestamp):
line = "%s %s" % (url.url, url.title)
self.reply(line, target) self.reply(line, target)
time.sleep(1) time.sleep(1)
elif cmd.startswith('http:') or cmd.startswith('https:'): elif cmd.startswith('http:') or cmd.startswith('https:'):
title = self.save_url(cmd) title = self.save_url(from_private, cmd)
if title == '': if title == '':
self.reply('URL added.', target) self.reply('URL added.', target)
if title != '': if title != '':
self.reply('URL added: %s' % title, target) self.reply('URL added: %s' % title, target)
elif cmd == 'clear':
del self.urls
self.urls = {}
self.reply('URLs cleared.', target)
else: else:
self.reply(self.exclaim(), target) self.reply(self.exclaim(), target)
def usage():
print """Run a lolbot.
-h, --help
This message.
-c, --config=<filename>
Specify a configuration file. Defaults to lolbot.conf in the same
directory as the script.
Configuration:
irc.server = <host>
The IRC server, e.g. irc.freenode.net
irc.port = <port>
The IRC server port. Default: 6667
irc.channel = <channel>
The chat channel to join, e.g. #trainspotting
irc.nickname = <nickname>
The nickname for your lolbot. Default: "lolbot"
db.url = <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__": if __name__ == "__main__":
try: 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: except KeyboardInterrupt:
print "Shutting down." print "Shutting down."
#!/bin/sh #!/bin/sh
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
SERVERS=`ps -ef|grep 'lolbot.py'|grep -v grep|awk '{print $2}'`
if [ "$SERVERS" != "" ]; then NOW=`date +%Y-%m-%d_%H%M`
kill $SERVERS if [ "x$1" = "x" ]; then
CONFIG=""
else
CONFIG="--config $1"
fi
LOLBOTS=`ps -ef|grep 'lolbot.py'|grep -v grep|awk '{print $2}'`
if [ "$LOLBOTS" != "" ]; then
kill $LOLBOTS
fi fi
APP_PATH=/home/johnno/projects/lolbot cd `dirname $0`
cd /tmp APP_PATH=`pwd`
nohup /usr/bin/python $APP_PATH/lolbot.py irc.freenode.net "#zomglol" lolbot > $APP_PATH/server.log 2> $APP_PATH/error.log & /usr/bin/python $APP_PATH/lolbot.py $CONFIG > server-$NOW.log 2> error-$NOW.log &
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment