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.
This commit is contained in:
parent
b7da0dae69
commit
eb6357a3b8
7 changed files with 258 additions and 94 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
|||
*.pyc
|
||||
*.log
|
||||
/lolbot.db
|
||||
/lolbot.conf
|
||||
|
|
|
|||
53
botcommon.py
53
botcommon.py
|
|
@ -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 <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()
|
||||
24
ircbot.py
24
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.
|
||||
|
|
|
|||
2
lolbot
2
lolbot
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
/usr/bin/python lolbot.py irc.freenode.net "#zomglol" lolbot
|
||||
6
lolbot.conf.sample
Normal file
6
lolbot.conf.sample
Normal file
|
|
@ -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
|
||||
|
||||
245
lolbot.py
Normal file → Executable file
245
lolbot.py
Normal file → Executable file
|
|
@ -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; <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.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=<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__":
|
||||
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."
|
||||
|
||||
|
|
|
|||
20
runbot
20
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 &
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue