actually i need to call this ircbot
so i don't collide with the django dr_botzo
This commit is contained in:
814
ircbot/DrBotIRC.py
Normal file
814
ircbot/DrBotIRC.py
Normal file
@@ -0,0 +1,814 @@
|
||||
"""
|
||||
DrBotIRC - customizations of irclib, for dr.botzo
|
||||
Copyright (C) 2011 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
import bisect
|
||||
import copy
|
||||
from ConfigParser import NoOptionError, NoSectionError
|
||||
import logging
|
||||
import re
|
||||
import signal
|
||||
from SimpleXMLRPCServer import SimpleXMLRPCServer
|
||||
import socket
|
||||
import sys
|
||||
import thread
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
|
||||
log = logging.getLogger('drbotzo')
|
||||
|
||||
|
||||
class DrBotzoMethods:
|
||||
|
||||
"""Methods to expose to the XML-RPC server."""
|
||||
|
||||
def __init__(self, irc):
|
||||
"""Store the same stuff the core module would, since we'll probably
|
||||
need it.
|
||||
|
||||
Args:
|
||||
irc the irc instance to save a reference to
|
||||
|
||||
"""
|
||||
|
||||
self.irc = irc
|
||||
|
||||
def say(self, target, message):
|
||||
"""Say a message in a channel/to a nick.
|
||||
|
||||
Args:
|
||||
target the nick/channel to privmsg
|
||||
message the message to send
|
||||
|
||||
Returns:
|
||||
"OK", since there's no other feedback to give.
|
||||
|
||||
"""
|
||||
|
||||
self.irc.server.privmsg(target, message)
|
||||
return "OK"
|
||||
|
||||
def execute_module_method(self, modname, method, args):
|
||||
"""Execute the method (with arguments) of the specified module.
|
||||
|
||||
Args:
|
||||
modname the loaded module to retrieve
|
||||
method the method to call from that module
|
||||
args the arguments to provide to the method as a tuple
|
||||
|
||||
Returns:
|
||||
An error string, or the outcome of the method call.
|
||||
|
||||
"""
|
||||
|
||||
for module in self.irc.modlist:
|
||||
if modname == module.__class__.__name__:
|
||||
try:
|
||||
func = getattr(module, method)
|
||||
except AttributeError:
|
||||
return ("couldn't find '{0:s}' in found module "
|
||||
"'{1:s}'".format(method, modname))
|
||||
|
||||
if hasattr(func, '__call__'):
|
||||
return func(*args)
|
||||
else:
|
||||
return ("'{0:s}' in found module '{1:s}' is not "
|
||||
"callable".format(method, modname))
|
||||
|
||||
return "couldn't find module '{0:s}'".format(modname)
|
||||
|
||||
class DrBotServerConnection(irclib.ServerConnection):
|
||||
|
||||
"""Subclass irclib's ServerConnection, in order to expand privmsg."""
|
||||
|
||||
nickmask = None
|
||||
|
||||
def __init__(self, irclibobj, nickname=None, username=None):
|
||||
"""Instantiate the server connection.
|
||||
|
||||
Also start guessing at the nickmask and get ready to do on_welcome
|
||||
stuff.
|
||||
|
||||
Args:
|
||||
irclibobj the irclib instance to connect with
|
||||
nickname the nickname to use in nickmask guess
|
||||
username the username to use in nickmask guess
|
||||
|
||||
"""
|
||||
|
||||
irclib.ServerConnection.__init__(self, irclibobj)
|
||||
|
||||
# temporary. hopefully on_welcome() will set this, but this should be
|
||||
# a pretty good guess if not
|
||||
nick = nickname
|
||||
user = username if username is not None else nick
|
||||
host = socket.getfqdn()
|
||||
self.nickmask = "{0:s}!~{1:s}@{2:s}".format(nick, user, host)
|
||||
log.debug("guessing at nickmask '{0:s}'".format(self.nickmask))
|
||||
|
||||
self.add_global_handler('welcome', self.on_welcome, 1)
|
||||
|
||||
def on_welcome(self, connection, event):
|
||||
"""Set the nickmask that the ircd tells us is us.
|
||||
|
||||
Args:
|
||||
connection source connection
|
||||
event incoming event
|
||||
|
||||
"""
|
||||
|
||||
what = event.arguments()[0]
|
||||
|
||||
log.debug("welcome: {0:s}".format(what))
|
||||
|
||||
match = re.search('(\S+!\S+@\S+)', what)
|
||||
if match:
|
||||
self.nickmask = match.group(1)
|
||||
log.debug("setting nickmask: {0:s}".format(self.nickmask))
|
||||
|
||||
def privmsg(self, target, text):
|
||||
"""Send a PRIVMSG command.
|
||||
|
||||
Args:
|
||||
target the destination nick/channel
|
||||
text the message to send
|
||||
|
||||
"""
|
||||
|
||||
log.debug("OUTGOING PRIVMSG: t[{0:s}] m[{1:s}]".format(target, text))
|
||||
|
||||
splitter = "..."
|
||||
|
||||
# split messages that are too long. Max length is 512.
|
||||
# TODO: this does not properly handle when the nickmask has been
|
||||
# masked by the ircd
|
||||
# is the above still the case?
|
||||
space = 512 - len('\r\n') - len(' PRIVMSG ') - len(target) - len(' :') - len(self.nickmask) - len(' :')
|
||||
splitspace = space - (len(splitter) + 1)
|
||||
|
||||
if len(text) > space:
|
||||
times = 1
|
||||
|
||||
while len(text) > splitspace:
|
||||
splitpos = text.rfind(' ', 0, splitspace)
|
||||
splittext = text[0:splitpos] + ' ' + splitter
|
||||
text = splitter + ' ' + text[splitpos+1:]
|
||||
self.send_raw("PRIVMSG {0:s} :{1:s}".format(target, splittext))
|
||||
|
||||
times = times + 1
|
||||
if times >= 4:
|
||||
# this is stupidly long, abort
|
||||
return
|
||||
|
||||
# done splitting
|
||||
self.send_raw("PRIVMSG {0:s} :{1:s}".format(target, text))
|
||||
else:
|
||||
self.send_raw("PRIVMSG {0:s} :{1:s}".format(target, text))
|
||||
|
||||
class DrBotIRC(irclib.IRC):
|
||||
|
||||
"""Implement a customized irclib IRC."""
|
||||
|
||||
modlist = []
|
||||
config = None
|
||||
server = None
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize XML-RPC interface and save references.
|
||||
|
||||
Args:
|
||||
config the config structure to load stuff from
|
||||
|
||||
"""
|
||||
irclib.IRC.__init__(self)
|
||||
|
||||
self.config = config
|
||||
self.xmlrpc = None
|
||||
|
||||
self.regex_handlers = dict()
|
||||
|
||||
# handle SIGINT
|
||||
signal.signal(signal.SIGINT, self.sigint_handler)
|
||||
|
||||
# load XML-RPC server
|
||||
try:
|
||||
if self.config.has_section('XMLRPC'):
|
||||
host = self.config.get('XMLRPC', 'host')
|
||||
port = self.config.getint('XMLRPC', 'port')
|
||||
if host and port:
|
||||
self.funcs = DrBotzoMethods(self)
|
||||
|
||||
self.xmlrpc = SimpleXMLRPCServer((host, port), logRequests=False)
|
||||
self.xmlrpc.register_introspection_functions()
|
||||
self.xmlrpc.register_instance(self.funcs)
|
||||
|
||||
thread.start_new_thread(self._xmlrpc_listen, ())
|
||||
except NoOptionError: pass
|
||||
|
||||
def server(self):
|
||||
"""Create a DrBotServerConnection.
|
||||
|
||||
Returns:
|
||||
The newly created DrBotServerConnection.
|
||||
|
||||
"""
|
||||
|
||||
# get the nick and user name so we can provide it to
|
||||
# DrBotServerConnection as a hint for the nickmask
|
||||
user = None
|
||||
nick = None
|
||||
try:
|
||||
if self.config.has_section('dr.botzo'):
|
||||
user = self.config.get('dr.botzo', 'user')
|
||||
nick = self.config.get('dr.botzo', 'nick')
|
||||
except NoOptionError: pass
|
||||
|
||||
self.server = DrBotServerConnection(self, user, nick)
|
||||
|
||||
# though we only handle one connection, the underlying library supports
|
||||
# multiple. append the new one though we intend no others
|
||||
self.connections.append(self.server)
|
||||
|
||||
return self.server
|
||||
|
||||
def add_global_regex_handler(self, event, regex, handler, priority=0):
|
||||
"""Adds a global handler function for a specific event type and regex.
|
||||
|
||||
The handler function is called whenever the specified event is
|
||||
triggered in any of the connections and the regex matches.
|
||||
See documentation for the Event class.
|
||||
|
||||
The handler functions are called in priority order (lowest
|
||||
number is highest priority). If a handler function returns
|
||||
"NO MORE", no more handlers will be called.
|
||||
|
||||
This is basically an extension of add_global_handler(), either may
|
||||
work, though it turns out most modules probably want this one.
|
||||
|
||||
The provided method should take as arguments:
|
||||
* nick the nick creating the event, or None
|
||||
* userhost the userhost creating the event, or None
|
||||
* event the raw IRC event, which may be None
|
||||
* from_admin whether or not the event came from an admin
|
||||
* groups list of match.groups(), from re.search()
|
||||
|
||||
Args:
|
||||
event event type (a string), as in numeric_events
|
||||
regex regex string to match before doing callback invocation
|
||||
handler callback function to invoke
|
||||
priority integer, the lower number, the higher priority
|
||||
|
||||
"""
|
||||
|
||||
if not event in self.regex_handlers:
|
||||
self.regex_handlers[event] = []
|
||||
bisect.insort(self.regex_handlers[event], ((priority, regex, handler)))
|
||||
|
||||
def remove_global_regex_handler(self, event, regex, handler):
|
||||
"""Removes a global regex handler function.
|
||||
|
||||
Args:
|
||||
event event type (a string), as in numeric_events
|
||||
regex regex string that was used in matching
|
||||
handler callback function to remove
|
||||
|
||||
Returns:
|
||||
1 on success, otherwise 0.
|
||||
|
||||
"""
|
||||
|
||||
if not event in self.regex_handlers:
|
||||
return 0
|
||||
|
||||
for h in self.regex_handlers[event]:
|
||||
if regex == h[1] and handler == h[2]:
|
||||
self.regex_handlers[event].remove(h)
|
||||
return 1
|
||||
|
||||
def _handle_event(self, connection, event):
|
||||
"""Override event handler to do recursion and regex checking.
|
||||
|
||||
Args:
|
||||
connection source connection
|
||||
event incoming event
|
||||
|
||||
"""
|
||||
|
||||
log.debug("EVENT: e[{0:s}] s[{1:s}] t[{2:s}] "
|
||||
"a[{3:s}]".format(event.eventtype(), event.source(),
|
||||
event.target(), event.arguments()))
|
||||
|
||||
try:
|
||||
nick = irclib.nm_to_n(event.source())
|
||||
except (IndexError, AttributeError):
|
||||
nick = ''
|
||||
|
||||
try:
|
||||
if self.config.has_section('Ignore'):
|
||||
alias = self.config.get('Ignore', nick.lower())
|
||||
if alias:
|
||||
log.debug("ignoring {0:s} as per config file".format(nick))
|
||||
return
|
||||
except NoOptionError: pass
|
||||
|
||||
self.try_alias_cmds(connection, event)
|
||||
self.try_recursion(connection, event)
|
||||
self.try_alias(connection, event)
|
||||
|
||||
nick = None
|
||||
userhost = None
|
||||
admin = False
|
||||
|
||||
if event.source() is not None:
|
||||
nick = irclib.nm_to_n(event.source())
|
||||
try:
|
||||
userhost = irclib.nm_to_uh(event.source())
|
||||
except IndexError: pass
|
||||
|
||||
try:
|
||||
if userhost == self.config.get('dr.botzo', 'admin_userhost'):
|
||||
admin = True
|
||||
except NoOptionError: pass
|
||||
|
||||
# try regex handlers first, since they're more specific
|
||||
rh = self.regex_handlers
|
||||
trh = sorted(rh.get('all_events', []) + rh.get(event.eventtype(), []))
|
||||
for handler in trh:
|
||||
try:
|
||||
prio, regex, method = handler
|
||||
for line in event.arguments():
|
||||
match = re.search(regex, line)
|
||||
if match:
|
||||
log.debug("pattern matched, calling "
|
||||
"{0:s}".format(method))
|
||||
# pattern matched, call method with pattern groups
|
||||
ret = method(nick, userhost, event, admin,
|
||||
match.groups())
|
||||
if ret == 'NO MORE':
|
||||
return
|
||||
except Exception as e:
|
||||
log.error("exception floated up to DrBotIrc!")
|
||||
log.exception(e)
|
||||
|
||||
h = self.handlers
|
||||
th = sorted(h.get('all_events', []) + h.get(event.eventtype(), []))
|
||||
for handler in th:
|
||||
try:
|
||||
prio, method = handler
|
||||
ret = method(connection, event)
|
||||
if ret == 'NO MORE':
|
||||
return
|
||||
except Exception as e:
|
||||
log.error("exception floated up to DrBotIrc!")
|
||||
log.exception(e)
|
||||
|
||||
def xmlrpc_register_function(self, func, name):
|
||||
"""Add a method to the XML-RPC interface.
|
||||
|
||||
Args:
|
||||
func the method to register
|
||||
name the name to expose the method as
|
||||
|
||||
"""
|
||||
|
||||
if func and self.xmlrpc:
|
||||
if hasattr(func, '__call__'):
|
||||
self.xmlrpc.register_function(func, name)
|
||||
|
||||
def _xmlrpc_listen(self):
|
||||
"""Begin listening.
|
||||
|
||||
Hopefully this was called in a new thread.
|
||||
|
||||
"""
|
||||
|
||||
self.xmlrpc.serve_forever()
|
||||
|
||||
def try_to_replace_event_text_with_module_text(self, connection, event):
|
||||
"""Do something very similar to _handle_event, but for recursion.
|
||||
|
||||
The intent here is that we replace [text] with whatever a module
|
||||
provides to us.
|
||||
|
||||
Args:
|
||||
connection source connection
|
||||
event incoming event
|
||||
|
||||
"""
|
||||
|
||||
replies = []
|
||||
|
||||
nick = None
|
||||
userhost = None
|
||||
admin = False
|
||||
|
||||
if event.source() is not None:
|
||||
nick = irclib.nm_to_n(event.source())
|
||||
try:
|
||||
userhost = irclib.nm_to_uh(event.source())
|
||||
except IndexError: pass
|
||||
|
||||
try:
|
||||
if userhost == self.config.get('dr.botzo', 'admin_userhost'):
|
||||
admin = True
|
||||
except NoOptionError: pass
|
||||
|
||||
# try regex handlers first, since they're more specific
|
||||
rh = self.regex_handlers
|
||||
trh = sorted(rh.get('all_events', []) + rh.get(event.eventtype(), []))
|
||||
for handler in trh:
|
||||
try:
|
||||
prio, regex, method = handler
|
||||
for line in event.arguments():
|
||||
match = re.search(regex, line)
|
||||
if match:
|
||||
log.debug("pattern matched, calling "
|
||||
"{0:s}".format(method))
|
||||
# pattern matched, call method with pattern groups
|
||||
ret = method(nick, userhost, event, admin,
|
||||
match.groups())
|
||||
if ret:
|
||||
replies.append(ret)
|
||||
except Exception as e:
|
||||
log.error("exception floated up to DrBotIrc!")
|
||||
log.exception(e)
|
||||
|
||||
h = self.handlers
|
||||
th = sorted(h.get('all_events', []) + h.get(event.eventtype(), []))
|
||||
for handler in th:
|
||||
try:
|
||||
prio, method = handler
|
||||
ret = method(connection, event)
|
||||
if ret:
|
||||
replies.append(ret)
|
||||
except Exception as e:
|
||||
log.error("exception floated up to DrBotIrc!")
|
||||
log.exception(e)
|
||||
|
||||
if len(replies):
|
||||
event.arguments()[0] = '\n'.join(replies)
|
||||
|
||||
def try_alias_cmds(self, connection, event):
|
||||
"""See if there is an alias ("!command") in the text, and if so
|
||||
do alias manipulation before any other recursion or aliasing.
|
||||
|
||||
Args:
|
||||
connection source connection
|
||||
event incoming event
|
||||
|
||||
Returns:
|
||||
The outcome of the alias command, if the text had one.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
nick = irclib.nm_to_n(event.source())
|
||||
except (IndexError, AttributeError):
|
||||
nick = ''
|
||||
try:
|
||||
userhost = irclib.nm_to_uh(event.source())
|
||||
except (IndexError, AttributeError):
|
||||
userhost = ''
|
||||
replypath = event.target()
|
||||
try:
|
||||
what = event.arguments()[0]
|
||||
except (IndexError, AttributeError):
|
||||
what = ''
|
||||
admin_unlocked = False
|
||||
|
||||
# privmsg
|
||||
if replypath == connection.get_nickname():
|
||||
replypath = nick
|
||||
|
||||
try:
|
||||
if userhost == self.config.get('dr.botzo', 'admin_userhost'):
|
||||
admin_unlocked = True
|
||||
except NoOptionError: pass
|
||||
|
||||
# first see if the aliases are being directly manipulated via add/remove
|
||||
whats = what.split(' ')
|
||||
|
||||
if len(whats) <= 1:
|
||||
return
|
||||
|
||||
try:
|
||||
if whats[0] == '!alias' and whats[1] == 'add' and len(whats) >= 4:
|
||||
if not self.config.has_section('Alias'):
|
||||
self.config.add_section('Alias')
|
||||
|
||||
self.config.set('Alias', whats[2], ' '.join(whats[3:]))
|
||||
replystr = "Added alias '{0:s}'.".format(whats[2])
|
||||
return self.reply(event, replystr)
|
||||
if whats[0] == '!alias' and whats[1] == 'remove' and len(whats) >= 3:
|
||||
if not self.config.has_section('Alias'):
|
||||
self.config.add_section('Alias')
|
||||
|
||||
if self.config.remove_option('Alias', whats[2]):
|
||||
replystr = "Removed alias '{0:s}'.".format(whats[2])
|
||||
return self.reply(event, replystr)
|
||||
elif whats[0] == '!alias' and whats[1] == 'list':
|
||||
try:
|
||||
if len(whats) > 2:
|
||||
alias = self.config.get('Alias', whats[2])
|
||||
return self.reply(event, alias)
|
||||
else:
|
||||
alist = self.config.options('Alias')
|
||||
alist.remove('debug')
|
||||
alist.sort()
|
||||
liststr = ', '.join(alist)
|
||||
return self.reply(event, liststr)
|
||||
except NoSectionError: pass
|
||||
except NoOptionError: pass
|
||||
except NoSectionError: pass
|
||||
|
||||
def try_alias(self, connection, event):
|
||||
"""Try turning aliases into commands.
|
||||
|
||||
Args:
|
||||
connection source connection
|
||||
event incoming event
|
||||
|
||||
Returns:
|
||||
The de-aliased event object.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
what = event.arguments()[0]
|
||||
alias_list = self.config.options('Alias')
|
||||
|
||||
for alias in alias_list:
|
||||
alias_re = re.compile(alias, re.IGNORECASE)
|
||||
if alias_re.search(what) and alias != 'debug':
|
||||
# we found an alias for our given string, doing a replace
|
||||
command = re.sub(alias, self.config.get('Alias', alias), what, flags=re.IGNORECASE)
|
||||
event.arguments()[0] = command
|
||||
|
||||
# now we have to check it for recursions again
|
||||
self.try_recursion(connection, event)
|
||||
|
||||
# i guess someone could have an alias of an alias... try again
|
||||
return self.try_alias(connection, event)
|
||||
except NoOptionError: pass
|
||||
except NoSectionError: pass
|
||||
except IndexError: pass
|
||||
|
||||
# if we got here, there are no matching aliases, so return what we got
|
||||
return event
|
||||
|
||||
def try_recursion(self, connection, event):
|
||||
"""Scan message for subcommands to execute and use as part of this command.
|
||||
|
||||
Upon seeing a line intended for this module, see if there are subcommands
|
||||
that we should do what is basically a text replacement on. The intent is to
|
||||
allow things like the following:
|
||||
|
||||
command arg1 [anothercommand arg1 arg2]
|
||||
|
||||
where the output of anothercommand is command's arg2..n.
|
||||
|
||||
Args:
|
||||
connection source connection
|
||||
event incoming event
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
# begin recursion search
|
||||
attempt = event.arguments()[0]
|
||||
|
||||
start_idx = attempt.find('[')
|
||||
subcmd = attempt[start_idx+1:]
|
||||
end_idx = subcmd.rfind(']')
|
||||
subcmd = subcmd[:end_idx]
|
||||
|
||||
if start_idx != -1 and end_idx != -1 and len(subcmd) > 0:
|
||||
# found recursion candidate
|
||||
# copy the event and see if IT has recursion to do
|
||||
newevent = copy.deepcopy(event)
|
||||
newevent.arguments()[0] = subcmd
|
||||
newevent._recursing = True
|
||||
|
||||
self.try_recursion(connection, newevent)
|
||||
|
||||
# recursion over, check for aliases
|
||||
self.try_alias(connection, newevent)
|
||||
|
||||
# now that we have a string that has been de-aliased and
|
||||
# recursed all the way deeper into its text, see if any
|
||||
# modules can do something with it. this calls the same
|
||||
# event handlers in the same way as if this were a native
|
||||
# event.
|
||||
self.try_to_replace_event_text_with_module_text(connection, newevent)
|
||||
|
||||
# we have done all we can do with the sub-event. whatever
|
||||
# the text of that event now is, we should replace the parent
|
||||
# event's [] section with it.
|
||||
oldtext = event.arguments()[0]
|
||||
newtext = oldtext.replace('['+subcmd+']', newevent.arguments()[0])
|
||||
event.arguments()[0] = newtext
|
||||
|
||||
# we have now resolved the []. recursion will unfold, replacing
|
||||
# it further and further, until we eventually get back to the
|
||||
# original irc event in _handle_event, which will do one
|
||||
# last search on the text.
|
||||
except IndexError: pass
|
||||
|
||||
def quit_irc(self, connection, msg):
|
||||
"""Quit IRC, disconnect, shut everything down.
|
||||
|
||||
Args:
|
||||
connection the connection to quit
|
||||
msg the message to send while quitting
|
||||
"""
|
||||
|
||||
for module in self.modlist:
|
||||
module.save()
|
||||
module.shutdown()
|
||||
|
||||
connection.quit(msg)
|
||||
log.info(self.save_config())
|
||||
self._xmlrpc_shutdown()
|
||||
log.info("Bot shutting down.")
|
||||
sys.exit()
|
||||
|
||||
def reply(self, event, replystr, stop=False):
|
||||
"""Reply over IRC to replypath or return a string with the reply.
|
||||
|
||||
The primary utility for this is to properly handle recursion. The
|
||||
recursion code in DrBotIRC will set up a couple hints that this method
|
||||
picks up on and will appropriately send an IRC event or return a
|
||||
string.
|
||||
|
||||
Unless you know what you are doing, the modules you write should use
|
||||
this method rather than send a privmsg reply, as failing to call this
|
||||
method will certainly have recursion do odd things with your module.
|
||||
|
||||
Args:
|
||||
event incoming event
|
||||
replystr the message to reply with
|
||||
stop whether or not to let other handlers see this
|
||||
|
||||
Returns:
|
||||
The replystr if the event is inside recursion, or, potentially,
|
||||
"NO MORE" to stop other event handlers from acting.
|
||||
|
||||
"""
|
||||
|
||||
replypath = event.target()
|
||||
|
||||
# if this is a privmsg, reply to the sender
|
||||
if replypath == self.server.get_nickname():
|
||||
replypath = irclib.nm_to_n(event.source())
|
||||
|
||||
if replystr is not None:
|
||||
if event._recursing:
|
||||
return replystr
|
||||
else:
|
||||
replies = replystr.split('\n')
|
||||
for reply in replies:
|
||||
self.server.privmsg(replypath, reply)
|
||||
if stop:
|
||||
return "NO MORE"
|
||||
|
||||
def save_modules(self):
|
||||
"""Call each module's save(), in case they have something to do."""
|
||||
|
||||
for module in self.modlist:
|
||||
module.save()
|
||||
|
||||
def _xmlrpc_shutdown(self):
|
||||
"""Shut down the XML-RPC server."""
|
||||
|
||||
if self.xmlrpc is not None:
|
||||
self.xmlrpc.shutdown()
|
||||
self.xmlrpc.server_close()
|
||||
|
||||
def save_config(self):
|
||||
"""Write the config file to disk.
|
||||
|
||||
Returns:
|
||||
Short string indicating success.
|
||||
|
||||
"""
|
||||
|
||||
with open('dr.botzo.cfg', 'w') as cfg:
|
||||
self.config.write(cfg)
|
||||
|
||||
return "Saved config."
|
||||
|
||||
def load_module(self, modname):
|
||||
"""Load a module (in both the python and dr.botzo sense) if not
|
||||
already loaded.
|
||||
|
||||
Args:
|
||||
modname the module to attempt to load
|
||||
|
||||
Returns:
|
||||
String describing the outcome of the load attempt.
|
||||
|
||||
"""
|
||||
|
||||
for module in self.modlist:
|
||||
if modname == module.__class__.__name__:
|
||||
return "Module '{0:s}' is already loaded.".format(modname)
|
||||
|
||||
# not loaded, let's get to work
|
||||
try:
|
||||
modstr = 'modules.'+modname
|
||||
__import__(modstr)
|
||||
module = sys.modules[modstr]
|
||||
botmod = eval('module.' + modname + '(self, self.config)')
|
||||
self.modlist.append(botmod)
|
||||
botmod.register_handlers()
|
||||
|
||||
# might as well add it to the list
|
||||
modset = set(self.config.get('dr.botzo', 'module_list').split(','))
|
||||
modset.add(modname)
|
||||
self.config.set('dr.botzo', 'module_list', ','.join(modset))
|
||||
|
||||
return "Module '{0:s}' loaded.".format(modname)
|
||||
except ImportError as e:
|
||||
log.error("Error loading '{0:s}'".format(modname))
|
||||
log.exception(e)
|
||||
return "Module '{0:s}' could not be loaded.".format(modname)
|
||||
|
||||
def unload_module(self, modname):
|
||||
"""Attempt to unload and del a module if it's loaded.
|
||||
|
||||
Args:
|
||||
modname the module to attempt to unload
|
||||
|
||||
Returns:
|
||||
String describing the outcome of the unload attempt.
|
||||
|
||||
"""
|
||||
|
||||
modstr = 'modules.'+modname
|
||||
for module in self.modlist:
|
||||
if modname == module.__class__.__name__:
|
||||
# do anything the module needs to do to clean up
|
||||
module.save()
|
||||
module.shutdown()
|
||||
|
||||
# remove module references
|
||||
self.modlist.remove(module)
|
||||
module.unregister_handlers()
|
||||
|
||||
# del it
|
||||
del(sys.modules[modstr])
|
||||
del(module)
|
||||
|
||||
# might as well remove it from the list
|
||||
modset = set(self.config.get('dr.botzo', 'module_list').split(','))
|
||||
modset.remove(modname)
|
||||
self.config.set('dr.botzo', 'module_list', ','.join(modset))
|
||||
|
||||
return "Module '{0:s}' unloaded.".format(modname)
|
||||
|
||||
# guess it was never loaded
|
||||
return "Module '{0:s}' is not loaded.".format(modname)
|
||||
|
||||
def list_modules(self):
|
||||
"""List loaded modules.
|
||||
|
||||
Returns:
|
||||
A list of the loaded modules' names.
|
||||
|
||||
"""
|
||||
|
||||
modnames = []
|
||||
for module in self.modlist:
|
||||
modnames.append(module.__class__.__name__)
|
||||
|
||||
return modnames
|
||||
|
||||
def sigint_handler(self, signal, frame):
|
||||
"""Cleanly shutdown on SIGINT."""
|
||||
|
||||
for module in self.modlist:
|
||||
module.save()
|
||||
module.shutdown()
|
||||
|
||||
log.info(self.save_config())
|
||||
self._xmlrpc_shutdown()
|
||||
sys.exit()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
328
ircbot/Module.py
Normal file
328
ircbot/Module.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
Module - dr.botzo modular functionality base class
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
from ConfigParser import NoSectionError, NoOptionError
|
||||
|
||||
import logging
|
||||
|
||||
import MySQLdb as mdb
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
class Module(object):
|
||||
|
||||
"""Provide module base class with convenience functionality/bootstrap."""
|
||||
|
||||
def priority(self):
|
||||
return 50
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Construct a feature module.
|
||||
|
||||
Inheritors can do special things here, but should be sure to call
|
||||
Module.__init__.
|
||||
|
||||
Args:
|
||||
irc DrBotIRC object for the running bot
|
||||
config ConfigParser object, the entire config file
|
||||
|
||||
"""
|
||||
|
||||
self.irc = irc
|
||||
self.config = config
|
||||
|
||||
# reload logging config every time
|
||||
logging.config.fileConfig('logging.cfg')
|
||||
self.log = logging.getLogger('drbotzo.'+self.__class__.__name__.lower())
|
||||
|
||||
self.is_shutdown = False
|
||||
|
||||
# set up database for this module
|
||||
self.db_init()
|
||||
|
||||
# set up XML-RPC for this module
|
||||
self.xmlrpc_init()
|
||||
|
||||
self.log.info("Loaded " + self.__class__.__name__)
|
||||
|
||||
def register_handlers(self):
|
||||
"""Hook handler functions into the IRC library.
|
||||
|
||||
This is called when the module is loaded. Classes with special stuff
|
||||
to do could implement this and set up the appropriate handlers, e.g.:
|
||||
|
||||
self.irc.server.add_global_handler('welcome', self.on_connect)
|
||||
|
||||
By default, a module attaches to pubmsg/privmsg, which sets up some
|
||||
common variables and then calls do(). You are free to implement do()
|
||||
(see below), or override this and do whatever you want.
|
||||
|
||||
Modules interested in doing so might also register XML-RPC stuff here.
|
||||
|
||||
"""
|
||||
|
||||
self.irc.server.add_global_handler('pubmsg', self.on_pub_or_privmsg,
|
||||
self.priority())
|
||||
self.irc.server.add_global_handler('privmsg', self.on_pub_or_privmsg,
|
||||
self.priority())
|
||||
|
||||
def unregister_handlers(self):
|
||||
"""Unhook handler functions from the IRC library.
|
||||
|
||||
Inverse of the above. This is called by unload, to remove the
|
||||
soon-to-be old object from the server global handlers (or whatever has
|
||||
been added via register_handlers). Classes inheriting from Module
|
||||
could reimplement this, e.g.:
|
||||
|
||||
self.irc.server.remove_global_handler('welcome', self.on_connect)
|
||||
|
||||
"""
|
||||
|
||||
self.irc.server.remove_global_handler('pubmsg', self.on_pub_or_privmsg)
|
||||
self.irc.server.remove_global_handler('privmsg', self.on_pub_or_privmsg)
|
||||
|
||||
def on_pub_or_privmsg(self, connection, event):
|
||||
"""Do a default thing on a pubmsg or privmsg.
|
||||
|
||||
Sets up a couple variables and then calls do(), which by default we
|
||||
expect implementers to implement.
|
||||
|
||||
Args:
|
||||
connection the source connection for this event
|
||||
event the event to handle
|
||||
|
||||
Returns:
|
||||
The results of handling the event.
|
||||
|
||||
"""
|
||||
|
||||
nick = irclib.nm_to_n(event.source())
|
||||
userhost = irclib.nm_to_uh(event.source())
|
||||
what = event.arguments()[0]
|
||||
admin_unlocked = False
|
||||
|
||||
try:
|
||||
if userhost == self.config.get('dr.botzo', 'admin_userhost'):
|
||||
admin_unlocked = True
|
||||
except NoOptionError: pass
|
||||
|
||||
return self.do(connection, event, nick, userhost, what, admin_unlocked)
|
||||
|
||||
def sendmsg(self, target, msg):
|
||||
"""Send a privmsg over IRC to target.
|
||||
|
||||
Args:
|
||||
target destination on the network
|
||||
msg the message to send
|
||||
|
||||
"""
|
||||
|
||||
if msg is not None:
|
||||
if target is not None:
|
||||
msgs = msg.split('\n')
|
||||
for line in msgs:
|
||||
self.irc.server.privmsg(target, line)
|
||||
|
||||
def save(self):
|
||||
"""Save whatever the module may need to save. Sync files, etc.
|
||||
|
||||
Implement this if you need it.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
"""Do pre-deletion type cleanup.
|
||||
|
||||
Implement this to close databases, write to disk, etc. Note that
|
||||
DrBotIRC will call save() before this, so implement appropriately.
|
||||
|
||||
"""
|
||||
|
||||
self.is_shutdown = True
|
||||
self.log.info("Unloading " + self.__class__.__name__)
|
||||
|
||||
def remove_metaoptions(self, list_):
|
||||
"""Remove metaoptions from provided list, which was probably from a
|
||||
config file.
|
||||
|
||||
DEPRECATED
|
||||
|
||||
This is a convenience method that can go away once we get module stuff
|
||||
out of the config file and into the database.
|
||||
|
||||
Args:
|
||||
list_ the list to prune known keywords from
|
||||
|
||||
"""
|
||||
|
||||
list_.remove('debug')
|
||||
|
||||
def retransmit_event(self, event):
|
||||
"""Pretend that some event the bot has generated is rather an incoming
|
||||
IRC event.
|
||||
|
||||
DEPRECATED
|
||||
|
||||
Why one would do this is unclear, but I wrote it and then realized
|
||||
I didn't need it.
|
||||
|
||||
Args:
|
||||
event the event to replay
|
||||
|
||||
"""
|
||||
|
||||
self.irc.server._handle_event(event)
|
||||
|
||||
def get_db(self):
|
||||
"""Get a database connection to mdb.
|
||||
|
||||
Once grabbed, it should be closed when work is done. Modules that
|
||||
need a database connection should test for and create (or, eventually,
|
||||
alter) required table structure in their __init__ IF that structure
|
||||
does not already exist. Well-behaved modules should use a prefix in
|
||||
their table names (eg "karma_log" rather than "log")
|
||||
|
||||
See also db_module_registered, below.
|
||||
|
||||
"""
|
||||
|
||||
dbhost = self.config.get('dr.botzo', 'dbhost')
|
||||
dbuser = self.config.get('dr.botzo', 'dbuser')
|
||||
dbpass = self.config.get('dr.botzo', 'dbpass')
|
||||
dbname = self.config.get('dr.botzo', 'dbname')
|
||||
db = mdb.connect(dbhost, dbuser, dbpass, dbname, charset='utf8',
|
||||
use_unicode=True)
|
||||
|
||||
return db
|
||||
|
||||
def db_module_registered(self, modulename):
|
||||
"""Ask the database for a version number for a module.
|
||||
|
||||
Return that version number if the module has registered, or None if
|
||||
not.
|
||||
|
||||
Args:
|
||||
modulename the name of the module to check
|
||||
|
||||
Returns:
|
||||
The version number stored for the module in the database, as a int
|
||||
|
||||
"""
|
||||
|
||||
db = self.get_db()
|
||||
version = None
|
||||
try:
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT version FROM drbotzo_modules WHERE module = %s",
|
||||
(modulename,))
|
||||
version = cur.fetchone()
|
||||
if (version != None):
|
||||
version = version[0]
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error during module registered check")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
return version
|
||||
|
||||
def db_register_module_version(self, modulename, version):
|
||||
"""Enter the given module name and version number into the database.
|
||||
|
||||
The primary want for this is to record what the current revision the
|
||||
tables are, to do upgrades.
|
||||
|
||||
Args:
|
||||
modulename the name of the module to update
|
||||
version the version number to set as a int
|
||||
|
||||
"""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('INSERT IGNORE INTO drbotzo_modules (version, module) VALUES (%s, %s)',
|
||||
(version, modulename))
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error during register module version")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def db_init(self):
|
||||
"""Set up the database tables and so on.
|
||||
|
||||
Modules interested in this should implement table setup here.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def xmlrpc_init(self):
|
||||
"""Set up XML-RPC handlers.
|
||||
|
||||
Modules interested in this should implement registration here.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin):
|
||||
"""Do the primary thing this module was intended to do, in most cases.
|
||||
|
||||
Implement this method in your subclass to have a fairly-automatic hook
|
||||
into IRC functionality. This is called by DrBotIRC during pubmsg and
|
||||
privmsg events.
|
||||
|
||||
Args:
|
||||
connection the connection instance to handle
|
||||
event the event to handle
|
||||
nick the nick of the originator of the event
|
||||
userhost the userhost of the originator of the event
|
||||
what the message body of the event
|
||||
admin if the event was created by an admin
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def _unencode_xml(self, text):
|
||||
"""Convert <, >, & to their real entities.
|
||||
|
||||
Convenience method for modules, I've been thinking about moving this.
|
||||
|
||||
Args:
|
||||
text the text to clean up
|
||||
|
||||
Returns:
|
||||
The provided text string, with the known entities replaced.
|
||||
|
||||
"""
|
||||
|
||||
text = text.replace('<', '<')
|
||||
text = text.replace('>', '>')
|
||||
text = text.replace('&', '&')
|
||||
return text
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
23
ircbot/dr.botzo.cfg.example
Normal file
23
ircbot/dr.botzo.cfg.example
Normal file
@@ -0,0 +1,23 @@
|
||||
[dr.botzo]
|
||||
server = irc.foonetic.net
|
||||
port = 6667
|
||||
nick = dr_devzo
|
||||
name = dr. devzo
|
||||
usermode = -x
|
||||
debug = true
|
||||
admin_userhost = bss@ayu.incorporeal.org
|
||||
module_list = IrcAdmin
|
||||
dbhost = localhost
|
||||
dbuser = dr_botzo
|
||||
dbpass = password
|
||||
dbname = dr_botzo
|
||||
ssl = no
|
||||
ipv6 = yes
|
||||
|
||||
[IrcAdmin]
|
||||
autojoin = #bss
|
||||
sleep = 30
|
||||
automsg = nickserv identify foo
|
||||
|
||||
[Karma]
|
||||
meta.pubmsg_needs_bot_prefix = false
|
||||
132
ircbot/dr.botzo.py
Normal file
132
ircbot/dr.botzo.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
dr.botzo - a pluggable IRC bot written in Python
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
from ConfigParser import ConfigParser, NoSectionError, NoOptionError
|
||||
import logging
|
||||
import logging.config
|
||||
import os
|
||||
import sys
|
||||
|
||||
import MySQLdb as mdb
|
||||
|
||||
import DrBotIRC
|
||||
|
||||
config_file = 'dr.botzo.cfg'
|
||||
# check argv
|
||||
if len(sys.argv) == 2:
|
||||
config_file = sys.argv[1]
|
||||
|
||||
# read config file
|
||||
config = ConfigParser({'debug': 'false'})
|
||||
config.read(os.path.expanduser(config_file))
|
||||
|
||||
# load necessary options
|
||||
try:
|
||||
# load connection info
|
||||
botserver = config.get('dr.botzo', 'server')
|
||||
botport = config.getint('dr.botzo', 'port')
|
||||
botnick = config.get('dr.botzo', 'nick')
|
||||
botpass = config.get('dr.botzo', 'pass')
|
||||
botuser = config.get('dr.botzo', 'user')
|
||||
botircname = config.get('dr.botzo', 'name')
|
||||
except NoSectionError as e:
|
||||
sys.exit("Aborted due to error with necessary configuration: "
|
||||
"{0:s}".format(str(e)))
|
||||
except NoOptionError as e:
|
||||
sys.exit("Aborted due to error with necessary configuration: "
|
||||
"{0:s}".format(str(e)))
|
||||
|
||||
logging.config.fileConfig('logging.cfg')
|
||||
log = logging.getLogger('drbotzo')
|
||||
|
||||
try:
|
||||
dbhost = config.get('dr.botzo', 'dbhost')
|
||||
dbuser = config.get('dr.botzo', 'dbuser')
|
||||
dbpass = config.get('dr.botzo', 'dbpass')
|
||||
dbname = config.get('dr.botzo', 'dbname')
|
||||
db = mdb.connect(dbhost, dbuser, dbpass, dbname, charset='utf8',
|
||||
use_unicode=True)
|
||||
try:
|
||||
cur = db.cursor()
|
||||
# need to create the drbotzo_modules table if it doesn't exist
|
||||
query = """
|
||||
SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema = %s
|
||||
AND table_name = %s
|
||||
"""
|
||||
cur.execute(query, (dbname, 'drbotzo_modules'))
|
||||
row = cur.fetchone()
|
||||
if row[0] == 0:
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS drbotzo_modules (
|
||||
module VARCHAR(64) PRIMARY KEY,
|
||||
version INTEGER
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
"""
|
||||
cur.execute(query)
|
||||
db.commit()
|
||||
finally: cur.close()
|
||||
except NoOptionError as e:
|
||||
sys.exit("Aborted due to error with necessary configuration: "
|
||||
"{0:s}".format(str(e)))
|
||||
|
||||
# get some optional parameters
|
||||
use_ssl = False
|
||||
try:
|
||||
use_ssl = config.getboolean('dr.botzo', 'ssl')
|
||||
except NoOptionError:
|
||||
pass
|
||||
if use_ssl:
|
||||
log.info("SSL support enabled")
|
||||
else:
|
||||
log.debug("SSL not requested")
|
||||
|
||||
use_ipv6 = False
|
||||
try:
|
||||
use_ipv6 = config.getboolean('dr.botzo', 'ipv6')
|
||||
except NoOptionError:
|
||||
pass
|
||||
if use_ipv6:
|
||||
log.info("IPv6 support enabled")
|
||||
else:
|
||||
log.debug("IPv6 not requested")
|
||||
|
||||
# start up the IRC bot
|
||||
|
||||
# create IRC and server objects and connect
|
||||
irc = DrBotIRC.DrBotIRC(config)
|
||||
server = irc.server().connect(botserver, botport, botnick, botpass,
|
||||
botuser, botircname, ssl=use_ssl, ipv6=use_ipv6)
|
||||
|
||||
# load features
|
||||
try:
|
||||
cfgmodlist = config.get('dr.botzo', 'module_list')
|
||||
|
||||
mods = cfgmodlist.split(',')
|
||||
for mod in mods:
|
||||
irc.load_module(mod)
|
||||
except NoOptionError as e:
|
||||
log.warning("You seem to be missing a module_list config option, which "
|
||||
"you probably wanted.")
|
||||
|
||||
# loop forever
|
||||
irc.process_forever()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
0
ircbot/extlib/__init__.py
Normal file
0
ircbot/extlib/__init__.py
Normal file
1711
ircbot/extlib/irclib.py
Normal file
1711
ircbot/extlib/irclib.py
Normal file
File diff suppressed because it is too large
Load Diff
58
ircbot/logging.cfg
Normal file
58
ircbot/logging.cfg
Normal file
@@ -0,0 +1,58 @@
|
||||
[loggers]
|
||||
keys = root,drbotzo,irclib,drbotzo.markov,drbotzo.weather,drbotzo.dispatch
|
||||
|
||||
[handlers]
|
||||
keys = stdout,logfile
|
||||
|
||||
[formatters]
|
||||
keys = verbose
|
||||
|
||||
[logger_root]
|
||||
level = INFO
|
||||
handlers = stdout,logfile
|
||||
|
||||
[logger_irclib]
|
||||
level = INFO
|
||||
handlers = stdout,logfile
|
||||
propagate = 0
|
||||
qualname = irclib
|
||||
|
||||
[logger_drbotzo]
|
||||
level = INFO
|
||||
handlers = stdout,logfile
|
||||
propagate = 0
|
||||
qualname = drbotzo
|
||||
|
||||
[logger_drbotzo.markov]
|
||||
level = DEBUG
|
||||
handlers = stdout,logfile
|
||||
propagate = 0
|
||||
qualname = drbotzo.markov
|
||||
|
||||
[logger_drbotzo.dispatch]
|
||||
level = DEBUG
|
||||
handlers = stdout,logfile
|
||||
propagate = 0
|
||||
qualname = drbotzo.dispatch
|
||||
|
||||
[logger_drbotzo.weather]
|
||||
level = DEBUG
|
||||
handlers = stdout,logfile
|
||||
propagate = 0
|
||||
qualname = drbotzo.weather
|
||||
|
||||
[handler_stdout]
|
||||
class = StreamHandler
|
||||
level = DEBUG
|
||||
formatter = verbose
|
||||
args = ''
|
||||
|
||||
[handler_logfile]
|
||||
class = FileHandler
|
||||
level = DEBUG
|
||||
formatter = verbose
|
||||
args = ('dr.botzo.log', 'a')
|
||||
|
||||
[formatter_verbose]
|
||||
format = [%(levelname)-8s %(asctime)s %(name)s] %(message)s
|
||||
datefmt =
|
||||
470
ircbot/modules/Achievements.py
Normal file
470
ircbot/modules/Achievements.py
Normal file
@@ -0,0 +1,470 @@
|
||||
"""
|
||||
Achievements - gamifying IRC
|
||||
Copyright (C) 2011 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import re
|
||||
import thread
|
||||
import time
|
||||
|
||||
import MySQLdb as mdb
|
||||
|
||||
from Module import Module
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
__author__ = "Brian S. Stephan"
|
||||
__copyright__ = "Copyright 2011, Brian S. Stephan"
|
||||
__credits__ = ["Brian S. Stephan", "#lh"]
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.1"
|
||||
__maintainer__ = "Brian S. Stephan"
|
||||
__email__ = "bss@incorporeal.org"
|
||||
__status__ = "Development"
|
||||
|
||||
class Achievements(Module):
|
||||
|
||||
"""Give out achievements for doing stuff on IRC, because why not."""
|
||||
|
||||
class AchievementsSettings():
|
||||
|
||||
"""Track system settings."""
|
||||
|
||||
pass
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Set up trigger regexes."""
|
||||
|
||||
# TODO
|
||||
joinpattern = '^!achievements\s+join$'
|
||||
leavepattern = '^!achievements\s+leave$'
|
||||
infopattern = '^!achievements\s+info\s+(.*)$'
|
||||
rankpattern = '^!achievements\s+rank\s+(.*)$'
|
||||
|
||||
self.joinre = re.compile(joinpattern)
|
||||
self.leavere = re.compile(leavepattern)
|
||||
self.infore = re.compile(infopattern)
|
||||
self.rankre = re.compile(rankpattern)
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
self.next_achievements_scan = 0
|
||||
thread.start_new_thread(self.thread_do, ())
|
||||
|
||||
def db_init(self):
|
||||
"""Set up the database tables, if they don't exist."""
|
||||
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if (version == None):
|
||||
# have to create the database tables
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE achievements_player (
|
||||
id SERIAL,
|
||||
nick VARCHAR(64) NOT NULL UNIQUE,
|
||||
userhost VARCHAR(256) NOT NULL DEFAULT '',
|
||||
is_playing INTEGER NOT NULL DEFAULT 0,
|
||||
last_seen_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE achievements_event (
|
||||
id SERIAL,
|
||||
player_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
event VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(64),
|
||||
msg_len INTEGER,
|
||||
event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(player_id) REFERENCES achievements_player(id)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE achievements_achievement (
|
||||
id SERIAL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
description VARCHAR(256) NOT NULL,
|
||||
query VARCHAR(1024) NOT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE achievements_log (
|
||||
id SERIAL,
|
||||
player_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
achievement_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(player_id) REFERENCES achievements_player(id),
|
||||
FOREIGN KEY(achievement_id) REFERENCES achievements_achievement(id)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE achievements_config (
|
||||
channel TEXT NOT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE achievements_filter (
|
||||
id SERIAL,
|
||||
filter VARCHAR(256) NOT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE achievements_filter_log (
|
||||
id SERIAL,
|
||||
filter_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
event_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
FOREIGN KEY(filter_id) REFERENCES achievements_filter(id),
|
||||
FOREIGN KEY(event_id) REFERENCES achievements_event(id)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def register_handlers(self):
|
||||
"""Handle all sorts of things to track."""
|
||||
|
||||
self.irc.server.add_global_handler('pubmsg', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('pubnotice', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('privmsg', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('privnotice', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('join', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('kick', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('mode', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('part', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('quit', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('invite', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('action', self.track_irc_event)
|
||||
self.irc.server.add_global_handler('topic', self.track_irc_event)
|
||||
|
||||
self.irc.server.add_global_handler('pubmsg', self.on_pub_or_privmsg)
|
||||
self.irc.server.add_global_handler('privmsg', self.on_pub_or_privmsg)
|
||||
|
||||
def unregister_handlers(self):
|
||||
self.irc.server.remove_global_handler('pubmsg', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('pubnotice', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('privmsg', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('privnotice', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('join', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('kick', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('mode', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('part', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('quit', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('invite', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('action', self.track_irc_event)
|
||||
self.irc.server.remove_global_handler('topic', self.track_irc_event)
|
||||
|
||||
self.irc.server.remove_global_handler('pubmsg', self.on_pub_or_privmsg)
|
||||
self.irc.server.remove_global_handler('privmsg', self.on_pub_or_privmsg)
|
||||
|
||||
def track_irc_event(self, connection, event):
|
||||
"""Put events in the database."""
|
||||
|
||||
if event.source():
|
||||
if event.source().find('!') >= 0:
|
||||
nick = irclib.nm_to_n(event.source())
|
||||
userhost = irclib.nm_to_uh(event.source())
|
||||
self.log.debug('good: ' + nick + ' ' + userhost + ' ' + event.eventtype() + ' ' + str(event.target()))
|
||||
if event.arguments():
|
||||
msg = event.arguments()[0]
|
||||
msg_len = len(event.arguments()[0])
|
||||
else:
|
||||
msg = ''
|
||||
msg_len = 0
|
||||
|
||||
player_id = self._get_or_add_player(nick, userhost)
|
||||
self._add_event(player_id, event.eventtype(), event.target(), msg, msg_len)
|
||||
else:
|
||||
self.log.debug('bad: ' + event.source() + ' ' + event.eventtype())
|
||||
else:
|
||||
self.log.debug('really bad: ' + event.eventtype())
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Do stuff when commanded."""
|
||||
|
||||
if self.joinre.search(what):
|
||||
return self.irc.reply(event, self._join_system(nick))
|
||||
elif self.leavere.search(what):
|
||||
return self.irc.reply(event, self._leave_system(nick))
|
||||
elif self.infore.search(what):
|
||||
match = self.infore.search(what)
|
||||
achievement = match.group(1)
|
||||
desc = self._get_achievement_info(achievement)
|
||||
if desc:
|
||||
return self.irc.reply(event, achievement + ': ' + desc)
|
||||
elif self.rankre.search(what):
|
||||
match = self.rankre.search(what)
|
||||
player = match.group(1)
|
||||
achievements = self._get_player_achievements(player)
|
||||
if len(achievements):
|
||||
return self.irc.reply(event, player + ' has obtained ' + ', '.join(achievements))
|
||||
|
||||
def thread_do(self):
|
||||
"""Do the scan for achievements and other miscellaneous tasks."""
|
||||
|
||||
while not self.is_shutdown:
|
||||
self._do_achievement_scan()
|
||||
time.sleep(1)
|
||||
|
||||
def _get_or_add_player(self, nick, userhost):
|
||||
"""Add a player to the database, or update the existing one, and return the id."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = '''
|
||||
INSERT IGNORE INTO achievements_player (nick) VALUES (%s)'''
|
||||
cur.execute(statement, (nick,))
|
||||
statement = '''
|
||||
UPDATE achievements_player SET userhost = %s, last_seen_time = CURRENT_TIMESTAMP
|
||||
WHERE nick = %s'''
|
||||
cur.execute(statement, (userhost, nick))
|
||||
db.commit()
|
||||
statement = '''SELECT id FROM achievements_player WHERE nick = %s'''
|
||||
cur.execute(statement, (nick,))
|
||||
result = cur.fetchone()
|
||||
return result['id']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error getting or adding player")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _add_event(self, player_id, event, target, msg, msg_len):
|
||||
"""Add an event to the log."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = '''
|
||||
INSERT INTO achievements_event (
|
||||
player_id, event, target, msg_len
|
||||
) VALUES (%s, %s, %s, %s)
|
||||
'''
|
||||
cur.execute(statement, (player_id, event, target, msg_len))
|
||||
db.commit()
|
||||
event_id = cur.lastrowid
|
||||
|
||||
# now see if the event matched any filters
|
||||
query = '''
|
||||
SELECT id FROM achievements_filter WHERE %s REGEXP filter
|
||||
'''
|
||||
cur.execute(query, (msg,))
|
||||
results = cur.fetchall()
|
||||
for result in results:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = '''
|
||||
INSERT INTO achievements_filter_log (filter_id, event_id)
|
||||
VALUES (%s, %s)
|
||||
'''
|
||||
cur.execute(statement, (result['id'], event_id))
|
||||
db.commit()
|
||||
|
||||
return event_id
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error adding event")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _get_achievements_settings(self):
|
||||
"""Get the report settings."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
# get the settings
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = 'SELECT channel FROM achievements_config'
|
||||
cur.execute(query)
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
settings = self.AchievementsSettings()
|
||||
settings.channel = result['channel']
|
||||
|
||||
return settings
|
||||
else:
|
||||
return None
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error getting settings")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
except AttributeError as e:
|
||||
self.log.error("could not get channel settings, probably unset")
|
||||
self.log.exception(e)
|
||||
return None
|
||||
finally: cur.close()
|
||||
|
||||
def _join_system(self, nick):
|
||||
"""Add the appropriate nick to the system."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = 'UPDATE achievements_player SET is_playing = 1 WHERE nick = %s'
|
||||
cur.execute(statement, (nick,))
|
||||
db.commit()
|
||||
return nick + ' joined.'
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error joining the system")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _leave_system(self, nick):
|
||||
"""Remove the appropriate nick from the system."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = 'UPDATE achievements_player SET is_playing = 0 WHERE nick = %s'
|
||||
cur.execute(statement, (nick,))
|
||||
db.commit()
|
||||
return nick + ' left.'
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error leaving the system")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _add_player_to_achievement_log(self, player_id, achievement_id):
|
||||
"""Log the success of a player."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = 'INSERT INTO achievements_log (player_id, achievement_id) VALUES (%s, %s)'
|
||||
cur.execute(statement, (player_id, achievement_id))
|
||||
db.commit()
|
||||
return
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error adding player to achievement log")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _get_achievement_info(self, achievement):
|
||||
"""Return the description of a given achievement."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = 'SELECT description FROM achievements_achievement WHERE name = %s'
|
||||
cur.execute(query, (achievement,))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return result['description']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error getting achievement info")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _get_player_achievements(self, nick):
|
||||
"""Return the achievements the nick has."""
|
||||
|
||||
achievements = []
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = '''
|
||||
SELECT a.name FROM achievements_achievement a
|
||||
INNER JOIN achievements_log l ON l.achievement_id = a.id
|
||||
INNER JOIN achievements_player p ON p.id = l.player_id
|
||||
WHERE p.nick = %s
|
||||
'''
|
||||
cur.execute(query, (nick,))
|
||||
results = cur.fetchall()
|
||||
for result in results:
|
||||
achievements.append(result['name'])
|
||||
|
||||
return achievements
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error getting player achievements")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _do_achievement_scan(self):
|
||||
"""Run the queries in the database, seeing if anyone new has an achievement."""
|
||||
|
||||
# don't do anything the first time
|
||||
if self.next_achievements_scan == 0:
|
||||
self.next_achievements_scan = time.time() + 300
|
||||
|
||||
if self.next_achievements_scan < time.time():
|
||||
self.next_achievements_scan = time.time() + 300
|
||||
|
||||
self.log.debug('in achievement scan')
|
||||
settings = self._get_achievements_settings()
|
||||
if settings is not None:
|
||||
channel = settings.channel
|
||||
achievers = self._query_for_new_achievers()
|
||||
for achiever in achievers:
|
||||
self.sendmsg(channel, achiever[0] + ' achieved ' + achiever[1] + '!')
|
||||
|
||||
def _query_for_new_achievers(self):
|
||||
"""Get new achievement earners for each achievement."""
|
||||
|
||||
achievers = []
|
||||
self.log.debug('checking achievements')
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = 'SELECT id, name, query FROM achievements_achievement'
|
||||
cur.execute(query)
|
||||
achievements = cur.fetchall()
|
||||
|
||||
for achievement in achievements:
|
||||
self.log.debug('checking achievement:[' + achievement['name'] + ']')
|
||||
query = '''
|
||||
SELECT p.id, p.nick FROM achievements_player p WHERE
|
||||
p.nick IN (
|
||||
'''
|
||||
query = query + achievement['query']
|
||||
query = query + '''
|
||||
) AND p.is_playing = 1
|
||||
AND p.id NOT IN (
|
||||
SELECT player_id FROM achievements_log l
|
||||
INNER JOIN achievements_achievement a
|
||||
ON a.id = l.achievement_id
|
||||
WHERE a.name = %s
|
||||
)
|
||||
'''
|
||||
cur.execute(query, (achievement['name'],))
|
||||
ach_achievers = cur.fetchall()
|
||||
|
||||
for ach_achiever in ach_achievers:
|
||||
self.log.debug('name:[' + ach_achiever['nick'] + '] achievement:[' + achievement['name'] + ']')
|
||||
self._add_player_to_achievement_log(ach_achiever['id'], achievement['id'])
|
||||
achievers.append((ach_achiever['nick'], achievement['name']))
|
||||
|
||||
return achievers
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error scanning new achievers")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
365
ircbot/modules/Acro.py
Normal file
365
ircbot/modules/Acro.py
Normal file
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
Acro - An acromania style game for IRC
|
||||
Copyright (C) 2012 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
import thread
|
||||
import time
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Acro(Module):
|
||||
|
||||
class AcroGame(object):
|
||||
|
||||
"""Track game details."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize basic stuff."""
|
||||
|
||||
# running state
|
||||
self.state = 0
|
||||
self.quit = False
|
||||
self.scores = dict()
|
||||
self.rounds = []
|
||||
self.channel = ''
|
||||
|
||||
class AcroRound(object):
|
||||
|
||||
"""Track a particular round of a game."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize basic stuff."""
|
||||
|
||||
self.acro = ""
|
||||
self.submissions = dict()
|
||||
self.sub_shuffle = []
|
||||
self.votes = dict()
|
||||
|
||||
# default options
|
||||
self.seconds_to_submit = 60
|
||||
self.seconds_to_vote = 45
|
||||
self.seconds_to_submit_step = 10
|
||||
self.seconds_to_pause = 5
|
||||
|
||||
"""
|
||||
Play a game where users come up with silly definitions for randomly-generated
|
||||
acronyms.
|
||||
"""
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Set up the game tracking and such."""
|
||||
|
||||
# build regexes
|
||||
acroprefix = r"!acro"
|
||||
statuscmd = r"status"
|
||||
startcmd = r"start"
|
||||
submitcmd = r"submit"
|
||||
votecmd = r"vote"
|
||||
quitcmd = r"quit"
|
||||
|
||||
self.statusre = re.compile(r"^{0:s}\s+{1:s}$".format(acroprefix, statuscmd))
|
||||
self.startre = re.compile(r"^{0:s}\s+{1:s}$".format(acroprefix, startcmd))
|
||||
self.submitre = re.compile(r"^{0:s}\s+{1:s}\s+(.*)$".format(acroprefix, submitcmd))
|
||||
self.votere = re.compile(r"^{0:s}\s+{1:s}\s+(\d+)$".format(acroprefix, votecmd))
|
||||
self.quitre = re.compile(r"^{0:s}\s+{1:s}$".format(acroprefix, quitcmd))
|
||||
|
||||
# game state
|
||||
self.game = self.AcroGame()
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Handle commands and inputs."""
|
||||
|
||||
target = event.target()
|
||||
|
||||
if self.statusre.search(what):
|
||||
return self.irc.reply(event, self.do_status(what))
|
||||
if self.startre.search(what):
|
||||
return self.irc.reply(event, self.do_start(target, what))
|
||||
if self.submitre.search(what):
|
||||
match = self.submitre.search(what)
|
||||
return self.irc.reply(event, self.do_submit(nick, match.group(1)))
|
||||
if self.votere.search(what):
|
||||
match = self.votere.search(what)
|
||||
return self.irc.reply(event, self.do_vote(nick, match.group(1)))
|
||||
if self.quitre.search(what):
|
||||
return self.irc.reply(event, self.do_quit(what))
|
||||
|
||||
def do_status(self, what):
|
||||
"""Return the status of the currently-running game, if there is one."""
|
||||
|
||||
if self.game.state == 0:
|
||||
return "there currently isn't a game running."
|
||||
else:
|
||||
return "the game is running."
|
||||
|
||||
def do_start(self, target, what):
|
||||
"""Start the game, notify the channel, and begin timers."""
|
||||
|
||||
if self.game.state != 0:
|
||||
return "the game is alredy running."
|
||||
else:
|
||||
if irclib.is_channel(target):
|
||||
self._start_new_game(target)
|
||||
else:
|
||||
return "you must start the game from a channel."
|
||||
|
||||
def do_submit(self, nick, what):
|
||||
"""Take a submission for an acro."""
|
||||
|
||||
return self._take_acro_submission(nick, what)
|
||||
|
||||
def do_vote(self, nick, what):
|
||||
"""Take a vote for an acro."""
|
||||
|
||||
return self._take_acro_vote(nick, what)
|
||||
|
||||
def do_quit(self, what):
|
||||
"""Quit the game after the current round ends."""
|
||||
|
||||
if self.game.state != 0:
|
||||
self.game.quit = True
|
||||
return "the game will end after the current round."
|
||||
|
||||
def thread_do_process_submissions(self, sleep_time):
|
||||
"""Wait for players to provide acro submissions, and then kick off voting."""
|
||||
|
||||
time.sleep(sleep_time)
|
||||
self._start_voting()
|
||||
|
||||
def thread_do_process_votes(self):
|
||||
"""Wait for players to provide votes, and then continue or quit."""
|
||||
|
||||
time.sleep(self.game.rounds[-1].seconds_to_vote)
|
||||
self._end_current_round()
|
||||
|
||||
def _start_new_game(self, channel):
|
||||
"""Begin a new game, which will have multiple rounds."""
|
||||
|
||||
self.game.state = 1
|
||||
self.game.channel = channel
|
||||
self.sendmsg(self.game.channel,
|
||||
"starting a new game of acro. it will run until you tell it to quit.")
|
||||
|
||||
self._start_new_round()
|
||||
|
||||
def _start_new_round(self):
|
||||
"""Start a new round for play."""
|
||||
|
||||
self.game.state = 2
|
||||
self.game.rounds.append(self.AcroRound())
|
||||
acro = self._generate_acro()
|
||||
self.game.rounds[-1].acro = acro
|
||||
sleep_time = self.game.rounds[-1].seconds_to_submit + (self.game.rounds[-1].seconds_to_submit_step * (len(acro)-3))
|
||||
|
||||
self.sendmsg(self.game.channel,
|
||||
"the round has started! your acronym is '{0:s}'. ".format(acro) +
|
||||
"submit within {0:d} seconds via !acro submit [meaning]".format(sleep_time))
|
||||
|
||||
thread.start_new_thread(self.thread_do_process_submissions, (sleep_time,))
|
||||
|
||||
def _generate_acro(self):
|
||||
"""
|
||||
Generate an acro to play with.
|
||||
|
||||
Letter frequencies pinched from
|
||||
http://www.math.cornell.edu/~mec/2003-2004/cryptography/subs/frequencies.html
|
||||
"""
|
||||
|
||||
acro = []
|
||||
# generate acro 3-8 characters long
|
||||
for i in range(1, random.randint(4, 9)):
|
||||
letter = random.randint(1, 182303)
|
||||
if letter <= 21912:
|
||||
acro.append('E')
|
||||
elif letter <= 38499:
|
||||
acro.append('T')
|
||||
elif letter <= 53309:
|
||||
acro.append('A')
|
||||
elif letter <= 67312:
|
||||
acro.append('O')
|
||||
elif letter <= 80630:
|
||||
acro.append('I')
|
||||
elif letter <= 93296:
|
||||
acro.append('N')
|
||||
elif letter <= 104746:
|
||||
acro.append('S')
|
||||
elif letter <= 115723:
|
||||
acro.append('R')
|
||||
elif letter <= 126518:
|
||||
acro.append('H')
|
||||
elif letter <= 134392:
|
||||
acro.append('D')
|
||||
elif letter <= 141645:
|
||||
acro.append('L')
|
||||
elif letter <= 146891:
|
||||
acro.append('U')
|
||||
elif letter <= 151834:
|
||||
acro.append('C')
|
||||
elif letter <= 156595:
|
||||
acro.append('M')
|
||||
elif letter <= 160795:
|
||||
acro.append('F')
|
||||
elif letter <= 164648:
|
||||
acro.append('Y')
|
||||
elif letter <= 168467:
|
||||
acro.append('W')
|
||||
elif letter <= 172160:
|
||||
acro.append('G')
|
||||
elif letter <= 175476:
|
||||
acro.append('P')
|
||||
elif letter <= 178191:
|
||||
acro.append('B')
|
||||
elif letter <= 180210:
|
||||
acro.append('V')
|
||||
elif letter <= 181467:
|
||||
acro.append('K')
|
||||
elif letter <= 181782:
|
||||
acro.append('X')
|
||||
elif letter <= 181987:
|
||||
acro.append('Q')
|
||||
elif letter <= 182175:
|
||||
acro.append('J')
|
||||
elif letter <= 182303:
|
||||
acro.append('Z')
|
||||
|
||||
return "".join(acro)
|
||||
|
||||
def _take_acro_submission(self, nick, submission):
|
||||
"""Take an acro submission and record it."""
|
||||
|
||||
if self.game.state == 2:
|
||||
sub_acro = self._turn_text_into_acro(submission)
|
||||
|
||||
if sub_acro == self.game.rounds[-1].acro:
|
||||
self.game.rounds[-1].submissions[nick] = submission
|
||||
return "your submission has been recorded. it replaced any old submission for this round."
|
||||
else:
|
||||
return "the current acro is '{0:s}', not '{1:s}'".format(self.game.rounds[-1].acro, sub_acro)
|
||||
|
||||
def _turn_text_into_acro(self, text):
|
||||
"""Turn text into an acronym."""
|
||||
|
||||
words = text.split()
|
||||
acro = []
|
||||
for w in words:
|
||||
acro.append(w[0].upper())
|
||||
return "".join(acro)
|
||||
|
||||
def _start_voting(self):
|
||||
"""Begin the voting period."""
|
||||
|
||||
self.game.state = 3
|
||||
self.game.rounds[-1].sub_shuffle = self.game.rounds[-1].submissions.keys()
|
||||
random.shuffle(self.game.rounds[-1].sub_shuffle)
|
||||
self.sendmsg(self.game.channel,
|
||||
"here are the results. vote with !acro vote [number]")
|
||||
self._print_round_acros()
|
||||
|
||||
thread.start_new_thread(self.thread_do_process_votes, ())
|
||||
|
||||
def _print_round_acros(self):
|
||||
"""Take the current round's acros and spit them to the channel."""
|
||||
|
||||
i = 0
|
||||
for s in self.game.rounds[-1].sub_shuffle:
|
||||
self.log.debug(str(i) + " is " + s)
|
||||
self.sendmsg(self.game.channel,
|
||||
" {0:d}: {1:s}".format(i+1, self.game.rounds[-1].submissions[s]))
|
||||
i += 1
|
||||
|
||||
def _take_acro_vote(self, nick, vote):
|
||||
"""Take an acro vote and record it."""
|
||||
|
||||
if self.game.state == 3:
|
||||
acros = self.game.rounds[-1].submissions
|
||||
if int(vote) > 0 and int(vote) <= len(acros):
|
||||
self.log.debug(vote + " is " + self.game.rounds[-1].sub_shuffle[int(vote)-1])
|
||||
key = self.game.rounds[-1].sub_shuffle[int(vote)-1]
|
||||
|
||||
if key != nick:
|
||||
self.game.rounds[-1].votes[nick] = key
|
||||
return "your vote has been recorded. it replaced any old vote for this round."
|
||||
else:
|
||||
return "you can't vote for yourself!"
|
||||
else:
|
||||
return "you must vote for 1-{0:d}".format(len(acros))
|
||||
|
||||
def _end_current_round(self):
|
||||
"""Clean up and output for ending the current round."""
|
||||
|
||||
self.game.state = 4
|
||||
self.sendmsg(self.game.channel,
|
||||
"voting's over! here are the scores for the round:")
|
||||
self._print_round_scores()
|
||||
self._add_round_scores_to_game_scores()
|
||||
|
||||
# delay a bit
|
||||
time.sleep(self.game.rounds[-1].seconds_to_pause)
|
||||
self._continue_or_quit()
|
||||
|
||||
def _print_round_scores(self):
|
||||
"""For the acros in the round, find the votes for them."""
|
||||
|
||||
i = 0
|
||||
for s in self.game.rounds[-1].submissions.keys():
|
||||
votes = filter(lambda x: x == s, self.game.rounds[-1].votes.values())
|
||||
self.sendmsg(self.game.channel,
|
||||
" {0:d} ({1:s}): {2:d}".format(i+1, s, len(votes)))
|
||||
i += 1
|
||||
|
||||
def _add_round_scores_to_game_scores(self):
|
||||
"""Apply the final round scores to the totall scores for the game."""
|
||||
|
||||
for s in self.game.rounds[-1].votes.values():
|
||||
votes = filter(lambda x: x == s, self.game.rounds[-1].votes.values())
|
||||
if s in self.game.scores.keys():
|
||||
self.game.scores[s] = self.game.scores[s] + len(votes)
|
||||
else:
|
||||
self.game.scores[s] = len(votes)
|
||||
|
||||
def _continue_or_quit(self):
|
||||
"""Decide whether the game should continue or quit."""
|
||||
|
||||
if self.game.state == 4:
|
||||
if self.game.quit == True:
|
||||
self._end_game()
|
||||
else:
|
||||
self._start_new_round()
|
||||
|
||||
def _end_game(self):
|
||||
"""Clean up the entire game."""
|
||||
|
||||
self.game.state = 0
|
||||
self.game.quit = False
|
||||
self.sendmsg(self.game.channel,
|
||||
"game's over! here are the final scores:")
|
||||
self._print_game_scores()
|
||||
|
||||
def _print_game_scores(self):
|
||||
"""Print the final calculated scores."""
|
||||
|
||||
for s in self.game.scores.keys():
|
||||
self.sendmsg(self.game.channel,
|
||||
" {0:s}: {1:d}".format(s, self.game.scores[s]))
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
80
ircbot/modules/Babelfish.py
Normal file
80
ircbot/modules/Babelfish.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Babelfish - go out to babelfish and translate sentences
|
||||
Copyright (C) 2012 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import re
|
||||
import urllib2
|
||||
from urllib import urlencode
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Babelfish(Module):
|
||||
|
||||
"""Class that translates text via Babelfish.
|
||||
|
||||
http://babelfish.yahoo.com/
|
||||
"""
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Handle IRC input."""
|
||||
|
||||
match = re.search('^!translate\s+(\S+)\s+(\S+)\s+(.*)$', what)
|
||||
if match:
|
||||
fromlang = match.group(1)
|
||||
tolang = match.group(2)
|
||||
text = match.group(3)
|
||||
|
||||
return self.irc.reply(event, self.translate(fromlang, tolang, text))
|
||||
|
||||
def translate(self, fromlang, tolang, text):
|
||||
|
||||
"""Translate text from fromlang to tolang, assuming it's a valid pair."""
|
||||
|
||||
langpair = '%s_%s' % (fromlang, tolang)
|
||||
|
||||
# do some text conversion
|
||||
text = text.replace('<', '< ') # babelfish blows up on e.g. <text> but < text> is fine
|
||||
|
||||
url = 'http://babelfish.yahoo.com/translate_txt'
|
||||
params = urlencode({'ei':'UTF-8', 'doit':'done', 'fr':'bf-home', 'intl':'1', 'tt':'urltext',
|
||||
'trtext':text, 'lp':langpair})
|
||||
req = urllib2.Request(url, params)
|
||||
req.add_header('Accept-Charset', 'UTF-8,*;q=0.5')
|
||||
res = urllib2.urlopen(req)
|
||||
content = res.read()
|
||||
start_idx = content.find('<div id="result"><div style="padding:')+45
|
||||
if start_idx > 0:
|
||||
end_idx = content.find('</div>', start_idx)
|
||||
translation = content[start_idx:end_idx]
|
||||
|
||||
# do some text conversion
|
||||
translation = translation.replace('"', '"')
|
||||
translation = translation.replace('&', '&')
|
||||
translation = translation.replace('<', '<')
|
||||
translation = translation.replace('>', '>')
|
||||
translation = translation.replace(''', '\'')
|
||||
translation = translation.replace(''', '\'')
|
||||
translation = translation.replace('< ', '<') # crappy attempt at undoing the safety above
|
||||
|
||||
return translation
|
||||
|
||||
if __name__ == '__main__':
|
||||
babelfish = Babelfish(None, None, None)
|
||||
print('\'' + babelfish.translate('en', 'ja', 'i can\'t read it, there aren\'t any words there') + '\'')
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
363
ircbot/modules/Countdown.py
Normal file
363
ircbot/modules/Countdown.py
Normal file
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Countdown - track and display time until events
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from dateutil.parser import *
|
||||
from dateutil.relativedelta import *
|
||||
from dateutil.tz import *
|
||||
import MySQLdb as mdb
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Countdown(Module):
|
||||
|
||||
"""Track when events will happen."""
|
||||
|
||||
def db_init(self):
|
||||
"""Set up the database tables, if they don't exist."""
|
||||
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if (version == None):
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE countdown_item (
|
||||
name VARCHAR(255) NOT NULL,
|
||||
source VARCHAR(255) NOT NULL,
|
||||
time TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (name, source)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci
|
||||
''')
|
||||
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def register_handlers(self):
|
||||
"""Hook handler functions into the IRC library."""
|
||||
|
||||
# register IRC regex handlers
|
||||
self.irc.add_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+add\s+(\S+)\s+(.*)$',
|
||||
self.add_item)
|
||||
self.irc.add_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+add\s+(\S+)\s+(.*)$',
|
||||
self.add_item)
|
||||
self.irc.add_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+remove\s+(\S+)$',
|
||||
self.remove_item)
|
||||
self.irc.add_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+remove\s+(\S+)$',
|
||||
self.remove_item)
|
||||
self.irc.add_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+list$',
|
||||
self.list_items)
|
||||
self.irc.add_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+list$',
|
||||
self.list_items)
|
||||
self.irc.add_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+(\S+)$',
|
||||
self.item_detail)
|
||||
self.irc.add_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+(\S+)$',
|
||||
self.item_detail)
|
||||
|
||||
def unregister_handlers(self):
|
||||
"""Unhook handler functions from the IRC library."""
|
||||
|
||||
# register IRC regex handlers
|
||||
self.irc.remove_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+add\s+(\S+)\s+(.*)$',
|
||||
self.add_item)
|
||||
self.irc.remove_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+add\s+(\S+)\s+(.*)$',
|
||||
self.add_item)
|
||||
self.irc.remove_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+remove\s+(\S+)$',
|
||||
self.remove_item)
|
||||
self.irc.remove_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+remove\s+(\S+)$',
|
||||
self.remove_item)
|
||||
self.irc.remove_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+list$',
|
||||
self.list_items)
|
||||
self.irc.remove_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+list$',
|
||||
self.list_items)
|
||||
self.irc.remove_global_regex_handler('pubmsg',
|
||||
r'^!countdown\s+(\S+)$',
|
||||
self.item_detail)
|
||||
self.irc.remove_global_regex_handler('privmsg',
|
||||
r'^!countdown\s+(\S+)$',
|
||||
self.item_detail)
|
||||
|
||||
def add_item(self, nick, userhost, event, from_admin, groups):
|
||||
"""Add a new item to track, and the datetime it occurs.
|
||||
|
||||
Args:
|
||||
nick source nickname (unused)
|
||||
userhost source userhost (unused)
|
||||
event IRC event, target used to associate item to a source
|
||||
from_admin whether or not the event came from an admin (unused)
|
||||
groups tuple length 2, the item name and the time of the item
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
name, time_str = groups
|
||||
time = parse(time_str, default=datetime.now().replace(tzinfo=tzlocal()))
|
||||
|
||||
# determine if source is a channel of a privmsg
|
||||
source = event.target()
|
||||
if source == self.irc.server.get_nickname():
|
||||
source = irclib.nm_to_n(event.source())
|
||||
|
||||
self._add_countdown_item_to_database(name, source,
|
||||
time.astimezone(tzutc()))
|
||||
self.log.debug("added countdown item '{0:s}' at "
|
||||
"{1:s}".format(name, time.isoformat()))
|
||||
replystr = "added countdown item '{0:s}'".format(name)
|
||||
return self.irc.reply(event, replystr)
|
||||
except Exception as e:
|
||||
self.log.error("could not add countdown item")
|
||||
self.log.exception(e)
|
||||
return self.irc.reply(event, ("could not add countdown item: "
|
||||
"{0:s}".format(str(e))))
|
||||
|
||||
def remove_item(self, nick, userhost, event, from_admin, groups):
|
||||
"""State when the provided item will occur.
|
||||
|
||||
Args:
|
||||
nick source nickname (unused)
|
||||
userhost source userhost (unused)
|
||||
event IRC event, target used to filter item names
|
||||
from_admin whether or not the event came from an admin (unused)
|
||||
groups tuple length 1, the item name to remove
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
name = groups[0]
|
||||
|
||||
# determine if source is a channel of a privmsg
|
||||
source = event.target()
|
||||
if source == self.irc.server.get_nickname():
|
||||
source = irclib.nm_to_n(event.source())
|
||||
|
||||
self._remove_countdown_item_from_database(name, source)
|
||||
replystr = "removed countdown item '{0:s}'".format(name)
|
||||
return self.irc.reply(event, replystr)
|
||||
except Exception: pass
|
||||
|
||||
def list_items(self, nick, userhost, event, from_admin, groups):
|
||||
"""State when the provided item will occur.
|
||||
|
||||
Args:
|
||||
nick source nickname (unused)
|
||||
userhost source userhost (unused)
|
||||
event IRC event, target used to filter item names
|
||||
from_admin whether or not the event came from an admin (unused)
|
||||
groups empty tuple (unused)
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
# determine if source is a channel of a privmsg
|
||||
source = event.target()
|
||||
if source == self.irc.server.get_nickname():
|
||||
source = irclib.nm_to_n(event.source())
|
||||
|
||||
cdlist = self._list_countdown_items(source)
|
||||
print(cdlist)
|
||||
liststr = "countdown items: {0:s}".format(", ".join(cdlist))
|
||||
return self.irc.reply(event, liststr)
|
||||
except Exception: pass
|
||||
|
||||
def item_detail(self, nick, userhost, event, from_admin, groups):
|
||||
"""State when the provided item will occur.
|
||||
|
||||
Args:
|
||||
nick source nickname (unused)
|
||||
userhost source userhost (unused)
|
||||
event IRC event, target used to filter item names
|
||||
from_admin whether or not the event came from an admin (unused)
|
||||
groups tuple length 1, the item name to look up
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
name = groups[0]
|
||||
|
||||
# determine if source is a channel of a privmsg
|
||||
source = event.target()
|
||||
if source == self.irc.server.get_nickname():
|
||||
source = irclib.nm_to_n(event.source())
|
||||
|
||||
time = self._get_countdown_item_time(name, source)
|
||||
if time:
|
||||
rdelta = relativedelta(time, datetime.now().replace(tzinfo=tzlocal()))
|
||||
relstr = "{0:s} will occur in ".format(name)
|
||||
if rdelta.years != 0:
|
||||
relstr += "{0:s} year{1:s} ".format(str(rdelta.years),
|
||||
"s" if rdelta.years != 1 else "")
|
||||
if rdelta.months != 0:
|
||||
relstr += "{0:s} month{1:s}, ".format(str(rdelta.months),
|
||||
"s" if rdelta.months != 1 else "")
|
||||
if rdelta.days != 0:
|
||||
relstr += "{0:s} day{1:s}, ".format(str(rdelta.days),
|
||||
"s" if rdelta.days != 1 else "")
|
||||
if rdelta.hours != 0:
|
||||
relstr += "{0:s} hour{1:s}, ".format(str(rdelta.hours),
|
||||
"s" if rdelta.hours != 1 else "")
|
||||
if rdelta.minutes != 0:
|
||||
relstr += "{0:s} minute{1:s}, ".format(str(rdelta.minutes),
|
||||
"s" if rdelta.minutes != 1 else "")
|
||||
if rdelta.seconds != 0:
|
||||
relstr += "{0:s} second{1:s}, ".format(str(rdelta.seconds),
|
||||
"s" if rdelta.seconds != 1 else "")
|
||||
# remove trailing comma from output
|
||||
return self.irc.reply(event, relstr[0:-2])
|
||||
except mdb.Error: pass
|
||||
|
||||
def _add_countdown_item_to_database(self, name, source, time):
|
||||
"""Add the given countdown item from the given source to the database.
|
||||
|
||||
Args:
|
||||
name the name of the item to add
|
||||
source the source (nick or channel) this item is from
|
||||
time the datetime of the item to add
|
||||
|
||||
"""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = '''
|
||||
INSERT INTO countdown_item (name, source, time)
|
||||
VALUES (%s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE time=%s
|
||||
'''
|
||||
cur.execute(statement, (name, source, time, time))
|
||||
db.commit()
|
||||
return cur.lastrowid
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error during adding countdown item")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _remove_countdown_item_from_database(self, name, source):
|
||||
"""Add the given countdown item from the given source to the database.
|
||||
|
||||
Args:
|
||||
name the name of the item to remove
|
||||
source the source (nick or channel) this item is from
|
||||
|
||||
"""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = '''
|
||||
DELETE FROM countdown_item WHERE name = %s
|
||||
AND source = %s
|
||||
'''
|
||||
cur.execute(statement, (name, source))
|
||||
db.commit()
|
||||
return
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error during removing countdown item")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _list_countdown_items(self, source):
|
||||
"""Retrieve the names of all countdown items in the database.
|
||||
|
||||
Args:
|
||||
source the source (nick or channel) to retrieve items for
|
||||
|
||||
Returns:
|
||||
The names of the items, as a list of strings.
|
||||
|
||||
"""
|
||||
|
||||
items = []
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = '''
|
||||
SELECT name FROM countdown_item
|
||||
WHERE source = %s
|
||||
'''
|
||||
cur.execute(query, (source,))
|
||||
results = cur.fetchall()
|
||||
for result in results:
|
||||
items.append(result['name'])
|
||||
|
||||
return items
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error while getting item time")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _get_countdown_item_time(self, name, source):
|
||||
"""Add the given countdown item from the given source to the database.
|
||||
|
||||
Args:
|
||||
name the name of the item to add
|
||||
source the source (nick or channel) this item is from
|
||||
|
||||
Returns:
|
||||
The datetime for the item in local time.
|
||||
|
||||
"""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = '''
|
||||
SELECT time FROM countdown_item
|
||||
WHERE name = %s AND source = %s
|
||||
'''
|
||||
cur.execute(query, (name, source))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return result['time'].replace(tzinfo=tzutc())
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error while getting item time")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
400
ircbot/modules/Dice.py
Normal file
400
ircbot/modules/Dice.py
Normal file
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
Dice - roll dice when asked, intended for RPGs
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import math
|
||||
import re
|
||||
import random
|
||||
|
||||
import ply.lex as lex
|
||||
import ply.yacc as yacc
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Dice(Module):
|
||||
|
||||
"""Roll simple or complex dice strings."""
|
||||
|
||||
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
|
||||
literals = ['#', '/', '+', '-', 'd']
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Set up XML-RPC methods."""
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
irc.xmlrpc_register_function(self.do_roll, "dice_roll")
|
||||
|
||||
def build(self):
|
||||
lex.lex(module=self)
|
||||
yacc.yacc(module=self)
|
||||
|
||||
t_TEXT = r'\s+[^;]+'
|
||||
t_ROLLSEP = r';\s*'
|
||||
|
||||
def t_NUMBER(self, t):
|
||||
r'\d+'
|
||||
t.value = int(t.value)
|
||||
return t
|
||||
|
||||
def t_error(self, t):
|
||||
t.lexer.skip(1)
|
||||
|
||||
precedence = (
|
||||
('left', 'ROLLSEP'),
|
||||
('left', '+', '-'),
|
||||
('right', 'd'),
|
||||
('left', '#'),
|
||||
('left', '/')
|
||||
)
|
||||
|
||||
output = ""
|
||||
|
||||
def roll_dice(self, keep, dice, size):
|
||||
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
|
||||
the actual roll. Returns a string representing the result.
|
||||
"""
|
||||
|
||||
a = range(dice)
|
||||
for i in range(dice):
|
||||
a[i] = random.randint(1, size)
|
||||
if keep != dice:
|
||||
b = sorted(a, reverse=True)
|
||||
b = b[0:keep]
|
||||
else:
|
||||
b = a
|
||||
total = sum(b)
|
||||
outstr = "[" + ",".join(str(i) for i in a) + "]"
|
||||
|
||||
return (total, outstr)
|
||||
|
||||
def process_roll(self, trials, mods, comment):
|
||||
"""Processes rolls coming from the parser.
|
||||
|
||||
This generates the inputs for the roll_dice() command, and returns
|
||||
the full string representing the whole current dice string (the part
|
||||
up to a semicolon or end of line).
|
||||
"""
|
||||
|
||||
output = ""
|
||||
repeat = 1
|
||||
if trials != None:
|
||||
repeat = trials
|
||||
for i in range(repeat):
|
||||
mode = 1
|
||||
total = 0
|
||||
curr_str = ""
|
||||
if i > 0:
|
||||
output += ", "
|
||||
for m in mods:
|
||||
keep = 0
|
||||
dice = 1
|
||||
res = 0
|
||||
# if m is a tuple, then it is a die roll
|
||||
# m[0] = (keep, num dice)
|
||||
# m[1] = num faces on the die
|
||||
if type(m) == tuple:
|
||||
if m[0] != None:
|
||||
if m[0][0] != None:
|
||||
keep = m[0][0]
|
||||
dice = m[0][1]
|
||||
size = m[1]
|
||||
if keep > dice or keep == 0:
|
||||
keep = dice
|
||||
if size < 1:
|
||||
output = "# of sides for die is incorrect: %d" % size
|
||||
return output
|
||||
if dice < 1:
|
||||
output = "# of dice is incorrect: %d" % dice
|
||||
return output
|
||||
res = self.roll_dice(keep, dice, size)
|
||||
curr_str += "%d%s" % (res[0], res[1])
|
||||
res = res[0]
|
||||
elif m == "+":
|
||||
mode = 1
|
||||
curr_str += "+"
|
||||
elif m == "-":
|
||||
mode = -1
|
||||
curr_str += "-"
|
||||
else:
|
||||
res = m
|
||||
curr_str += str(m)
|
||||
total += mode * res
|
||||
if repeat == 1:
|
||||
if comment != None:
|
||||
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
|
||||
else:
|
||||
output = "%d (%s)" % (total, curr_str)
|
||||
else:
|
||||
output += "%d (%s)" % (total, curr_str)
|
||||
if i == repeat - 1:
|
||||
if comment != None:
|
||||
output += " (%s)" % (comment.strip())
|
||||
return output
|
||||
|
||||
def p_roll_r(self, p):
|
||||
# Chain rolls together.
|
||||
|
||||
# General idea I had when creating this grammar: A roll string is a chain
|
||||
# of modifiers, which may be repeated for a certain number of trials. It can
|
||||
# have a comment that describes the roll
|
||||
# Multiple roll strings can be chained with semicolon
|
||||
|
||||
'roll : roll ROLLSEP roll'
|
||||
global output
|
||||
p[0] = p[1] + "; " + p[3]
|
||||
output = p[0]
|
||||
|
||||
def p_roll(self, p):
|
||||
# Parse a basic roll string.
|
||||
|
||||
'roll : trial modifier comment'
|
||||
global output
|
||||
mods = []
|
||||
if type(p[2]) == list:
|
||||
mods = p[2]
|
||||
else:
|
||||
mods = [p[2]]
|
||||
p[0] = self.process_roll(p[1], mods, p[3])
|
||||
output = p[0]
|
||||
|
||||
def p_roll_no_trials(self, p):
|
||||
# Parse a roll string without trials.
|
||||
|
||||
'roll : modifier comment'
|
||||
global output
|
||||
mods = []
|
||||
if type(p[1]) == list:
|
||||
mods = p[1]
|
||||
else:
|
||||
mods = [p[1]]
|
||||
p[0] = self.process_roll(None, mods, p[2])
|
||||
output = p[0]
|
||||
|
||||
def p_comment(self, p):
|
||||
# Parse a comment.
|
||||
|
||||
'''comment : TEXT
|
||||
|'''
|
||||
if len(p) == 2:
|
||||
p[0] = p[1]
|
||||
else:
|
||||
p[0] = None
|
||||
|
||||
def p_modifier(self, p):
|
||||
# Parse a modifier on a roll string.
|
||||
|
||||
'''modifier : modifier "+" modifier
|
||||
| modifier "-" modifier'''
|
||||
# Use append to prevent nested lists (makes dealing with this easier)
|
||||
if type(p[1]) == list:
|
||||
p[1].append(p[2])
|
||||
p[1].append(p[3])
|
||||
p[0] = p[1]
|
||||
elif type(p[3]) == list:
|
||||
p[3].insert(0, p[2])
|
||||
p[3].insert(0, p[1])
|
||||
p[0] = p[3]
|
||||
else:
|
||||
p[0] = [p[1], p[2], p[3]]
|
||||
|
||||
def p_die(self, p):
|
||||
# Return the left side before the "d", and the number of faces.
|
||||
|
||||
'modifier : left NUMBER'
|
||||
p[0] = (p[1], p[2])
|
||||
|
||||
def p_die_num(self, p):
|
||||
'modifier : NUMBER'
|
||||
p[0] = p[1]
|
||||
|
||||
def p_left(self, p):
|
||||
# Parse the number of dice we are rolling, and how many we are keeping.
|
||||
'left : keep dice'
|
||||
if p[1] == None:
|
||||
p[0] = [None, p[2]]
|
||||
else:
|
||||
p[0] = [p[1], p[2]]
|
||||
|
||||
def p_left_all(self, p):
|
||||
'left : dice'
|
||||
p[0] = [None, p[1]]
|
||||
|
||||
def p_left_e(self, p):
|
||||
'left :'
|
||||
p[0] = None
|
||||
|
||||
def p_total(self, p):
|
||||
'trial : NUMBER "#"'
|
||||
if len(p) > 1:
|
||||
p[0] = p[1]
|
||||
else:
|
||||
p[0] = None
|
||||
|
||||
def p_keep(self, p):
|
||||
'keep : NUMBER "/"'
|
||||
if p[1] != None:
|
||||
p[0] = p[1]
|
||||
else:
|
||||
p[0] = None
|
||||
|
||||
def p_dice(self, p):
|
||||
'dice : NUMBER "d"'
|
||||
p[0] = p[1]
|
||||
|
||||
def p_dice_one(self, p):
|
||||
'dice : "d"'
|
||||
p[0] = 1
|
||||
|
||||
def p_error(self, p):
|
||||
# Provide the user with something (albeit not much) when the roll can't be parsed.
|
||||
global output
|
||||
output = "Unable to parse roll"
|
||||
|
||||
def get_result(self):
|
||||
global output
|
||||
return output
|
||||
|
||||
def do_roll(self, dicestr):
|
||||
"""
|
||||
Roll some dice and get the result (with broken out rolls).
|
||||
|
||||
Keyword arguments:
|
||||
dicestr - format:
|
||||
N#X/YdS+M label
|
||||
N#: do the following roll N times (optional)
|
||||
X/: take the top X rolls of the Y times rolled (optional)
|
||||
Y : roll the die specified Y times (optional, defaults to 1)
|
||||
dS: roll a S-sided die
|
||||
+M: add M to the result (-M for subtraction) (optional)
|
||||
"""
|
||||
self.build()
|
||||
yacc.parse(dicestr)
|
||||
return self.get_result()
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
match = re.search('^!roll\s+(.*)$', what)
|
||||
if match:
|
||||
dicestr = match.group(1)
|
||||
reply = nick + ": " + self.do_roll(dicestr)
|
||||
# irc format it
|
||||
return self.irc.reply(event,
|
||||
re.sub(r"(\d+)(.*?\s+)(\(.*?\))", r"\1\214\3", reply))
|
||||
|
||||
match = re.search('^!ctech\s+(.*)$', what)
|
||||
if match:
|
||||
rollitrs = re.split(';\s*', match.group(1))
|
||||
reply = ""
|
||||
for count, roll in enumerate(rollitrs):
|
||||
pattern = '^(\d+)d(?:(\+|\-)(\d+))?(?:\s+(.*))?'
|
||||
regex = re.compile(pattern)
|
||||
matches = regex.search(roll)
|
||||
if matches is not None:
|
||||
dice = int(matches.group(1))
|
||||
modifier = 0
|
||||
|
||||
if matches.group(2) is not None and matches.group(3) is not None:
|
||||
if str(matches.group(2)) == '-':
|
||||
modifier = -1 * int(matches.group(3))
|
||||
else:
|
||||
modifier = int(matches.group(3))
|
||||
|
||||
result = roll + ': '
|
||||
|
||||
rolls = []
|
||||
for d in range(dice):
|
||||
rolls.append(random.randint(1, 10))
|
||||
rolls.sort()
|
||||
rolls.reverse()
|
||||
|
||||
# highest single die method
|
||||
method1 = rolls[0]
|
||||
|
||||
# highest set method
|
||||
method2 = 0
|
||||
rolling_sum = 0
|
||||
for i, r in enumerate(rolls):
|
||||
# if next roll is same as current, sum and continue, else see if sum is best so far
|
||||
if i+1 < len(rolls) and rolls[i+1] == r:
|
||||
if rolling_sum == 0:
|
||||
rolling_sum = r
|
||||
rolling_sum += r
|
||||
else:
|
||||
if rolling_sum > method2:
|
||||
method2 = rolling_sum
|
||||
rolling_sum = 0
|
||||
# check for set in progress (e.g. lots of 1s)
|
||||
if rolling_sum > method2:
|
||||
method2 = rolling_sum
|
||||
|
||||
# straight method
|
||||
method3 = 0
|
||||
rolling_sum = 0
|
||||
count = 0
|
||||
for i, r in enumerate(rolls):
|
||||
# if next roll is one less as current, sum and continue, else check len and see if sum is best so far
|
||||
if i+1 < len(rolls) and rolls[i+1] == r-1:
|
||||
if rolling_sum == 0:
|
||||
rolling_sum = r
|
||||
count += 1
|
||||
rolling_sum += r-1
|
||||
count += 1
|
||||
else:
|
||||
if count >= 3 and rolling_sum > method3:
|
||||
method3 = rolling_sum
|
||||
rolling_sum = 0
|
||||
# check for straight in progress (e.g. straight ending in 1)
|
||||
if count >= 3 and rolling_sum > method3:
|
||||
method3 = rolling_sum
|
||||
|
||||
# get best roll
|
||||
best = max([method1, method2, method3])
|
||||
|
||||
# check for critical failure
|
||||
botch = False
|
||||
ones = 0
|
||||
for r in rolls:
|
||||
if r == 1:
|
||||
ones += 1
|
||||
if ones >= math.ceil(float(len(rolls))/2):
|
||||
botch = True
|
||||
|
||||
if botch:
|
||||
result += 'BOTCH'
|
||||
else:
|
||||
result += str(best + modifier)
|
||||
rollres = ''
|
||||
for i,r in enumerate(rolls):
|
||||
rollres += str(r)
|
||||
if i is not len(rolls)-1:
|
||||
rollres += ','
|
||||
result += ' [' + rollres
|
||||
if modifier != 0:
|
||||
if modifier > 0:
|
||||
result += ' +' + str(modifier)
|
||||
else:
|
||||
result += ' -' + str(modifier * -1)
|
||||
result += ']'
|
||||
|
||||
reply += result
|
||||
if count is not len(rollitrs)-1:
|
||||
reply += "; "
|
||||
if reply is not "":
|
||||
return self.irc.reply(event, nick + ': ' + reply)
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
145
ircbot/modules/Dispatch.py
Normal file
145
ircbot/modules/Dispatch.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Dispatch - accept messages and route them to notification channels
|
||||
Copyright (C) 2012 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
import MySQLdb as mdb
|
||||
import os
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Dispatch(Module):
|
||||
|
||||
"""Accept messages via XML-RPC, send them to the configured IRC
|
||||
channel.
|
||||
|
||||
"""
|
||||
|
||||
def db_init(self):
|
||||
"""Set up the database tables, if they don't exist."""
|
||||
|
||||
try:
|
||||
db = self.get_db()
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version is None:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE dispatch_item (
|
||||
key_ VARCHAR(255) NOT NULL,
|
||||
dest VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (key_)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci
|
||||
''')
|
||||
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__,
|
||||
version)
|
||||
if version == 1:
|
||||
version = 2
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
ALTER TABLE dispatch_item DROP PRIMARY KEY, ADD PRIMARY KEY (key_, dest)
|
||||
''')
|
||||
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__,
|
||||
version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def xmlrpc_init(self):
|
||||
"""Expose dispatching interface."""
|
||||
|
||||
self.irc.xmlrpc_register_function(self.dispatch_message, "dispatch")
|
||||
|
||||
def register_handlers(self):
|
||||
"""Hook handler functions into the IRC library."""
|
||||
|
||||
pass
|
||||
|
||||
def unregister_handlers(self):
|
||||
"""Unhook handler functions from the IRC library."""
|
||||
|
||||
pass
|
||||
|
||||
def dispatch_message(self, key, message):
|
||||
"""Send a message, of the given type, to the configured channel.
|
||||
|
||||
Args:
|
||||
key the type of message. this will be converted uppercase
|
||||
message the message to send to the channel
|
||||
|
||||
Returns:
|
||||
list of all notified targets
|
||||
|
||||
"""
|
||||
|
||||
notified = []
|
||||
for key, dest in self._get_key_and_destination(key):
|
||||
if irclib.is_channel(dest):
|
||||
msg = "[{0:s}] {1:s}".format(key, message.encode('utf-8', 'ignore'))
|
||||
self.sendmsg(dest, msg)
|
||||
notified.append(dest)
|
||||
elif dest.find('FILE:') == 0:
|
||||
filename = dest.replace('FILE:', '')
|
||||
filename = os.path.abspath(filename)
|
||||
self.log.info("filename: {0:s}".format(filename))
|
||||
msg = "[{0:s}] {1:s}\n".format(key, message.encode('utf-8', 'ignore'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write(msg)
|
||||
|
||||
return notified
|
||||
|
||||
def _get_key_and_destination(self, key):
|
||||
"""Retrieve the configured channel for the given key.
|
||||
|
||||
Args:
|
||||
key the type of message. this will be converted uppercase
|
||||
|
||||
Returns:
|
||||
list of tuple of strings: (the key, the destination channel)
|
||||
|
||||
"""
|
||||
|
||||
targets = []
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = '''
|
||||
SELECT key_, dest FROM dispatch_item
|
||||
WHERE key_ = %s
|
||||
'''
|
||||
cur.execute(query, (key.upper(),))
|
||||
results = cur.fetchall()
|
||||
for result in results:
|
||||
targets.append((result['key_'], result['dest']))
|
||||
|
||||
return targets
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error while getting destination")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
79
ircbot/modules/Echo.py
Normal file
79
ircbot/modules/Echo.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Echo - repeat text
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Echo(Module):
|
||||
|
||||
"""Repeat provided text."""
|
||||
|
||||
def register_handlers(self):
|
||||
"""Hook handler functions into the IRC library."""
|
||||
|
||||
# register IRC regex handlers
|
||||
self.irc.add_global_regex_handler('pubmsg', r'^!echo\s+(.*)$',
|
||||
self.echo)
|
||||
self.irc.add_global_regex_handler('privmsg', r'^!echo\s+(.*)$',
|
||||
self.echo)
|
||||
|
||||
# register XML-RPC handlers
|
||||
def echo_wrap(msg):
|
||||
"""Get back the sent message.
|
||||
|
||||
Args:
|
||||
msg the message to get returned
|
||||
|
||||
Returns:
|
||||
msg
|
||||
|
||||
"""
|
||||
|
||||
return self.echo(None, None, None, False, (msg,))
|
||||
self.irc.xmlrpc_register_function(echo_wrap, 'echo')
|
||||
|
||||
def unregister_handlers(self):
|
||||
"""Unhook handler functions from the IRC library."""
|
||||
|
||||
self.irc.remove_global_regex_handler('pubmsg', r'^!echo\s+(.*)$',
|
||||
self.echo)
|
||||
self.irc.remove_global_regex_handler('privmsg', r'^!echo\s+(.*)$',
|
||||
self.echo)
|
||||
|
||||
def echo(self, nick, userhost, event, from_admin, groups):
|
||||
"""Return the message received.
|
||||
|
||||
Args:
|
||||
nick source nickname (unused)
|
||||
userhost source userhost (unused)
|
||||
event IRC event, used in IRC reply, or none for direct call
|
||||
from_admin whether or not the event came from an admin (unused)
|
||||
groups tuple length 1, the message to echo
|
||||
|
||||
"""
|
||||
|
||||
msg = groups[0]
|
||||
self.log.debug("replying with '{0:s}'".format(msg))
|
||||
|
||||
if event:
|
||||
return self.irc.reply(event, msg)
|
||||
else:
|
||||
return msg
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
67
ircbot/modules/EightBall.py
Normal file
67
ircbot/modules/EightBall.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
EightBall - consult the oracle
|
||||
Copyright (C) 2011 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
|
||||
from Module import Module
|
||||
|
||||
class EightBall(Module):
|
||||
|
||||
"""Return a random answer when asked a question."""
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Initialize the list of self.responses."""
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
self.responses = []
|
||||
|
||||
self.responses.append('As I see it, yes.')
|
||||
self.responses.append('It is certain.')
|
||||
self.responses.append('It is decidedly so.')
|
||||
self.responses.append('Most likely.')
|
||||
self.responses.append('Outlook good.')
|
||||
self.responses.append('Signs point to yes.')
|
||||
self.responses.append('Without a doubt.')
|
||||
self.responses.append('Yes.')
|
||||
self.responses.append('Yes - definitely.')
|
||||
self.responses.append('You may rely on it.')
|
||||
|
||||
self.responses.append('Reply hazy, try again.')
|
||||
self.responses.append('Ask again later.')
|
||||
self.responses.append('Better not tell you now.')
|
||||
self.responses.append('Cannot predict now.')
|
||||
self.responses.append('Concentrate and ask again.')
|
||||
|
||||
self.responses.append('Don\'t count on it.')
|
||||
self.responses.append('My reply is no.')
|
||||
self.responses.append('My sources say no.')
|
||||
self.responses.append('Outlook not so good.')
|
||||
self.responses.append('Very doubtful.')
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Determine the response to the question. Spoiler alert: it's random."""
|
||||
|
||||
match = re.search('^!8ball', what)
|
||||
if match:
|
||||
response = self.responses[random.randint(1,len(self.responses))-1]
|
||||
return self.irc.reply(event, response)
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
134
ircbot/modules/Facts.py
Normal file
134
ircbot/modules/Facts.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Facts - display facts, from within a category, from the database
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
|
||||
import MySQLdb as mdb
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Facts(Module):
|
||||
|
||||
"""Select a fact from the database.
|
||||
|
||||
Facts are categorized by a name, which may allow for random selection and so on.
|
||||
"""
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Set up XML-RPC methods."""
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
irc.xmlrpc_register_function(self._get_fact, "facts_get")
|
||||
|
||||
def db_init(self):
|
||||
"""Initialize database tables."""
|
||||
|
||||
# init the database if module isn't registered
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version == None:
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE facts_facts (
|
||||
id SERIAL,
|
||||
category VARCHAR(64) NOT NULL,
|
||||
fact LONGTEXT NOT NULL,
|
||||
who VARCHAR(64) NOT NULL,
|
||||
userhost VARCHAR(256) NOT NULL,
|
||||
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Add or retrieve a fact from the database."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
|
||||
match = re.search('^!facts\s+add\s+(\S+)\s+(.*)$', what)
|
||||
if match:
|
||||
category = match.group(1)
|
||||
fact = match.group(2)
|
||||
cur.execute('''INSERT INTO facts_facts (category, fact, who, userhost)
|
||||
VALUES (%s, %s, %s, %s)''', (category, fact, nick, userhost))
|
||||
db.commit()
|
||||
return self.irc.reply(event, category + ' added.')
|
||||
|
||||
match = re.search('^!facts\s+(\S+)\s+(.*)$', what)
|
||||
if match:
|
||||
category = match.group(1)
|
||||
regex = match.group(2)
|
||||
return self.irc.reply(event, self._get_fact(category, regex))
|
||||
|
||||
match = re.search('^!facts\s+(\S+)$', what)
|
||||
if match:
|
||||
category = match.group(1)
|
||||
return self.irc.reply(event, self._get_fact(category))
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error during add/retrieve")
|
||||
self.log.exception(e)
|
||||
return self.irc.reply(event, "database error during add/retrieve fact")
|
||||
finally: cur.close()
|
||||
|
||||
def _get_fact(self, category, search=""):
|
||||
"""
|
||||
Get a fact in the given category from the database.
|
||||
|
||||
Keyword arguments:
|
||||
category - the fact category to query
|
||||
search - the optional regex to match against within that category
|
||||
"""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
|
||||
if search == "":
|
||||
cur.execute("SELECT * FROM facts_facts WHERE category = %s",
|
||||
(category,))
|
||||
facts = cur.fetchall()
|
||||
else:
|
||||
cur.execute("SELECT * FROM facts_facts WHERE category = %s AND fact REGEXP %s",
|
||||
(category, search))
|
||||
facts = cur.fetchall()
|
||||
|
||||
if len(facts) > 0:
|
||||
fact = facts[random.randint(1,len(facts))-1]
|
||||
return fact['fact'].rstrip()
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error in _get_fact")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
181
ircbot/modules/IrcAdmin.py
Normal file
181
ircbot/modules/IrcAdmin.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
IrcAdmin - handle normal IRC functions one would expect
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from ConfigParser import NoOptionError
|
||||
import time
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
from Module import Module
|
||||
|
||||
class IrcAdmin(Module):
|
||||
|
||||
"""Support miscellaneous IRC stuff --- joining channels, changing the nick, etc."""
|
||||
|
||||
def register_handlers(self):
|
||||
self.irc.server.add_global_handler('pubmsg', self.on_pub_or_privmsg, self.priority())
|
||||
self.irc.server.add_global_handler('privmsg', self.on_pub_or_privmsg, self.priority())
|
||||
self.irc.server.add_global_handler('welcome', self.on_connect, self.priority())
|
||||
|
||||
def unregister_handlers(self):
|
||||
self.irc.server.remove_global_handler('pubmsg', self.on_pub_or_privmsg)
|
||||
self.irc.server.remove_global_handler('privmsg', self.on_pub_or_privmsg)
|
||||
self.irc.server.remove_global_handler('welcome', self.on_connect)
|
||||
|
||||
def on_connect(self, connection, event):
|
||||
"""Set up handlers when the bot has connected to IRC."""
|
||||
|
||||
# user modes
|
||||
try:
|
||||
nick = self.config.get('dr.botzo', 'nick')
|
||||
usermode = self.config.get('dr.botzo', 'usermode')
|
||||
connection.mode(nick, usermode)
|
||||
except NoOptionError: pass
|
||||
|
||||
# run automsg commands
|
||||
# TODO NOTE: if the bot is sending something that changes the vhost
|
||||
# (like 'hostserv on') we don't pick it up
|
||||
try:
|
||||
automsgs = self.config.get(self.__class__.__name__, 'automsg').split(',')
|
||||
for command in automsgs:
|
||||
connection.privmsg(command.split(' ')[0],
|
||||
' '.join(command.split(' ')[1:]))
|
||||
except NoOptionError: pass
|
||||
|
||||
# sleep for a bit before autojoining, if told to
|
||||
try:
|
||||
sleep = self.config.getint(self.__class__.__name__, 'sleep')
|
||||
time.sleep(sleep)
|
||||
except NoOptionError: pass
|
||||
|
||||
# join the specified channels
|
||||
try:
|
||||
autojoins = self.config.get(self.__class__.__name__, 'autojoin').split(',')
|
||||
for channel in autojoins:
|
||||
if irclib.is_channel(channel):
|
||||
connection.join(channel)
|
||||
except NoOptionError: pass
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Try all the admin methods."""
|
||||
|
||||
# TODO: sophisticate. also, document all of these
|
||||
|
||||
whats = what.split(' ')
|
||||
|
||||
if whats[0] == '!join' and admin_unlocked and len(whats) >= 2:
|
||||
return self.irc.reply(event, self.sub_join_channel(connection, event, nick, userhost, what, admin_unlocked))
|
||||
elif whats[0] == '!part' and admin_unlocked and len(whats) >= 2:
|
||||
return self.irc.reply(event, self.sub_part_channel(connection, event, nick, userhost, what, admin_unlocked))
|
||||
elif whats[0] == '!quit' and admin_unlocked:
|
||||
return self.irc.reply(event, self.sub_quit_irc(connection, event, nick, userhost, what, admin_unlocked))
|
||||
elif whats[0] == '!autojoin' and admin_unlocked and len(whats) >= 3:
|
||||
return self.irc.reply(event, self.sub_autojoin_manipulate(connection, event, nick, userhost, what, admin_unlocked))
|
||||
elif whats[0] == '!save' and admin_unlocked:
|
||||
self.irc.save_modules()
|
||||
self.irc.save_config()
|
||||
return self.irc.reply(event, 'Saved.')
|
||||
elif whats[0] == '!nick' and admin_unlocked and len(whats) >= 2:
|
||||
return self.irc.reply(event, self.sub_change_nick(connection, event, nick, userhost, what, admin_unlocked))
|
||||
elif whats[0] == '!load' and admin_unlocked and len(whats) >= 2:
|
||||
return self.irc.reply(event, self.sub_load_module(connection, event, nick, userhost, what, admin_unlocked))
|
||||
elif whats[0] == '!unload' and admin_unlocked and len(whats) >= 2:
|
||||
return self.irc.reply(event, self.sub_unload_module(connection, event, nick, userhost, what, admin_unlocked))
|
||||
elif whats[0] == '!modules':
|
||||
return self.irc.reply(event, self.sub_list_modules(connection, event, nick, userhost, what, admin_unlocked))
|
||||
|
||||
def sub_join_channel(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
whats = what.split(' ')
|
||||
|
||||
channel = whats[1]
|
||||
if irclib.is_channel(channel):
|
||||
connection.join(channel)
|
||||
replystr = 'Joined ' + channel + '.'
|
||||
return replystr
|
||||
|
||||
def sub_part_channel(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
whats = what.split(' ')
|
||||
target = event.target()
|
||||
|
||||
channel = whats[1]
|
||||
if irclib.is_channel(channel):
|
||||
connection.part(channel, ' '.join(whats[2:]))
|
||||
if target != channel:
|
||||
replystr = 'Parted ' + channel + '.'
|
||||
return replystr
|
||||
|
||||
def sub_quit_irc(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
whats = what.split(' ')
|
||||
self.irc.quit_irc(connection, ' '.join(whats[1:]))
|
||||
|
||||
def sub_autojoin_manipulate(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
whats = what.split(' ')
|
||||
|
||||
if whats[1] == 'add':
|
||||
try:
|
||||
# get existing list
|
||||
channel = whats[2]
|
||||
if irclib.is_channel(channel):
|
||||
channelset = set(self.config.get(self.__class__.__name__, 'autojoin').split(','))
|
||||
channelset.add(channel)
|
||||
self.config.set(self.__class__.__name__, 'autojoin', ','.join(channelset))
|
||||
replystr = 'Added ' + channel + ' to autojoin.'
|
||||
return replystr
|
||||
except NoOptionError: pass
|
||||
elif whats[1] == 'remove':
|
||||
try:
|
||||
# get existing list
|
||||
channel = whats[2]
|
||||
if irclib.is_channel(channel):
|
||||
channelset = set(self.config.get(self.__class__.__name__, 'autojoin').split(','))
|
||||
channelset.discard(channel)
|
||||
self.config.set(self.__class__.__name__, 'autojoin', ','.join(channelset))
|
||||
replystr = 'Removed ' + channel + ' from autojoin.'
|
||||
return replystr
|
||||
except NoOptionError: pass
|
||||
|
||||
def sub_change_nick(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
whats = what.split(' ')
|
||||
|
||||
newnick = whats[1]
|
||||
connection.nick(newnick)
|
||||
self.config.set('dr.botzo', 'nick', newnick)
|
||||
replystr = 'changed nickname'
|
||||
return replystr
|
||||
|
||||
def sub_load_module(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Load a module (in both the python and dr.botzo sense) if not
|
||||
already loaded.
|
||||
"""
|
||||
|
||||
whats = what.split(' ')
|
||||
return self.irc.load_module(whats[1])
|
||||
|
||||
def sub_unload_module(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Attempt to unload and del a module if it's loaded."""
|
||||
|
||||
whats = what.split(' ')
|
||||
return self.irc.unload_module(whats[1])
|
||||
|
||||
def sub_list_modules(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Get the list of loaded modules from DrBotIRC and display it."""
|
||||
|
||||
return ', '.join(self.irc.list_modules())
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
254
ircbot/modules/Karma.py
Normal file
254
ircbot/modules/Karma.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Karma - handle karma (++ and --) tracking
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from math import floor
|
||||
import re
|
||||
|
||||
import MySQLdb as mdb
|
||||
|
||||
from Module import Module
|
||||
|
||||
__author__ = "Mike Bloy <mike@bloy.org>"
|
||||
__date__ = "$Oct 23, 2010 11:12:33 AM$"
|
||||
|
||||
class Karma(Module):
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""
|
||||
Upon creation, determine the save file location
|
||||
"""
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
pattern = "(?:\((.+?)\)|(\S+))"
|
||||
karmapattern = pattern + '(\+\+|--|\+-|-\+)' + '(\s+|$)'
|
||||
querypattern = '^!rank\s+(.*)'
|
||||
reportpattern = '^!karma\s+report\s+(highest|lowest|positive|negative|top)'
|
||||
statpattern = '^!karma\s+stat\s+(.*)'
|
||||
|
||||
self.karmare = re.compile(karmapattern)
|
||||
self.queryre = re.compile(querypattern)
|
||||
self.reportre = re.compile(reportpattern)
|
||||
self.statre = re.compile(statpattern)
|
||||
|
||||
def db_init(self):
|
||||
# need to init the database if karma tables don't already exist
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if (version == None):
|
||||
# have to create the database tables
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE karma_log (
|
||||
id SERIAL,
|
||||
karma_key VARCHAR(128) NOT NULL,
|
||||
delta INTEGER NOT NULL,
|
||||
who VARCHAR(64) NOT NULL,
|
||||
userhost VARCHAR(256) NOT NULL,
|
||||
karmatime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('CREATE INDEX karma_log_key_ix ON karma_log (karma_key)')
|
||||
cur.execute('CREATE INDEX karma_log_who_ix ON karma_log (who)')
|
||||
cur.execute('''
|
||||
CREATE VIEW karma_values AS
|
||||
SELECT karma_key, SUM(delta) AS value
|
||||
FROM karma_log
|
||||
GROUP BY karma_key''')
|
||||
cur.execute('''
|
||||
CREATE VIEW karma_users AS
|
||||
SELECT who, COUNT(NULLIF(delta, -1)) AS pos,
|
||||
COUNT(NULLIF(delta, 1)) AS neg
|
||||
FROM karma_log GROUP BY who''')
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""look for karma strings at the start of messages"""
|
||||
|
||||
# don't return anything, do this attempt regardless
|
||||
if (self.karmare.search(what)):
|
||||
self.handle_karma_change(connection, nick, userhost, what)
|
||||
|
||||
if (self.queryre.search(what)):
|
||||
return self.irc.reply(event, self.handle_karma_query(connection, nick, userhost, what))
|
||||
elif (self.statre.search(what)):
|
||||
return self.irc.reply(event, self.handle_stat_query(connection, nick, userhost, what))
|
||||
elif (self.reportre.search(what)):
|
||||
return self.irc.reply(event, self.handle_report_query(connection, nick, userhost, what))
|
||||
|
||||
def handle_karma_change(self, connection, nick, userhost, what):
|
||||
"""
|
||||
handle the karma change and storage.
|
||||
"""
|
||||
|
||||
if self.karmare.search(what):
|
||||
matches = self.karmare.findall(what)
|
||||
for match in matches:
|
||||
key = match[0] if match[0] else match[1]
|
||||
value = match[2]
|
||||
|
||||
if (value == '++'):
|
||||
self.karma_modify(key, 1, nick, userhost)
|
||||
elif (value == '--'):
|
||||
self.karma_modify(key, -1, nick, userhost)
|
||||
elif (value == '+-'):
|
||||
self.karma_modify(key, 1, nick, userhost)
|
||||
self.karma_modify(key, -1, nick, userhost)
|
||||
elif (value == '-+'):
|
||||
self.karma_modify(key, -1, nick, userhost)
|
||||
self.karma_modify(key, 1, nick, userhost)
|
||||
|
||||
def karma_modify(self, key, value, nick, userhost):
|
||||
"""
|
||||
Go out to the database and update the karma value.
|
||||
"""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
sql = '''
|
||||
INSERT INTO karma_log (karma_key, delta, who, userhost)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
'''
|
||||
cur.execute(sql, (key.lower(), value, nick, userhost))
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error modifying karma")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def handle_report_query(self, connection, nick, userhost, what):
|
||||
match = self.reportre.search(what)
|
||||
report = match.group(1)
|
||||
|
||||
message = '{nick}: the desired report is not yet implemented'.format(nick=nick)
|
||||
query = None
|
||||
header = None
|
||||
if (report == 'highest'):
|
||||
query = 'SELECT karma_key AS who, value FROM karma_values ORDER BY value DESC LIMIT 5'
|
||||
header = 'Top 5 karma recipients:'
|
||||
elif (report == 'lowest'):
|
||||
query = 'SELECT karma_key AS who, value FROM karma_values ORDER BY value ASC LIMIT 5'
|
||||
header = 'Bottom 5 karma recipients:'
|
||||
elif (report == 'positive'):
|
||||
query = 'SELECT who, pos AS value FROM karma_users ORDER BY pos DESC LIMIT 5'
|
||||
header = 'Top 5 Optimists:'
|
||||
elif (report == 'negative'):
|
||||
query = 'SELECT who, neg AS value FROM karma_users ORDER BY neg DESC LIMIT 5'
|
||||
header = 'Top 5 Pessimists:'
|
||||
elif (report == 'top'):
|
||||
query = 'SELECT who, pos+neg AS value FROM karma_users ORDER BY value DESC LIMIT 5'
|
||||
header = 'Top 5 Total Karma Givers:'
|
||||
|
||||
if (query != None):
|
||||
db = self.get_db()
|
||||
list = []
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(query)
|
||||
result = cur.fetchone()
|
||||
while (result != None):
|
||||
list.append("{key} ({value})".format(key=result['who'], value=result['value']))
|
||||
result = cur.fetchone()
|
||||
list = ', '.join(list)
|
||||
message = '{header} {list}'.format(header=header, list=list)
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error during report query")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
return message
|
||||
|
||||
def handle_stat_query(self, connection, nick, userhost, what):
|
||||
match = self.statre.search(what)
|
||||
statnick = match.group(1)
|
||||
|
||||
db = self.get_db()
|
||||
reply = '{nick}: {statnick} has never given karma'.format(nick=nick, statnick=statnick)
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = '''
|
||||
SELECT pos, neg
|
||||
FROM karma_users
|
||||
WHERE who = :who
|
||||
'''
|
||||
cur.execute(query, {'who': statnick})
|
||||
value = cur.fetchone()
|
||||
if (value != None):
|
||||
pos = value[0]
|
||||
neg = value[1]
|
||||
total = pos+neg;
|
||||
reply = '{nick}: {statnick} has given {pos} postive karma and {neg} negative karma, for a total of {total} karma'.format(nick=nick, statnick=statnick, pos=pos, neg=neg, total=total)
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error during handle stat query")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
return reply
|
||||
|
||||
def handle_karma_query(self, connection, nick, userhost, what):
|
||||
match = self.queryre.search(what)
|
||||
key = match.group(1)
|
||||
|
||||
db = self.get_db()
|
||||
reply = '{nick}: {key} has no karma'.format(nick=nick, key=key)
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = '''
|
||||
SELECT value
|
||||
FROM karma_values
|
||||
WHERE karma_key = %s
|
||||
'''
|
||||
cur.execute(query, (key.lower(),))
|
||||
value = cur.fetchone()
|
||||
|
||||
if (value != None):
|
||||
query = '''
|
||||
SELECT count(*) FROM karma_values WHERE value > %s
|
||||
'''
|
||||
cur.execute(query, (value['value'],))
|
||||
rank = cur.fetchone()
|
||||
rank = rank['count(*)'] + 1;
|
||||
|
||||
reply = '{0:s}: {1:s} has {2:d} points of karma (rank {3:d})'.format(
|
||||
nick, key, int(floor(value['value'])), rank)
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error during handle karma query")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
return reply
|
||||
|
||||
if __name__ == "__main__":
|
||||
print "Hello World"
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
718
ircbot/modules/Markov.py
Normal file
718
ircbot/modules/Markov.py
Normal file
@@ -0,0 +1,718 @@
|
||||
"""
|
||||
Markov - Chatterbot via Markov chains for IRC
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import random
|
||||
import re
|
||||
import thread
|
||||
import time
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import MySQLdb as mdb
|
||||
|
||||
from extlib import irclib
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Markov(Module):
|
||||
|
||||
"""Create a chatterbot very similar to a MegaHAL, but simpler and
|
||||
implemented in pure Python. Proof of concept code from Ape.
|
||||
|
||||
Ape wrote: based on this:
|
||||
http://uswaretech.com/blog/2009/06/pseudo-random-text-markov-chains-python/
|
||||
and this:
|
||||
http://code.activestate.com/recipes/194364-the-markov-chain-algorithm/
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Create the Markov chainer, and learn text from a file if
|
||||
available.
|
||||
|
||||
"""
|
||||
|
||||
# set up some keywords for use in the chains --- don't change these
|
||||
# once you've created a brain
|
||||
self.start1 = '__start1'
|
||||
self.start2 = '__start2'
|
||||
self.stop = '__stop'
|
||||
|
||||
# set up regexes, for replying to specific stuff
|
||||
learnpattern = '^!markov\s+learn\s+(.*)$'
|
||||
replypattern = '^!markov\s+reply(\s+min=(\d+))?(\s+max=(\d+))?(\s+(.*)$|$)'
|
||||
|
||||
self.learnre = re.compile(learnpattern)
|
||||
self.replyre = re.compile(replypattern)
|
||||
|
||||
self.shut_up = False
|
||||
self.lines_seen = []
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
self.next_shut_up_check = 0
|
||||
self.next_chatter_check = 0
|
||||
thread.start_new_thread(self.thread_do, ())
|
||||
|
||||
irc.xmlrpc_register_function(self._generate_line,
|
||||
"markov_generate_line")
|
||||
|
||||
def db_init(self):
|
||||
"""Create the markov chain table."""
|
||||
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version == None:
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE markov_chatter_target (
|
||||
id SERIAL,
|
||||
target VARCHAR(256) NOT NULL,
|
||||
chance INTEGER NOT NULL DEFAULT 99999
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE markov_context (
|
||||
id SERIAL,
|
||||
context VARCHAR(256) NOT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE markov_target_to_context_map (
|
||||
id SERIAL,
|
||||
target VARCHAR(256) NOT NULL,
|
||||
context_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
FOREIGN KEY(context_id) REFERENCES markov_context(id)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE TABLE markov_chain (
|
||||
id SERIAL,
|
||||
k1 VARCHAR(128) NOT NULL,
|
||||
k2 VARCHAR(128) NOT NULL,
|
||||
v VARCHAR(128) NOT NULL,
|
||||
context_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
FOREIGN KEY(context_id) REFERENCES markov_context(id)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE INDEX markov_chain_keys_and_context_id_index
|
||||
ON markov_chain (k1, k2, context_id)''')
|
||||
|
||||
cur.execute('''
|
||||
CREATE INDEX markov_chain_value_and_context_id_index
|
||||
ON markov_chain (v, context_id)''')
|
||||
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__,
|
||||
version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def register_handlers(self):
|
||||
"""Handle pubmsg/privmsg, to learn and/or reply to IRC events."""
|
||||
|
||||
self.irc.server.add_global_handler('pubmsg', self.on_pub_or_privmsg,
|
||||
self.priority())
|
||||
self.irc.server.add_global_handler('privmsg', self.on_pub_or_privmsg,
|
||||
self.priority())
|
||||
self.irc.server.add_global_handler('pubmsg',
|
||||
self.learn_from_irc_event)
|
||||
self.irc.server.add_global_handler('privmsg',
|
||||
self.learn_from_irc_event)
|
||||
|
||||
def unregister_handlers(self):
|
||||
self.irc.server.remove_global_handler('pubmsg',
|
||||
self.on_pub_or_privmsg)
|
||||
self.irc.server.remove_global_handler('privmsg',
|
||||
self.on_pub_or_privmsg)
|
||||
self.irc.server.remove_global_handler('pubmsg',
|
||||
self.learn_from_irc_event)
|
||||
self.irc.server.remove_global_handler('privmsg',
|
||||
self.learn_from_irc_event)
|
||||
|
||||
def learn_from_irc_event(self, connection, event):
|
||||
"""Learn from IRC events."""
|
||||
|
||||
what = ''.join(event.arguments()[0])
|
||||
my_nick = connection.get_nickname()
|
||||
what = re.sub('^' + my_nick + '[:,]\s+', '', what)
|
||||
target = event.target()
|
||||
nick = irclib.nm_to_n(event.source())
|
||||
|
||||
if not irclib.is_channel(target):
|
||||
target = nick
|
||||
|
||||
self.lines_seen.append((nick, datetime.now()))
|
||||
|
||||
# don't learn from commands
|
||||
if self.learnre.search(what) or self.replyre.search(what):
|
||||
return
|
||||
|
||||
self._learn_line(what, target, event)
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Handle commands and inputs."""
|
||||
|
||||
target = event.target()
|
||||
|
||||
if self.learnre.search(what):
|
||||
return self.irc.reply(event, self.markov_learn(event,
|
||||
nick, userhost, what, admin_unlocked))
|
||||
elif self.replyre.search(what) and not self.shut_up:
|
||||
return self.irc.reply(event, self.markov_reply(event,
|
||||
nick, userhost, what, admin_unlocked))
|
||||
|
||||
if not self.shut_up:
|
||||
# not a command, so see if i'm being mentioned
|
||||
if re.search(connection.get_nickname(), what, re.IGNORECASE) is not None:
|
||||
addressed_pattern = '^' + connection.get_nickname() + '[:,]\s+(.*)'
|
||||
addressed_re = re.compile(addressed_pattern)
|
||||
if addressed_re.match(what):
|
||||
# i was addressed directly, so respond, addressing
|
||||
# the speaker
|
||||
self.lines_seen.append(('.self.said.', datetime.now()))
|
||||
return self.irc.reply(event, '{0:s}: {1:s}'.format(nick,
|
||||
self._generate_line(target, line=addressed_re.match(what).group(1))))
|
||||
else:
|
||||
# i wasn't addressed directly, so just respond
|
||||
self.lines_seen.append(('.self.said.', datetime.now()))
|
||||
return self.irc.reply(event, '{0:s}'.format(self._generate_line(target, line=what)))
|
||||
|
||||
def markov_learn(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Learn one line, as provided to the command."""
|
||||
|
||||
target = event.target()
|
||||
|
||||
if not irclib.is_channel(target):
|
||||
target = nick
|
||||
|
||||
match = self.learnre.search(what)
|
||||
if match:
|
||||
line = match.group(1)
|
||||
self._learn_line(line, target, event)
|
||||
|
||||
# return what was learned, for weird chaining purposes
|
||||
return line
|
||||
|
||||
def markov_reply(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Generate a reply to one line, without learning it."""
|
||||
|
||||
target = event.target()
|
||||
|
||||
if not irclib.is_channel(target):
|
||||
target = nick
|
||||
|
||||
match = self.replyre.search(what)
|
||||
if match:
|
||||
min_size = 15
|
||||
max_size = 30
|
||||
|
||||
if match.group(2):
|
||||
min_size = int(match.group(2))
|
||||
if match.group(4):
|
||||
max_size = int(match.group(4))
|
||||
|
||||
if match.group(5) != '':
|
||||
line = match.group(6)
|
||||
self.lines_seen.append(('.self.said.', datetime.now()))
|
||||
return self._generate_line(target, line=line, min_size=min_size, max_size=max_size)
|
||||
else:
|
||||
self.lines_seen.append(('.self.said.', datetime.now()))
|
||||
return self._generate_line(target, min_size=min_size, max_size=max_size)
|
||||
|
||||
def thread_do(self):
|
||||
"""Do various things."""
|
||||
|
||||
while not self.is_shutdown:
|
||||
self._do_shut_up_checks()
|
||||
self._do_random_chatter_check()
|
||||
time.sleep(1)
|
||||
|
||||
def _do_random_chatter_check(self):
|
||||
"""Randomly say something to a channel."""
|
||||
|
||||
# don't immediately potentially chatter, let the bot
|
||||
# join channels first
|
||||
if self.next_chatter_check == 0:
|
||||
self.next_chatter_check = time.time() + 600
|
||||
|
||||
if self.next_chatter_check < time.time():
|
||||
self.next_chatter_check = time.time() + 600
|
||||
|
||||
targets = self._get_chatter_targets()
|
||||
for t in targets:
|
||||
if t['chance'] > 0:
|
||||
a = random.randint(1, t['chance'])
|
||||
if a == 1:
|
||||
self.sendmsg(t['target'], self._generate_line(t['target']))
|
||||
|
||||
def _do_shut_up_checks(self):
|
||||
"""Check to see if we've been talking too much, and shut up if so."""
|
||||
|
||||
if self.next_shut_up_check < time.time():
|
||||
self.shut_up = False
|
||||
self.next_shut_up_check = time.time() + 30
|
||||
|
||||
last_30_sec_lines = []
|
||||
|
||||
for (nick, then) in self.lines_seen:
|
||||
rdelta = relativedelta(datetime.now(), then)
|
||||
if (rdelta.years == 0 and rdelta.months == 0 and rdelta.days == 0 and
|
||||
rdelta.hours == 0 and rdelta.minutes == 0 and rdelta.seconds <= 29):
|
||||
last_30_sec_lines.append((nick, then))
|
||||
|
||||
if len(last_30_sec_lines) >= 8:
|
||||
lines_i_said = len(filter(lambda (a, b): a == '.self.said.', last_30_sec_lines))
|
||||
if lines_i_said >= 8:
|
||||
self.shut_up = True
|
||||
targets = self._get_chatter_targets()
|
||||
for t in targets:
|
||||
self.sendmsg(t['target'],
|
||||
'shutting up for 30 seconds due to last 30 seconds of activity')
|
||||
|
||||
def _learn_line(self, line, target, event):
|
||||
"""Create Markov chains from the provided line."""
|
||||
|
||||
# set up the head of the chain
|
||||
k1 = self.start1
|
||||
k2 = self.start2
|
||||
|
||||
context_id = self._get_context_id_for_target(target)
|
||||
|
||||
# don't learn recursion
|
||||
if not event._recursing:
|
||||
words = line.split()
|
||||
if len(words) == 0:
|
||||
return line
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = 'INSERT INTO markov_chain (k1, k2, v, context_id) VALUES (%s, %s, %s, %s)'
|
||||
for word in words:
|
||||
cur.execute(statement, (k1, k2, word, context_id))
|
||||
k1, k2 = k2, word
|
||||
cur.execute(statement, (k1, k2, self.stop, context_id))
|
||||
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error learning line")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _generate_line(self, target, line='', min_size=15, max_size=30):
|
||||
"""Create a line, optionally using some text in a seed as a point in
|
||||
the chain.
|
||||
|
||||
Keyword arguments:
|
||||
target - the target to retrieve the context for (i.e. a channel or nick)
|
||||
line - the line to reply to, by picking a random word and seeding with it
|
||||
min_size - the minimum desired size in words. not guaranteed
|
||||
max_size - the maximum desired size in words. not guaranteed
|
||||
|
||||
"""
|
||||
|
||||
# if the limit is too low, there's nothing to do
|
||||
if (max_size <= 3):
|
||||
raise Exception("max_size is too small: %d" % max_size)
|
||||
|
||||
# if the min is too large, abort
|
||||
if (min_size > 20):
|
||||
raise Exception("min_size is too large: %d" % min_size)
|
||||
|
||||
seed_words = []
|
||||
# shuffle the words in the input
|
||||
seed_words = line.split()
|
||||
random.shuffle(seed_words)
|
||||
self.log.debug("seed words: {0:s}".format(seed_words))
|
||||
|
||||
# hit to generate a new seed word immediately if possible
|
||||
seed_word = None
|
||||
hit_word = None
|
||||
|
||||
context_id = self._get_context_id_for_target(target)
|
||||
|
||||
# start with an empty chain, and work from there
|
||||
gen_words = [self.start1, self.start2]
|
||||
|
||||
# build a response by creating multiple sentences
|
||||
while len(gen_words) < max_size + 2:
|
||||
# if we're past the min and on a stop, we can end
|
||||
if len(gen_words) > min_size + 2:
|
||||
if gen_words[-1] == self.stop:
|
||||
break
|
||||
|
||||
# pick a word from the shuffled seed words, if we need a new one
|
||||
if seed_word == hit_word:
|
||||
if len(seed_words) > 0:
|
||||
seed_word = seed_words.pop()
|
||||
self.log.debug("picked new seed word: "
|
||||
"{0:s}".format(seed_word))
|
||||
else:
|
||||
seed_word = None
|
||||
self.log.debug("ran out of seed words")
|
||||
|
||||
# if we have a stop, the word before it might need to be
|
||||
# made to look like a sentence end
|
||||
if gen_words[-1] == self.stop:
|
||||
# chop off the stop, temporarily
|
||||
gen_words = gen_words[:-1]
|
||||
|
||||
# we should have a real word, make it look like a
|
||||
# sentence end
|
||||
sentence_end = gen_words[-1]
|
||||
eos_punctuation = ['!', '?', ',', '.']
|
||||
if sentence_end[-1] not in eos_punctuation:
|
||||
random.shuffle(eos_punctuation)
|
||||
gen_words[-1] = sentence_end + eos_punctuation.pop()
|
||||
self.log.debug("monkeyed with end of sentence, it's "
|
||||
"now: {0:s}".format(gen_words[-1]))
|
||||
|
||||
# put the stop back on
|
||||
gen_words.append(self.stop)
|
||||
self.log.debug("gen_words: {0:s}".format(" ".join(gen_words)))
|
||||
|
||||
# first, see if we should start a new sentence. if so,
|
||||
# work backwards
|
||||
if gen_words[-1] in (self.start2, self.stop) and seed_word is not None and 0 == 1:
|
||||
# drop a stop, since we're starting another sentence
|
||||
if gen_words[-1] == self.stop:
|
||||
gen_words = gen_words[:-1]
|
||||
|
||||
# work backwards from seed_word
|
||||
working_backwards = []
|
||||
back_k2 = self._retrieve_random_k2_for_value(seed_word, context_id)
|
||||
if back_k2:
|
||||
found_word = seed_word
|
||||
if back_k2 == self.start2:
|
||||
self.log.debug("random further back was start2, swallowing")
|
||||
else:
|
||||
working_backwards.append(back_k2)
|
||||
working_backwards.append(found_word)
|
||||
self.log.debug("started working backwards with: {0:s}".format(found_word))
|
||||
self.log.debug("working_backwards: {0:s}".format(" ".join(working_backwards)))
|
||||
|
||||
# now work backwards until we randomly bump into a start
|
||||
# to steer the chainer away from spending too much time on
|
||||
# the weaker-context reverse chaining, we make max_size
|
||||
# a non-linear distribution, making it more likely that
|
||||
# some time is spent on better forward chains
|
||||
max_back = min(random.randint(1, max_size/2) + random.randint(1, max_size/2),
|
||||
max_size/4)
|
||||
self.log.debug("max_back: {0:d}".format(max_back))
|
||||
while len(working_backwards) < max_back:
|
||||
back_k2 = self._retrieve_random_k2_for_value(working_backwards[0], context_id)
|
||||
if back_k2 == self.start2:
|
||||
self.log.debug("random further back was start2, finishing")
|
||||
break
|
||||
elif back_k2:
|
||||
working_backwards.insert(0, back_k2)
|
||||
self.log.debug("added '{0:s}' to working_backwards".format(back_k2))
|
||||
self.log.debug("working_backwards: {0:s}".format(" ".join(working_backwards)))
|
||||
else:
|
||||
self.log.debug("nothing (at all!?) further back, finishing")
|
||||
break
|
||||
|
||||
gen_words += working_backwards
|
||||
self.log.debug("gen_words: {0:s}".format(" ".join(gen_words)))
|
||||
hit_word = gen_words[-1]
|
||||
else:
|
||||
# we are working forward, with either:
|
||||
# * a pair of words (normal path, filling out a sentence)
|
||||
# * start1, start2 (completely new chain, no seed words)
|
||||
# * stop (new sentence in existing chain, no seed words)
|
||||
self.log.debug("working forwards")
|
||||
forw_v = None
|
||||
if gen_words[-1] in (self.start2, self.stop):
|
||||
# case 2 or 3 above, need to work forward on a beginning
|
||||
# of a sentence (this is slow)
|
||||
if gen_words[-1] == self.stop:
|
||||
# remove the stop if it's there
|
||||
gen_words = gen_words[:-1]
|
||||
|
||||
new_sentence = self._create_chain_with_k1_k2(self.start1,
|
||||
self.start2,
|
||||
3, context_id,
|
||||
avoid_address=True)
|
||||
|
||||
if len(new_sentence) > 0:
|
||||
self.log.debug("started new sentence "
|
||||
"'{0:s}'".format(" ".join(new_sentence)))
|
||||
gen_words += new_sentence
|
||||
self.log.debug("gen_words: {0:s}".format(" ".join(gen_words)))
|
||||
else:
|
||||
# this is a problem. we started a sentence on
|
||||
# start1,start2, and still didn't find anything. to
|
||||
# avoid endlessly looping we need to abort here
|
||||
break
|
||||
else:
|
||||
if seed_word:
|
||||
self.log.debug("preferring: '{0:s}'".format(seed_word))
|
||||
forw_v = self._retrieve_random_v_for_k1_and_k2_with_pref(gen_words[-2],
|
||||
gen_words[-1],
|
||||
seed_word,
|
||||
context_id)
|
||||
else:
|
||||
forw_v = self._retrieve_random_v_for_k1_and_k2(gen_words[-2],
|
||||
gen_words[-1],
|
||||
context_id)
|
||||
|
||||
if forw_v:
|
||||
gen_words.append(forw_v)
|
||||
self.log.debug("added random word '{0:s}' to gen_words".format(forw_v))
|
||||
self.log.debug("gen_words: {0:s}".format(" ".join(gen_words)))
|
||||
hit_word = gen_words[-1]
|
||||
else:
|
||||
# append stop. this is an end to a sentence (since
|
||||
# we had non-start words to begin with)
|
||||
gen_words.append(self.stop)
|
||||
self.log.debug("nothing found, added stop")
|
||||
self.log.debug("gen_words: {0:s}".format(" ".join(gen_words)))
|
||||
|
||||
# chop off the seed data at the start
|
||||
gen_words = gen_words[2:]
|
||||
|
||||
if len(gen_words):
|
||||
# chop off the end text, if it was the keyword indicating an end of chain
|
||||
if gen_words[-1] == self.stop:
|
||||
gen_words = gen_words[:-1]
|
||||
else:
|
||||
self.log.warning("after all this we have an empty list of words. "
|
||||
"there probably isn't any data for this context")
|
||||
|
||||
return ' '.join(gen_words)
|
||||
|
||||
def _retrieve_random_v_for_k1_and_k2(self, k1, k2, context_id):
|
||||
"""Get one v for a given k1,k2."""
|
||||
|
||||
self.log.debug("searching with '{0:s}','{1:s}'".format(k1, k2))
|
||||
values = []
|
||||
db = self.get_db()
|
||||
try:
|
||||
query = '''
|
||||
SELECT v FROM markov_chain AS r1
|
||||
JOIN (
|
||||
SELECT (RAND() * (SELECT MAX(id) FROM markov_chain)) AS id
|
||||
) AS r2
|
||||
WHERE r1.k1 = %s
|
||||
AND r1.k2 = %s
|
||||
AND r1.context_id = %s
|
||||
ORDER BY r1.id >= r2.id DESC, r1.id ASC
|
||||
LIMIT 1
|
||||
'''
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(query, (k1, k2, context_id))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
self.log.debug("found '{0:s}'".format(result['v']))
|
||||
return result['v']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error in _retrieve_random_v_for_k1_and_k2")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _retrieve_random_v_for_k1_and_k2_with_pref(self, k1, k2, prefer, context_id):
|
||||
"""Get one v for a given k1,k2.
|
||||
|
||||
Prefer that the result be prefer, if it's found.
|
||||
|
||||
"""
|
||||
|
||||
self.log.debug("searching with '{0:s}','{1:s}', prefer "
|
||||
"'{2:s}'".format(k1, k2, prefer))
|
||||
values = []
|
||||
db = self.get_db()
|
||||
try:
|
||||
query = '''
|
||||
SELECT v FROM markov_chain AS r1
|
||||
JOIN (
|
||||
SELECT (RAND() * (SELECT MAX(id) FROM markov_chain)) AS id
|
||||
) AS r2
|
||||
WHERE r1.k1 = %s
|
||||
AND r1.k2 = %s
|
||||
AND r1.context_id = %s
|
||||
ORDER BY r1.id >= r2.id DESC, r1.v = %s DESC, r1.id ASC
|
||||
LIMIT 1
|
||||
'''
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(query, (k1, k2, context_id, prefer))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
self.log.debug("found '{0:s}'".format(result['v']))
|
||||
return result['v']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error in _retrieve_random_v_for_k1_and_k2_with_pref")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _retrieve_random_k2_for_value(self, v, context_id):
|
||||
"""Get one k2 for a given value."""
|
||||
|
||||
values = []
|
||||
db = self.get_db()
|
||||
try:
|
||||
query = '''
|
||||
SELECT k2 FROM markov_chain AS r1
|
||||
JOIN (
|
||||
SELECT (RAND() * (SELECT MAX(id) FROM markov_chain)) AS id
|
||||
) AS r2
|
||||
WHERE r1.v = %s
|
||||
AND r1.context_id = %s
|
||||
ORDER BY r1.id >= r2.id DESC, r1.id ASC
|
||||
LIMIT 1
|
||||
'''
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(query, (v, context_id))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return result['k2']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error in _retrieve_random_k2_for_value")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _create_chain_with_k1_k2(self, k1, k2, length, context_id,
|
||||
avoid_address=False):
|
||||
"""Create a chain of the given length, using k1,k2.
|
||||
|
||||
k1,k2 does not appear in the resulting chain.
|
||||
|
||||
"""
|
||||
|
||||
chain = [k1, k2]
|
||||
self.log.debug("creating chain for {0:s},{1:s}".format(k1, k2))
|
||||
|
||||
for _ in range(length):
|
||||
v = self._retrieve_random_v_for_k1_and_k2(chain[-2],
|
||||
chain[-1],
|
||||
context_id)
|
||||
if v:
|
||||
chain.append(v)
|
||||
|
||||
# check for addresses (the "whoever:" in
|
||||
# __start1 __start2 whoever: some words)
|
||||
addressing_suffixes = [':', ',']
|
||||
if len(chain) > 2 and chain[2][-1] in addressing_suffixes and avoid_address:
|
||||
return chain[3:]
|
||||
elif len(chain) > 2:
|
||||
return chain[2:]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _get_chatter_targets(self):
|
||||
"""Get all possible chatter targets."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
# need to create our own db object, since this is likely going to be in a new thread
|
||||
query = 'SELECT target, chance FROM markov_chatter_target'
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(query)
|
||||
results = cur.fetchall()
|
||||
return results
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error in _get_chatter_targets")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _get_context_id_for_target(self, target):
|
||||
"""Get the context ID for the desired/input target."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
query = '''
|
||||
SELECT mc.id FROM markov_context mc
|
||||
INNER JOIN markov_target_to_context_map mt
|
||||
ON mt.context_id = mc.id
|
||||
WHERE mt.target = %s
|
||||
'''
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(query, (target,))
|
||||
result = cur.fetchone()
|
||||
db.close()
|
||||
if result:
|
||||
return result['id']
|
||||
else:
|
||||
# auto-generate a context to keep things private
|
||||
self._add_context_for_target(target)
|
||||
return self._get_context_id_for_target(target)
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error in _get_context_id_for_target")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _add_context_for_target(self, target):
|
||||
"""Create a new context for the desired/input target."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
statement = 'INSERT INTO markov_context (context) VALUES (%s)'
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(statement, (target,))
|
||||
statement = '''
|
||||
INSERT INTO markov_target_to_context_map (target, context_id)
|
||||
VALUES (%s, (SELECT id FROM markov_context WHERE context = %s))
|
||||
'''
|
||||
cur.execute(statement, (target, target))
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error in _add_context_for_target")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
try:
|
||||
query = '''
|
||||
SELECT mc.id FROM markov_context mc
|
||||
INNER JOIN markov_target_to_context_map mt
|
||||
ON mt.context_id = mc.id
|
||||
WHERE mt.target = %s
|
||||
'''
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute(query, (target,))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return result['id']
|
||||
else:
|
||||
# auto-generate a context to keep things private
|
||||
self._add_context_for_target(target)
|
||||
return self._get_context_id_for_target(target)
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error in _get_context_id_for_target")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
121
ircbot/modules/Pi.py
Normal file
121
ircbot/modules/Pi.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Pi - calculate pi over time via the monte carlo method. idea borrowed from #linode
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
import re
|
||||
|
||||
import MySQLdb as mdb
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Pi(Module):
|
||||
|
||||
"""Use the Monte Carlo method to approximate pi.
|
||||
|
||||
Each time this method is called, it calculates a random point inside a
|
||||
square on the xy plane. If that point also falls within a circle bound
|
||||
within that square, it is added to the count of points inside. Over time,
|
||||
4 * count_inside / count approaches pi.
|
||||
|
||||
Idea from #linode on OFTC.
|
||||
Code from http://www.eveandersson.com/pi/monte-carlo-circle
|
||||
|
||||
"""
|
||||
|
||||
def db_init(self):
|
||||
"""Initialize database tables."""
|
||||
|
||||
# init the database if pi table doesn't exist
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version == None:
|
||||
# create tables
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE pi_log (
|
||||
id SERIAL,
|
||||
count_inside INTEGER NOT NULL,
|
||||
count_total INTEGER NOT NULL,
|
||||
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE VIEW pi_latest_pi AS
|
||||
SELECT count_inside, count_total
|
||||
FROM pi_log
|
||||
ORDER BY id DESC
|
||||
''')
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
match = re.search('^!pi$', what)
|
||||
if match:
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('SELECT * FROM pi_latest_pi')
|
||||
datum = cur.fetchone()
|
||||
|
||||
if datum == None:
|
||||
count_inside = 0
|
||||
count = 0
|
||||
else:
|
||||
# load values
|
||||
count_inside = datum['count_inside']
|
||||
count = datum['count_total']
|
||||
|
||||
x = random.random()
|
||||
y = random.random()
|
||||
inside = False
|
||||
d = math.hypot(x,y)
|
||||
if d < 1:
|
||||
inside = True
|
||||
count_inside += 1
|
||||
count += 1
|
||||
pi = 4.0 * count_inside / count
|
||||
|
||||
cur.execute('INSERT INTO pi_log (count_inside, count_total) VALUES (%s,%s)',
|
||||
(count_inside, count))
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error doing pi stuff")
|
||||
self.log.exception(e)
|
||||
return self.irc.reply(event,
|
||||
"database error while estimating pi: {0:s}".format(str(e)))
|
||||
finally: cur.close()
|
||||
|
||||
return self.irc.reply(event,
|
||||
"({0:.10f}, {1:.10f}) is {2}within the unit circle. "\
|
||||
"pi is {5:.10f}. (i:{3:d} p:{4:d})".format(x, y,
|
||||
"" if inside else "not ",
|
||||
count_inside, count, pi))
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
105
ircbot/modules/Seen.py
Normal file
105
ircbot/modules/Seen.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Seen - track when a person speaks, and allow data to be queried
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from dateutil.tz import *
|
||||
import MySQLdb as mdb
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Seen(Module):
|
||||
|
||||
"""Track when people say things in public channels, and report on it."""
|
||||
|
||||
def db_init(self):
|
||||
"""Create the table to store seen data."""
|
||||
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version == None:
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE seen_nicks (
|
||||
nick VARCHAR(64) NOT NULL,
|
||||
location VARCHAR(64) NOT NULL,
|
||||
host VARCHAR(256) NOT NULL,
|
||||
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
what LONGTEXT NOT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
''')
|
||||
cur.execute('''
|
||||
CREATE UNIQUE INDEX seen_nicks_nick_and_location_index
|
||||
ON seen_nicks (nick, location)
|
||||
''')
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Track pubmsg/privmsg events, and if asked, report on someone."""
|
||||
|
||||
where = event.target()
|
||||
|
||||
db = self.get_db()
|
||||
# whatever it is, store it
|
||||
try:
|
||||
# if there's no where, this is probably a sub-command. don't learn it
|
||||
if where:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = 'REPLACE INTO seen_nicks (nick, location, host, what) VALUES (%s, %s, %s, %s)'
|
||||
cur.execute(statement, (nick, where, userhost, what))
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error storing seen data")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
match = re.search('^!seen\s+(\S+)$', what)
|
||||
if match:
|
||||
nick = match.group(1)
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = 'SELECT * FROM seen_nicks WHERE nick = %s AND location = %s'
|
||||
cur.execute(query, (nick,where))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
seentime = result['time'].replace(tzinfo=tzlocal())
|
||||
replystr = 'last saw {0:s} in {3:s} at {1:s} saying \'{2:s}\'.'.format(result['nick'], seentime.astimezone(tzlocal()).strftime('%Y/%m/%d %H:%M:%S %Z'), result['what'], result['location'])
|
||||
return self.irc.reply(event, replystr)
|
||||
else:
|
||||
return self.irc.reply(event, 'i have not seen {0:s} in {1:s}.'.format(nick, where))
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error retrieving seen data")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
1187
ircbot/modules/Storycraft.py
Normal file
1187
ircbot/modules/Storycraft.py
Normal file
File diff suppressed because it is too large
Load Diff
137
ircbot/modules/TextTransform.py
Normal file
137
ircbot/modules/TextTransform.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
TextTransform - assorted text transformations (e.g. rot13)
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import re
|
||||
|
||||
from Module import Module
|
||||
|
||||
class TextTransform(Module):
|
||||
"""
|
||||
Do a number of text transformations, like rot13.
|
||||
"""
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""
|
||||
Pass the real work on to one of our methods and see if any bite.
|
||||
"""
|
||||
|
||||
# if the caller replied, this will be the reply and they'll have returned true
|
||||
reply = ['']
|
||||
|
||||
if self.rot13(what, reply):
|
||||
return self.irc.reply(event, reply[0])
|
||||
elif self.base64(what, reply):
|
||||
return self.irc.reply(event, reply[0])
|
||||
elif self.upper(what, reply):
|
||||
return self.irc.reply(event, reply[0])
|
||||
elif self.lower(what, reply):
|
||||
return self.irc.reply(event, reply[0])
|
||||
elif self.al_bhed(what, reply):
|
||||
return self.irc.reply(event, reply[0])
|
||||
|
||||
def rot13(self, what, reply):
|
||||
"""
|
||||
Apply a rot13 method to the text if first word is 'rot13'.
|
||||
"""
|
||||
|
||||
match = re.search('^!rot13\s+(.*)$', what)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
reply[0] = text.encode('rot13', 'ignore')
|
||||
return True
|
||||
|
||||
def base64(self, what, reply):
|
||||
"""
|
||||
Encode/decode base64 string.
|
||||
"""
|
||||
|
||||
match = re.search('^!base64\s+encode\s+(.*)$', what)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
reply[0] = base64.encodestring(text).replace('\n','')
|
||||
return True
|
||||
|
||||
match = re.search('^!base64\s+decode\s+(.*)$', what)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
reply[0] = base64.decodestring(text).replace('\n','')
|
||||
return True
|
||||
|
||||
def upper(self, what, reply):
|
||||
"""
|
||||
Convert a string to uppercase.
|
||||
"""
|
||||
|
||||
match = re.search('^!upper\s+(.*)$', what)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
reply[0] = text.upper()
|
||||
return True
|
||||
|
||||
def lower(self, what, reply):
|
||||
"""
|
||||
Convert a string to lowercase.
|
||||
"""
|
||||
|
||||
match = re.search('^!lower\s+(.*)$', what)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
reply[0] = text.lower()
|
||||
return True
|
||||
|
||||
def al_bhed(self, what, reply):
|
||||
"""
|
||||
Convert a string to al bhed.
|
||||
"""
|
||||
|
||||
cipher = {'A':'Y','B':'P','C':'L','D':'T','E':'A','F':'V','G':'K','H':'R','I':'E','J':'Z','K':'G','L':'M','M':'S','N':'H','O':'U','P':'B','Q':'X','R':'N','S':'C','T':'D','U':'I','V':'J','W':'F','X':'Q','Y':'O','Z':'W'}
|
||||
decipher = dict([(v,k) for (k,v) in cipher.iteritems()])
|
||||
|
||||
match = re.search('^!al-bhed\s+(.*)$', what)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
trans = []
|
||||
for i in range(len(text)):
|
||||
if text[i] in cipher:
|
||||
trans.append(cipher[text[i]])
|
||||
elif text[i].upper() in cipher:
|
||||
trans.append(cipher[text[i].upper()].lower())
|
||||
else:
|
||||
trans.append(text[i])
|
||||
|
||||
reply[0] = "".join(trans)
|
||||
return True
|
||||
|
||||
match = re.search('^!deal-bhed\s+(.*)$', what)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
trans = []
|
||||
for i in range(len(text)):
|
||||
if text[i] in decipher:
|
||||
trans.append(decipher[text[i]])
|
||||
elif text[i].upper() in cipher:
|
||||
trans.append(decipher[text[i].upper()].lower())
|
||||
else:
|
||||
trans.append(text[i])
|
||||
|
||||
reply[0] = "".join(trans)
|
||||
return True
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
107
ircbot/modules/TopicDump.py
Normal file
107
ircbot/modules/TopicDump.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
TopicDump - periodically write out the topic of channels
|
||||
Copyright (C) 2013 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
import MySQLdb as mdb
|
||||
import os
|
||||
|
||||
from Module import Module
|
||||
|
||||
class TopicDump(Module):
|
||||
|
||||
"""For each topic the bot is in, output the topic to a file."""
|
||||
|
||||
def db_init(self):
|
||||
"""Set up the database tables, if they don't exist."""
|
||||
|
||||
db = self.get_db()
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
try:
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version is None:
|
||||
version = 1
|
||||
cur.execute('''
|
||||
CREATE TABLE topicdump_config (
|
||||
destdir VARCHAR(255) NOT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci
|
||||
''')
|
||||
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__,
|
||||
version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def register_handlers(self):
|
||||
"""Hook handler functions into the IRC library."""
|
||||
|
||||
self.irc.server.add_global_handler('topic', self._on_topic)
|
||||
|
||||
def unregister_handlers(self):
|
||||
"""Unhook handler functions from the IRC library."""
|
||||
|
||||
self.irc.server.remove_global_handler('topic', self._on_topic)
|
||||
|
||||
def _on_topic(self, connection, event):
|
||||
"""Write topics to a file named after the source channel."""
|
||||
|
||||
self.log.debug("topic change")
|
||||
self.log.debug(event.arguments()[0])
|
||||
self.log.debug(event.target())
|
||||
|
||||
target_dir = self._get_destdir()
|
||||
if target_dir:
|
||||
target_file = '{0:s}.topic'.format(event.target())
|
||||
target = os.path.abspath(os.path.join(target_dir, target_file))
|
||||
self.log.debug("target file: {0:s}".format(target))
|
||||
|
||||
self.log.info("writing topic to {0:s}".format(target))
|
||||
with open(target, 'w') as f:
|
||||
f.write(event.arguments()[0] + '\n')
|
||||
|
||||
def _get_destdir(self):
|
||||
"""Retrieve the destination directory from the database.
|
||||
|
||||
Returns:
|
||||
string: the destination to write topics to
|
||||
|
||||
"""
|
||||
|
||||
target = None
|
||||
db = self.get_db()
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
try:
|
||||
query = '''
|
||||
SELECT destdir FROM topicdump_config
|
||||
'''
|
||||
cur.execute(query, ())
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
target = result['destdir']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error while getting destination")
|
||||
self.log.exception(e)
|
||||
finally: cur.close()
|
||||
|
||||
return target
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
426
ircbot/modules/Twitter.py
Normal file
426
ircbot/modules/Twitter.py
Normal file
@@ -0,0 +1,426 @@
|
||||
"""
|
||||
Twitter - access to Twitter through bot commands
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import re
|
||||
import thread
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
import MySQLdb as mdb
|
||||
import twython
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Twitter(Module):
|
||||
|
||||
"""Access Twitter via the bot as an authenticated client."""
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Prepare for oauth stuff (but don't execute it yet)."""
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
# setup regexes
|
||||
getstatuspattern = "^!twitter\s+getstatus(\s+nosource)?(\s+noid)?\s+(\S+)$"
|
||||
getuserstatuspattern = "^!twitter\s+getuserstatus(\s+nosource)?(\s+noid)?\s+(\S+)(\s+.*|$)"
|
||||
tweetpattern = "^!twitter\s+tweet\s+(.*)"
|
||||
gettokenpattern = "^!twitter\s+gettoken$"
|
||||
authpattern = "^!twitter\s+auth\s+(\S+)$"
|
||||
replytopattern = "^!twitter\s+replyto\s+(\S+)\s+(.*)"
|
||||
|
||||
self.getstatusre = re.compile(getstatuspattern)
|
||||
self.getuserstatusre = re.compile(getuserstatuspattern)
|
||||
self.tweetre = re.compile(tweetpattern)
|
||||
self.gettokenre = re.compile(gettokenpattern)
|
||||
self.authre = re.compile(authpattern)
|
||||
self.replytore = re.compile(replytopattern)
|
||||
|
||||
# prep twitter
|
||||
self.consumer_key = 'N2aSGxBP8t3cCgWyF1B2Aw'
|
||||
self.consumer_secret = '0aQPEV4K3MMpicfi2lDtCP5pvjsKaqIpfuWtsPzx8'
|
||||
|
||||
# settings
|
||||
# force timeline check to wait 5 minutes (for channel joins and antispam)
|
||||
self.next_timeline_check = time.time() + 300
|
||||
|
||||
self.authed = False
|
||||
# try getting the stored auth tokens and logging in
|
||||
(oauth_token, oauth_token_secret) = self._retrieve_stored_auth_tokens()
|
||||
if oauth_token is not None and oauth_token_secret is not None:
|
||||
self.twit = twython.Twython(self.consumer_key, self.consumer_secret, oauth_token, oauth_token_secret)
|
||||
if self.twit.verify_credentials():
|
||||
self.authed = True
|
||||
# print timeline stuff. this will set up the appropriate timer
|
||||
self._check_self_timeline()
|
||||
self.log.debug("Logged in to Twitter with saved token.")
|
||||
else:
|
||||
self.twit = twython.Twython(self.consumer_key, self.consumer_secret)
|
||||
else:
|
||||
self.twit = twython.Twython(self.consumer_key, self.consumer_secret)
|
||||
|
||||
thread.start_new_thread(self.thread_do, ())
|
||||
|
||||
def db_init(self):
|
||||
"""Set up the settings table."""
|
||||
|
||||
# init the table if it doesn't exist
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version == None or version < 1:
|
||||
db = self.get_db()
|
||||
# create tables
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute("""
|
||||
CREATE TABLE twitter_settings (
|
||||
since_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
output_channel VARCHAR(64) NOT NULL,
|
||||
oauth_token VARCHAR(256) DEFAULT NULL,
|
||||
oauth_token_secret VARCHAR(256) DEFAULT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
|
||||
""")
|
||||
cur.execute("""INSERT INTO twitter_settings (since_id, output_channel) VALUES (0, '#dr.botzo')""")
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Attempt to do twitter things."""
|
||||
|
||||
if self.getstatusre.search(what):
|
||||
return self.irc.reply(event, self.twitter_getstatus(event, nick,
|
||||
userhost, what, admin_unlocked))
|
||||
elif self.getuserstatusre.search(what):
|
||||
return self.irc.reply(event, self.twitter_getuserstatus(event, nick,
|
||||
userhost, what, admin_unlocked))
|
||||
elif self.tweetre.search(what):
|
||||
return self.irc.reply(event, self.twitter_tweet(event, nick,
|
||||
userhost, what, admin_unlocked))
|
||||
elif self.replytore.search(what):
|
||||
return self.irc.reply(event, self.twitter_replyto(event, nick,
|
||||
userhost, what, admin_unlocked))
|
||||
elif self.gettokenre.search(what):
|
||||
return self.irc.reply(event, self.twitter_gettoken(event, nick,
|
||||
userhost, what, admin_unlocked))
|
||||
elif self.authre.search(what):
|
||||
return self.irc.reply(event, self.twitter_auth(event, nick,
|
||||
userhost, what, admin_unlocked))
|
||||
|
||||
def twitter_getstatus(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Get a status by tweet ID."""
|
||||
|
||||
match = self.getstatusre.search(what)
|
||||
if match:
|
||||
print_source = True
|
||||
print_id = True
|
||||
if match.group(1):
|
||||
print_source = False
|
||||
if match.group(2):
|
||||
print_id = False
|
||||
status = match.group(3)
|
||||
try:
|
||||
tweet = self.twit.show_status(id=status)
|
||||
return self._return_tweet_or_retweet_text(tweet=tweet, print_source=print_source, print_id=print_id)
|
||||
except twython.exceptions.TwythonError as e:
|
||||
return "Couldn't obtain status: " + str(e)
|
||||
|
||||
def twitter_getuserstatus(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Get a status for a user. Allows for getting one other than the most recent."""
|
||||
|
||||
match = self.getuserstatusre.search(what)
|
||||
if match:
|
||||
print_source = True
|
||||
print_id = True
|
||||
if match.group(1):
|
||||
print_source = False
|
||||
if match.group(2):
|
||||
print_id = False
|
||||
user = match.group(3)
|
||||
index = match.group(4)
|
||||
|
||||
try:
|
||||
if index:
|
||||
index = int(index)
|
||||
if index > 0:
|
||||
index = 0
|
||||
else:
|
||||
index = 0
|
||||
except ValueError as e:
|
||||
self.log.error("Couldn't convert index: " + str(e))
|
||||
index = 0
|
||||
|
||||
count = (-1*index) + 1
|
||||
|
||||
try:
|
||||
tweets = self.twit.get_user_timeline(screen_name=user, count=count, include_rts=True)
|
||||
if tweets:
|
||||
tweet = tweets[-1*index]
|
||||
return self._return_tweet_or_retweet_text(tweet=tweet, print_source=print_source, print_id=print_id)
|
||||
except twython.exceptions.TwythonError as e:
|
||||
return "Couldn't obtain status: " + str(e)
|
||||
except ValueError as e:
|
||||
return "Couldn't obtain status: " + str(e)
|
||||
|
||||
def twitter_tweet(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Tweet. Needs authentication."""
|
||||
|
||||
match = self.tweetre.search(what)
|
||||
if match:
|
||||
tweet = match.group(1)
|
||||
if not self.twit.verify_credentials():
|
||||
return "You must be authenticated to tweet."
|
||||
if admin_unlocked is False:
|
||||
return "Only admins can tweet."
|
||||
|
||||
try:
|
||||
if self.twit.update_status(status=tweet, display_coordinates=False) is not None:
|
||||
return "'{0:s}' tweeted.".format(tweet)
|
||||
else:
|
||||
return "Unknown error sending tweet(s)."
|
||||
except twython.exceptions.TwythonError as e:
|
||||
return "Couldn't tweet: " + str(e)
|
||||
|
||||
def twitter_replyto(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Reply to a tweet, in the twitter in_reply_to_status_id sense. Needs authentication."""
|
||||
|
||||
match = self.replytore.search(what)
|
||||
if match:
|
||||
status_id = match.group(1)
|
||||
tweet = match.group(2)
|
||||
|
||||
if not self.twit.verify_credentials():
|
||||
return "You must be authenticated to tweet."
|
||||
if admin_unlocked is False:
|
||||
return "Only admins can tweet."
|
||||
|
||||
replyee_tweet = self.twit.show_status(id=status_id)
|
||||
target = replyee_tweet['user']['screen_name'].encode('utf-8', 'ignore')
|
||||
|
||||
try:
|
||||
reptweet = "@{0:s}: {1:s}".format(target, tweet)
|
||||
if self.twit.update_status(status=reptweet, display_coordinates=False, in_reply_to_status_id=status_id) is not None:
|
||||
return "'{0:s}' tweeted.".format(tweet)
|
||||
else:
|
||||
return "Unknown error sending tweet."
|
||||
except twython.exceptions.TwythonError as e:
|
||||
return "Couldn't tweet: " + str(e)
|
||||
|
||||
def twitter_gettoken(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Get an oauth token, so that the user may authenticate the bot."""
|
||||
|
||||
match = self.gettokenre.search(what)
|
||||
if match:
|
||||
if self.twit.verify_credentials():
|
||||
self.authed = False
|
||||
self.twit = twython.Twython(self.consumer_key, self.consumer_secret)
|
||||
|
||||
auth = self.twit.get_authentication_tokens()
|
||||
self.temp_token = auth['oauth_token']
|
||||
self.temp_token_secret = auth['oauth_token_secret']
|
||||
return ("Go to the following link in your browser: {0:s} "
|
||||
"and send me the pin.".format(auth['auth_url']))
|
||||
|
||||
def twitter_auth(self, event, nick, userhost, what, admin_unlocked):
|
||||
"""Authenticate, given a PIN (following gettoken)."""
|
||||
|
||||
match = self.authre.search(what)
|
||||
if match:
|
||||
oauth_verifier = match.group(1)
|
||||
self.twit = twython.Twython(self.consumer_key, self.consumer_secret,
|
||||
self.temp_token, self.temp_token_secret)
|
||||
final_step = self.twit.get_authorized_tokens(oauth_verifier)
|
||||
|
||||
self.twit = twython.Twython(self.consumer_key, self.consumer_secret,
|
||||
final_step['oauth_token'], final_step['oauth_token_secret'])
|
||||
self._persist_auth_tokens(final_step['oauth_token'], final_step['oauth_token_secret'])
|
||||
|
||||
if self.twit.verify_credentials():
|
||||
self.authed = True
|
||||
# print timeline stuff. this will set up the appropriate timer
|
||||
self._check_self_timeline()
|
||||
return "The bot is now logged in."
|
||||
else:
|
||||
self.twit = twython.Twython(self.consumer_key, self.consumer_secret)
|
||||
|
||||
def thread_do(self):
|
||||
"""Check the timeline."""
|
||||
|
||||
while not self.is_shutdown:
|
||||
self._check_self_timeline()
|
||||
time.sleep(1)
|
||||
|
||||
def _check_self_timeline(self):
|
||||
"""Check my timeline, and if there are entries, print them to the channel."""
|
||||
|
||||
if self.next_timeline_check < time.time():
|
||||
self.next_timeline_check = time.time() + 300
|
||||
|
||||
if self.twit.verify_credentials():
|
||||
# get the id of the last check we made
|
||||
since_id = self._get_last_since_id()
|
||||
output_channel = self._get_output_channel()
|
||||
|
||||
if since_id is not None and output_channel != '':
|
||||
tweets = self.twit.get_home_timeline(since_id=since_id)
|
||||
tweets.reverse()
|
||||
for tweet in tweets:
|
||||
tweet_text = self._return_tweet_or_retweet_text(tweet=tweet, print_source=True)
|
||||
self.sendmsg(output_channel.encode('utf-8', 'ignore'), tweet_text)
|
||||
|
||||
# friends timeline printed, find the latest id
|
||||
new_since_id = self._get_latest_tweet_id(tweets, since_id)
|
||||
|
||||
tweets = self.twit.get_mentions_timeline(since_id=since_id)
|
||||
tweets.reverse()
|
||||
for tweet in tweets:
|
||||
tweet_text = self._return_tweet_or_retweet_text(tweet=tweet, print_source=True)
|
||||
self.sendmsg(output_channel.encode('utf-8', 'ignore'), tweet_text)
|
||||
|
||||
# mentions printed, find the latest id
|
||||
new_since_id = self._get_latest_tweet_id(tweets, new_since_id)
|
||||
|
||||
# set since_id
|
||||
self._set_last_since_id(new_since_id)
|
||||
|
||||
def _return_tweet_or_retweet_text(self, tweet, print_source=False, print_id=True):
|
||||
"""
|
||||
Return a string of the author and text body of a status,
|
||||
accounting for whether or not the fetched status is a
|
||||
retweet.
|
||||
"""
|
||||
|
||||
reply = ""
|
||||
retweet = getattr(tweet, 'retweeted_status', None)
|
||||
if retweet:
|
||||
if print_source:
|
||||
reply = "@%s (RT @%s): %s" % (tweet['user']['screen_name'].encode('utf-8', 'ignore'),
|
||||
retweet['user']['screen_name'].encode('utf-8', 'ignore'),
|
||||
super(Twitter, self)._unencode_xml(retweet['text'].encode('utf-8', 'ignore')))
|
||||
else:
|
||||
reply = "(RT @%s): %s" % (retweet['user']['screen_name'].encode('utf-8', 'ignore'),
|
||||
super(Twitter, self)._unencode_xml(retweet['text'].encode('utf-8', 'ignore')))
|
||||
else:
|
||||
if print_source:
|
||||
reply = "@%s: %s" % (tweet['user']['screen_name'].encode('utf-8', 'ignore'),
|
||||
super(Twitter, self)._unencode_xml(tweet['text'].encode('utf-8', 'ignore')))
|
||||
else:
|
||||
reply = "%s" % (super(Twitter, self)._unencode_xml(tweet['text'].encode('utf-8', 'ignore')))
|
||||
|
||||
if print_id:
|
||||
reply = reply + " [{0:d}]".format(tweet['id'])
|
||||
|
||||
return reply
|
||||
|
||||
def _get_last_since_id(self):
|
||||
"""Get the since_id out of the database."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = "SELECT since_id FROM twitter_settings"
|
||||
cur.execute(query)
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return result['since_id']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error getting last since ID")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _get_output_channel(self):
|
||||
"""Get the output_channel out of the database."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = "SELECT output_channel FROM twitter_settings"
|
||||
cur.execute(query)
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return result['output_channel']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error getting output channel")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _set_last_since_id(self, since_id):
|
||||
"""Set the since_id."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = "UPDATE twitter_settings SET since_id = %s"
|
||||
cur.execute(statement, (since_id,))
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error saving last since ID")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _get_latest_tweet_id(self, tweets, since_id):
|
||||
"""Find the latest tweet id in the provided list, or the given since_id."""
|
||||
|
||||
latest = since_id
|
||||
for tweet in tweets:
|
||||
if tweet['id'] > latest:
|
||||
latest = tweet['id']
|
||||
|
||||
return latest
|
||||
|
||||
def _persist_auth_tokens(self, oauth_token, oauth_token_secret):
|
||||
"""Save the auth tokens to the database, with the intent of reusing them."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
statement = "UPDATE twitter_settings SET oauth_token = %s, oauth_token_secret = %s"
|
||||
cur.execute(statement, (oauth_token, oauth_token_secret))
|
||||
db.commit()
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error saving auth tokens")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def _retrieve_stored_auth_tokens(self):
|
||||
"""Check the database for existing auth tokens, try reusing them."""
|
||||
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = "SELECT oauth_token, oauth_token_secret FROM twitter_settings"
|
||||
cur.execute(query)
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
return (result['oauth_token'], result['oauth_token_secret'])
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error retrieving auth tokens")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
296
ircbot/modules/Weather.py
Normal file
296
ircbot/modules/Weather.py
Normal file
@@ -0,0 +1,296 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Weather - query weather underground for info
|
||||
Copyright (C) 2010 Brian S. Stephan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
import MySQLdb as mdb
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from Module import Module
|
||||
|
||||
class Weather(Module):
|
||||
"""Provide weather lookup services to the bot."""
|
||||
|
||||
def __init__(self, irc, config):
|
||||
"""Set up regexes and wunderground API."""
|
||||
|
||||
Module.__init__(self, irc, config)
|
||||
|
||||
self.weatherre = re.compile("^!weather\s+(.*)$")
|
||||
|
||||
# get the API key from config
|
||||
api_key = self._get_api_key_from_db()
|
||||
if api_key is None:
|
||||
raise Exception("weather underground API key not found!")
|
||||
|
||||
self.wu_base = 'http://api.wunderground.com/api/{0:s}/'.format(api_key)
|
||||
|
||||
def db_init(self):
|
||||
"""Initialize the tiny table that stores the API key."""
|
||||
|
||||
version = self.db_module_registered(self.__class__.__name__)
|
||||
if version == None:
|
||||
db = self.get_db()
|
||||
try:
|
||||
version = 1
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
cur.execute('''
|
||||
CREATE TABLE weather_settings (
|
||||
api_key VARCHAR(256) NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
db.commit()
|
||||
self.db_register_module_version(self.__class__.__name__, version)
|
||||
except mdb.Error as e:
|
||||
db.rollback()
|
||||
self.log.error("database error trying to create tables")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
finally: cur.close()
|
||||
|
||||
def do(self, connection, event, nick, userhost, what, admin_unlocked):
|
||||
"""Handle IRC input for a weather queries."""
|
||||
|
||||
match = self.weatherre.search(what)
|
||||
if match:
|
||||
# see if there was a specific command given in the query, first
|
||||
query = match.group(1)
|
||||
queryitems = query.split(" ")
|
||||
if len(queryitems) <= 0:
|
||||
return
|
||||
|
||||
# search for commands
|
||||
if queryitems[0] == "conditions":
|
||||
# current weather query
|
||||
results = self.get_conditions_for_query(queryitems[1:])
|
||||
return self.irc.reply(event, results)
|
||||
elif queryitems[0] == "forecast":
|
||||
# forecast query
|
||||
results = self.get_forecast_for_query(queryitems[1:])
|
||||
return self.irc.reply(event, results)
|
||||
else:
|
||||
# assume they wanted current weather
|
||||
results = self.get_conditions_for_query(queryitems)
|
||||
return self.irc.reply(event, results)
|
||||
|
||||
def get_conditions_for_query(self, queryitems):
|
||||
"""Make a wunderground conditions call, return as string."""
|
||||
|
||||
# recombine the query into a string
|
||||
query = ' '.join(queryitems)
|
||||
query = query.replace(' ', '_')
|
||||
|
||||
try:
|
||||
url = self.wu_base + ('{0:s}/q/{1:s}.json'.format('conditions',
|
||||
query))
|
||||
json_resp = urllib2.urlopen(url)
|
||||
condition_data = json.load(json_resp)
|
||||
except IOError as e:
|
||||
self.log.error("error while making conditions query")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
|
||||
# condition data is loaded. the rest of this is obviously specific to
|
||||
# http://www.wunderground.com/weather/api/d/docs?d=data/conditions
|
||||
self.log.debug(json.dumps(condition_data, sort_keys=True, indent=4))
|
||||
|
||||
try:
|
||||
# just see if we have current_observation data
|
||||
current = condition_data['current_observation']
|
||||
except KeyError as e:
|
||||
# ok, try to see if the ambiguous results stuff will help
|
||||
self.log.debug("potentially ambiguous results, checking")
|
||||
try:
|
||||
results = condition_data['response']['results']
|
||||
reply = "Multiple results, try one of the following zmw codes:"
|
||||
for res in results[:-1]:
|
||||
q = res['l'].strip('/q/')
|
||||
reply += " {0:s} ({1:s}, {2:s}),".format(q, res['name'],
|
||||
res['country_name'])
|
||||
q = results[-1]['l'].strip('/q/')
|
||||
reply += " or {0:s} ({1:s}, {2:s}).".format(q, results[-1]['name'],
|
||||
results[-1]['country_name'])
|
||||
return reply
|
||||
except KeyError as e:
|
||||
# now we really know something is wrong
|
||||
self.log.error("error or bad query in conditions lookup")
|
||||
self.log.exception(e)
|
||||
return "No results."
|
||||
else:
|
||||
try:
|
||||
location = current['display_location']['full']
|
||||
reply = "Conditions for {0:s}: ".format(location)
|
||||
|
||||
weather_str = current['weather']
|
||||
if weather_str != '':
|
||||
reply += "{0:s}, ".format(weather_str)
|
||||
|
||||
temp_f = current['temp_f']
|
||||
temp_c = current['temp_c']
|
||||
temp_str = current['temperature_string']
|
||||
if temp_f != '' and temp_c != '':
|
||||
reply += "{0:.1f}°F ({1:.1f}°C)".format(temp_f, temp_c)
|
||||
elif temp_str != '':
|
||||
reply += "{0:s}".format(temp_str)
|
||||
|
||||
# append feels like if we have it
|
||||
feelslike_f = current['feelslike_f']
|
||||
feelslike_c = current['feelslike_c']
|
||||
feelslike_str = current['feelslike_string']
|
||||
if feelslike_f != '' and feelslike_c != '':
|
||||
reply += ", feels like {0:s}°F ({1:s}°C)".format(feelslike_f, feelslike_c)
|
||||
elif feelslike_str != '':
|
||||
reply += ", feels like {0:s}".format(feelslike_str)
|
||||
|
||||
# whether this is current or current + feelslike, terminate sentence
|
||||
reply += ". "
|
||||
|
||||
humidity_str = current['relative_humidity']
|
||||
if humidity_str != '':
|
||||
reply += "Humidity: {0:s}. ".format(humidity_str)
|
||||
|
||||
wind_str = current['wind_string']
|
||||
if wind_str != '':
|
||||
reply += "Wind: {0:s}. ".format(wind_str)
|
||||
|
||||
pressure_in = current['pressure_in']
|
||||
pressure_trend = current['pressure_trend']
|
||||
if pressure_in != '':
|
||||
reply += "Pressure: {0:s}\"".format(pressure_in)
|
||||
if pressure_trend != '':
|
||||
reply += " and {0:s}".format("dropping" if pressure_trend == '-' else "rising")
|
||||
reply += ". "
|
||||
|
||||
heat_index_str = current['heat_index_string']
|
||||
if heat_index_str != '' and heat_index_str != 'NA':
|
||||
reply += "Heat index: {0:s}. ".format(heat_index_str)
|
||||
|
||||
windchill_str = current['windchill_string']
|
||||
if windchill_str != '' and windchill_str != 'NA':
|
||||
reply += "Wind chill: {0:s}. ".format(windchill_str)
|
||||
|
||||
visibility_mi = current['visibility_mi']
|
||||
if visibility_mi != '':
|
||||
reply += "Visibility: {0:s} miles. ".format(visibility_mi)
|
||||
|
||||
precip_in = current['precip_today_in']
|
||||
if precip_in != '':
|
||||
reply += "Precipitation today: {0:s}\". ".format(precip_in)
|
||||
|
||||
observation_time = current['observation_time']
|
||||
if observation_time != '':
|
||||
reply += "{0:s}. ".format(observation_time)
|
||||
|
||||
return self._prettify_weather_strings(reply.rstrip())
|
||||
except KeyError as e:
|
||||
self.log.error("error or unexpected results in conditions reply")
|
||||
self.log.exception(e)
|
||||
return "Error parsing results."
|
||||
|
||||
def get_forecast_for_query(self, queryitems):
|
||||
"""Make a wunderground forecast call, return as string."""
|
||||
|
||||
# recombine the query into a string
|
||||
query = ' '.join(queryitems)
|
||||
query = query.replace(' ', '_')
|
||||
|
||||
try:
|
||||
url = self.wu_base + ('{0:s}/q/{1:s}.json'.format('forecast',
|
||||
query))
|
||||
json_resp = urllib2.urlopen(url)
|
||||
forecast_data = json.load(json_resp)
|
||||
except IOError as e:
|
||||
self.log.error("error while making forecast query")
|
||||
self.log.exception(e)
|
||||
raise
|
||||
|
||||
# forecast data is loaded. the rest of this is obviously specific to
|
||||
# http://www.wunderground.com/weather/api/d/docs?d=data/forecast
|
||||
self.log.debug(json.dumps(forecast_data, sort_keys=True, indent=4))
|
||||
|
||||
try:
|
||||
# just see if we have forecast data
|
||||
forecasts = forecast_data['forecast']['txt_forecast']
|
||||
except KeyError as e:
|
||||
# ok, try to see if the ambiguous results stuff will help
|
||||
self.log.debug("potentially ambiguous results, checking")
|
||||
try:
|
||||
results = forecast_data['response']['results']
|
||||
reply = "Multiple results, try one of the following zmw codes:"
|
||||
for res in results[:-1]:
|
||||
q = res['l'].strip('/q/')
|
||||
reply += " {0:s} ({1:s}, {2:s}),".format(q, res['name'],
|
||||
res['country_name'])
|
||||
q = results[-1]['l'].strip('/q/')
|
||||
reply += " or {0:s} ({1:s}, {2:s}).".format(q, results[-1]['name'],
|
||||
results[-1]['country_name'])
|
||||
return reply
|
||||
except KeyError as e:
|
||||
# now we really know something is wrong
|
||||
self.log.error("error or bad query in forecast lookup")
|
||||
self.log.exception(e)
|
||||
return "No results."
|
||||
else:
|
||||
try:
|
||||
reply = "Forecast: "
|
||||
for forecast in forecasts['forecastday'][0:5]:
|
||||
reply += "{0:s}: {1:s} ".format(forecast['title'],
|
||||
forecast['fcttext'])
|
||||
|
||||
return self._prettify_weather_strings(reply.rstrip())
|
||||
except KeyError as e:
|
||||
self.log.error("error or unexpected results in forecast reply")
|
||||
self.log.exception(e)
|
||||
return "Error parsing results."
|
||||
|
||||
def _get_api_key_from_db(self):
|
||||
"""Get the API key string from the database, or None if unset."""
|
||||
|
||||
api_key = None
|
||||
db = self.get_db()
|
||||
try:
|
||||
cur = db.cursor(mdb.cursors.DictCursor)
|
||||
query = '''SELECT api_key FROM weather_settings'''
|
||||
cur.execute(query)
|
||||
value = cur.fetchone()
|
||||
if (value != None):
|
||||
api_key = value['api_key']
|
||||
except mdb.Error as e:
|
||||
self.log.error("database error during api key retrieval")
|
||||
self.log.exception(e)
|
||||
finally: cur.close()
|
||||
|
||||
return api_key
|
||||
|
||||
def _prettify_weather_strings(self, weather_str):
|
||||
"""
|
||||
Clean up output strings.
|
||||
|
||||
For example, turn 32F into 32°F in input string.
|
||||
|
||||
Input:
|
||||
weather_str --- the string to clean up
|
||||
|
||||
"""
|
||||
|
||||
return re.sub(r'(\d+)\s*([FC])', r'\1°\2', weather_str)
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
# kate: indent-mode python;indent-width 4;replace-tabs on;
|
||||
0
ircbot/modules/__init__.py
Normal file
0
ircbot/modules/__init__.py
Normal file
0
ircbot/old-web/__init__.py
Normal file
0
ircbot/old-web/__init__.py
Normal file
11
ircbot/old-web/dr.botzo.wsgi
Normal file
11
ircbot/old-web/dr.botzo.wsgi
Normal file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
path = '/home/bss/Programs/dr.botzo/web'
|
||||
if path not in sys.path:
|
||||
sys.path.append(path)
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
|
||||
|
||||
import django.core.handlers.wsgi
|
||||
application = django.core.handlers.wsgi.WSGIHandler()
|
||||
6
ircbot/old-web/index/views.py
Normal file
6
ircbot/old-web/index/views.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.shortcuts import render_to_response
|
||||
|
||||
def index(request):
|
||||
return render_to_response('index.html')
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
0
ircbot/old-web/karma/__init__.py
Normal file
0
ircbot/old-web/karma/__init__.py
Normal file
47
ircbot/old-web/karma/models.py
Normal file
47
ircbot/old-web/karma/models.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.db import models
|
||||
|
||||
class LogEntry(models.Model):
|
||||
id = models.IntegerField(primary_key=True)
|
||||
key = models.CharField(max_length=255)
|
||||
delta = models.IntegerField(choices=((1, u'++'), (-1, u'--')))
|
||||
who = models.CharField(max_length=30)
|
||||
userhost = models.CharField(max_length=512)
|
||||
timestamp = models.DateTimeField(db_column='karmatime')
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s given %s by %s on %s" % (self.key, self.delta,
|
||||
self.who, self.timestamp)
|
||||
|
||||
class Meta:
|
||||
db_table = 'karma_log'
|
||||
ordering = ['timestamp']
|
||||
managed = False
|
||||
|
||||
class User(models.Model):
|
||||
who = models.CharField(max_length=30, primary_key=True)
|
||||
pos = models.IntegerField()
|
||||
neg = models.IntegerField()
|
||||
|
||||
def _get_total(self):
|
||||
return self.pos + self.neg
|
||||
total = property(_get_total)
|
||||
|
||||
def __unicode__(self):
|
||||
return "Karma User %s (%s/%s)" % (self.who, self,pos, self.neg)
|
||||
|
||||
class Meta:
|
||||
db_table = 'karma_users'
|
||||
ordering = ['who']
|
||||
managed = False
|
||||
|
||||
class Value(models.Model):
|
||||
key = models.CharField(max_length=255, primary_key=True)
|
||||
value = models.IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return "Karma Value %s (%s)" % (self.key, self.value)
|
||||
|
||||
class Meta:
|
||||
db_table = 'karma_values'
|
||||
ordering = ['value']
|
||||
managed = False
|
||||
23
ircbot/old-web/karma/tests.py
Normal file
23
ircbot/old-web/karma/tests.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
This file demonstrates two different styles of tests (one doctest and one
|
||||
unittest). These will both pass when you run "manage.py test".
|
||||
|
||||
Replace these with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.failUnlessEqual(1 + 1, 2)
|
||||
|
||||
__test__ = {"doctest": """
|
||||
Another way to test that 1 + 1 is equal to 2.
|
||||
|
||||
>>> 1 + 1 == 2
|
||||
True
|
||||
"""}
|
||||
|
||||
8
ircbot/old-web/karma/urls.py
Executable file
8
ircbot/old-web/karma/urls.py
Executable file
@@ -0,0 +1,8 @@
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
urlpatterns = patterns('karma.views',
|
||||
(r'^$', 'index'),
|
||||
(r'^givers/$', 'givers'),
|
||||
(r'^stats/$', 'stats'),
|
||||
(r'^stats/(?P<key>.*)/$', 'key_detail', dict(), 'key_detail'),
|
||||
)
|
||||
24
ircbot/old-web/karma/views.py
Normal file
24
ircbot/old-web/karma/views.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
|
||||
from karma.models import Value, User, LogEntry
|
||||
|
||||
def index(request):
|
||||
karma_values = len(Value.objects.all())
|
||||
karma_users = len(User.objects.all())
|
||||
return render_to_response('karma/index.html', {'value_count': karma_values,
|
||||
'user_count': karma_users})
|
||||
|
||||
def stats(request):
|
||||
values = Value.objects.all().order_by('-value')
|
||||
return render_to_response('karma/stats.html', {'values': values})
|
||||
|
||||
def givers(request):
|
||||
users = User.objects.all().order_by('who')
|
||||
return render_to_response('karma/givers.html', {'users': users})
|
||||
|
||||
def key_detail(request, key):
|
||||
deltas = LogEntry.objects.filter(key=key)
|
||||
return render_to_response('karma/key_detail.html', {'key': key,'deltas': deltas}, context_instance=RequestContext(request))
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
11
ircbot/old-web/manage.py
Normal file
11
ircbot/old-web/manage.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
from django.core.management import execute_manager
|
||||
try:
|
||||
import settings # Assumed to be in the same directory.
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
execute_manager(settings)
|
||||
113
ircbot/old-web/settings.py
Normal file
113
ircbot/old-web/settings.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# Django settings for web project.
|
||||
|
||||
import os.path
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
('Mike Bloy', 'mike@bloy.org'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DB_LOCATION = os.path.normpath(
|
||||
os.path.join(os.path.abspath(os.path.dirname(__file__)),
|
||||
'..', 'dr.botzo.data'))
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': DB_LOCATION, # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# On Unix systems, a value of None will cause Django to use the same
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'America/Chicago'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = True
|
||||
|
||||
# If you set this to False, Django will not format dates, numbers and
|
||||
# calendars according to the current locale
|
||||
USE_L10N = True
|
||||
|
||||
# Absolute path to the directory that holds media.
|
||||
# Example: "/home/media/media.lawrence.com/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash if there is a path component (optional in other cases).
|
||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://foo.com/media/", "/media/".
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 'x(mks19qjlqj=l)werudifqhhr_3b6v($kwihs+=p^ldqcc4$q'
|
||||
|
||||
# List of callables that know how to import templates from various sources.
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
# 'django.template.loaders.eggs.Loader',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'urls'
|
||||
|
||||
TEMPLATE_BASE_LOC = os.path.normpath(
|
||||
os.path.join(os.path.abspath(os.path.dirname(__file__)),
|
||||
'templates'))
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
TEMPLATE_BASE_LOC,
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
#'django.contrib.auth',
|
||||
#'django.contrib.contenttypes',
|
||||
#'django.contrib.sessions',
|
||||
#'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
# Uncomment the next line to enable the admin:
|
||||
#'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
# 'django.contrib.admindocs',
|
||||
'karma',
|
||||
'storycraft',
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print DB_LOCATION
|
||||
print TEMPLATE_DIRS
|
||||
167
ircbot/old-web/static/storycraft/page.css
Normal file
167
ircbot/old-web/static/storycraft/page.css
Normal file
@@ -0,0 +1,167 @@
|
||||
* {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #333333;
|
||||
background: #efefef;
|
||||
font-family: 'Bitstream Vera Sans', sans-serif;
|
||||
font-size: 9pt;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#container {
|
||||
background: white;
|
||||
min-height: 100%;
|
||||
border-left: 1px solid #666666;
|
||||
border-right: 1px solid #666666;
|
||||
width: 85%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#messages {
|
||||
background: white;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#messages .messagelist .error {
|
||||
list-style-type: none;
|
||||
color: red;
|
||||
}
|
||||
|
||||
#mainpage {
|
||||
padding: 5px;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
#mainpage #title {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#mainpage #title h1 {
|
||||
font-family: 'Cardo', 'Times New Roman', serif;
|
||||
font-size: 32pt;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.gameitem {
|
||||
margin: 10px;
|
||||
border: 1px solid #999999;
|
||||
}
|
||||
|
||||
.gameitem .gameinfo {
|
||||
padding: 0px;
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gameitem .gameplayers {
|
||||
padding: 5px;
|
||||
border: 1px solid #e0e0e0;
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gameitem .gameinfo li {
|
||||
padding: 5px;
|
||||
display: table-cell;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.gameitem .gameplayers li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.gameitem .gameplayers li:before {
|
||||
content: ", ";
|
||||
}
|
||||
.gameitem .gameplayers li:first-child:before {
|
||||
content: "Players: ";
|
||||
}
|
||||
|
||||
.gameitem .gameinfo li.gameid {
|
||||
width: 45%;
|
||||
background: #e0e0e0;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.gameitem .gameinfo li.gamestatus {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.gameitem .gameinfo li.gamecode {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.gameitem a {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.gameitem .progressblock {
|
||||
width: 100%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.progressbar .yui-pb {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.progressbar .yui-pb-bar {
|
||||
background-color: #811610;
|
||||
}
|
||||
|
||||
.storyblock {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin: 0px 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.gamestory {
|
||||
font-family: 'Cardo', 'Times New Roman', serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.gamestory p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#footer {
|
||||
position: relative;
|
||||
margin-top: -50px;
|
||||
height: 50px;
|
||||
clear: both;
|
||||
|
||||
text-align: center;
|
||||
|
||||
background: #333333;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
#footer p {
|
||||
padding-top: 10px;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
vi:tabstop=4:expandtab:autoindent
|
||||
*/
|
||||
0
ircbot/old-web/storycraft/__init__.py
Normal file
0
ircbot/old-web/storycraft/__init__.py
Normal file
64
ircbot/old-web/storycraft/models.py
Normal file
64
ircbot/old-web/storycraft/models.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from django.db import models
|
||||
|
||||
class StorycraftGame(models.Model):
|
||||
id = models.IntegerField(primary_key=True)
|
||||
round_mode = models.IntegerField()
|
||||
game_length = models.IntegerField()
|
||||
line_length = models.IntegerField()
|
||||
random_method = models.IntegerField()
|
||||
lines_per_turn = models.IntegerField()
|
||||
status = models.CharField()
|
||||
owner_nick = models.CharField()
|
||||
owner_userhost = models.CharField()
|
||||
start_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'storycraft_game'
|
||||
ordering = ['id']
|
||||
managed = False
|
||||
|
||||
def __unicode__(self):
|
||||
"""Return a terse summary of the vital game info."""
|
||||
|
||||
return '#{0:d} - created on {1:s} by {2:s}, {3:s}'.format(self.id, self.start_time, self.owner_nick, self.status)
|
||||
|
||||
def is_completed(self):
|
||||
"""Return if this game is completed."""
|
||||
|
||||
return self.status == 'COMPLETED' and self.end_time
|
||||
|
||||
class StorycraftPlayer(models.Model):
|
||||
id = models.IntegerField(primary_key=True)
|
||||
game = models.ForeignKey(StorycraftGame)
|
||||
nick = models.CharField()
|
||||
userhost = models.CharField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'storycraft_player'
|
||||
ordering = ['id']
|
||||
managed = False
|
||||
|
||||
def __unicode__(self):
|
||||
"""Return the nick!user@host."""
|
||||
|
||||
return '{0:s}!{1:s}'.format(self.nick, self.userhost)
|
||||
|
||||
class StorycraftLine(models.Model):
|
||||
id = models.IntegerField(primary_key=True)
|
||||
game = models.ForeignKey(StorycraftGame)
|
||||
player = models.ForeignKey(StorycraftPlayer)
|
||||
line = models.TextField()
|
||||
time = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'storycraft_line'
|
||||
ordering = ['id']
|
||||
managed = False
|
||||
|
||||
def __unicode__(self):
|
||||
"""Just return the line."""
|
||||
|
||||
return '{0:s}'.format(self.line)
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
8
ircbot/old-web/storycraft/urls.py
Normal file
8
ircbot/old-web/storycraft/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
urlpatterns = patterns('storycraft.views',
|
||||
(r'^$', 'index'),
|
||||
(r'^games/(?P<game_id>\d+)/$', 'game_index', dict(), 'game_index'),
|
||||
)
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
48
ircbot/old-web/storycraft/views.py
Normal file
48
ircbot/old-web/storycraft/views.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render_to_response, get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from storycraft.models import StorycraftGame, StorycraftLine, StorycraftPlayer
|
||||
|
||||
def index(request):
|
||||
"""Display a short list of each game (and its summary) in the system.
|
||||
|
||||
TODO: add paginator.
|
||||
"""
|
||||
|
||||
games = StorycraftGame.objects.all()
|
||||
return render_to_response('storycraft/index.html', {'games': games}, context_instance=RequestContext(request))
|
||||
|
||||
def game_index(request, game_id):
|
||||
"""Display one individual game's details, including the story, if it's done."""
|
||||
|
||||
game = get_object_or_404(StorycraftGame, pk=game_id)
|
||||
players = StorycraftPlayer.objects.filter(game=game.id)
|
||||
lines = StorycraftLine.objects.filter(game=game.id)
|
||||
pretty_story = []
|
||||
|
||||
if game.is_completed():
|
||||
# make a HTML-formatted string that is the entire story, in
|
||||
# which we've added paragraphs and such.
|
||||
period_count = 0
|
||||
pretty_story.append('<p>')
|
||||
for line in lines:
|
||||
period_count = period_count + line.line.count('.')
|
||||
if period_count > 6:
|
||||
period_count = 0
|
||||
split = line.line.rsplit('.', 1)
|
||||
pretty_story.append('<span title="' + line.player.nick + ' @ ' + line.time + '">' + escape(split[0]) + '.</span></p>')
|
||||
pretty_story.append('<p><span title="' + line.player.nick + ' @ ' + line.time + '">' + escape(split[1]) + '</span>')
|
||||
else:
|
||||
pretty_story.append('<span title="' + line.player.nick + ' @ ' + line.time + '">' + escape(line.line) + '</span>')
|
||||
pretty_story.append('</p>')
|
||||
pretty_story_text = ' '.join(pretty_story)
|
||||
mark_safe(pretty_story_text)
|
||||
|
||||
return render_to_response('storycraft/game_index.html', {'game': game, 'players': players, 'lines': lines, 'prettystory': pretty_story_text}, context_instance=RequestContext(request))
|
||||
|
||||
# vi:tabstop=4:expandtab:autoindent
|
||||
13
ircbot/old-web/templates/index.html
Normal file
13
ircbot/old-web/templates/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>dr.botzo</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li><a href="/dr.botzo/karma/">Karma</a></li>
|
||||
<li><a href="/dr.botzo/storycraft/">Storycraft</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
9
ircbot/old-web/templates/karma/givers.html
Executable file
9
ircbot/old-web/templates/karma/givers.html
Executable file
@@ -0,0 +1,9 @@
|
||||
{% autoescape on %}
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>
|
||||
{{ user.who }} - {{ user.pos }} positive, {{ user.neg }} negative, {{ user.total }} total
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endautoescape %}
|
||||
6
ircbot/old-web/templates/karma/index.html
Executable file
6
ircbot/old-web/templates/karma/index.html
Executable file
@@ -0,0 +1,6 @@
|
||||
{% autoescape on %}
|
||||
<ul>
|
||||
<li><a href="/karma/stats/">Karma Stats</a> ({{ value_count }} karma entries)</li>
|
||||
<li><a href="/karma/givers/">Karma Giver Stats</a> ({{ user_count }} karma givers)</li>
|
||||
</ul>
|
||||
{% endautoescape %}
|
||||
31
ircbot/old-web/templates/karma/key_detail.html
Normal file
31
ircbot/old-web/templates/karma/key_detail.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% autoescape on %}
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>dr.botzo — Karma — {{ key }}</title>
|
||||
</head>
|
||||
<body>
|
||||
{% if messages %}
|
||||
<div id="messages">
|
||||
<ul class="messagelist">
|
||||
{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="container">
|
||||
<ul>
|
||||
{% for delta in deltas %}
|
||||
<li>{{ delta }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!--
|
||||
vi:tabstop=2:expandtab:autoindent
|
||||
-->
|
||||
{% endautoescape %}
|
||||
9
ircbot/old-web/templates/karma/stats.html
Executable file
9
ircbot/old-web/templates/karma/stats.html
Executable file
@@ -0,0 +1,9 @@
|
||||
{% autoescape on %}
|
||||
<ol>
|
||||
{% for value in values %}
|
||||
<li>
|
||||
<a href="{% url key_detail value.key %}">{{ value.key }}</a> ({{ value.value }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endautoescape %}
|
||||
49
ircbot/old-web/templates/storycraft/game_index.html
Normal file
49
ircbot/old-web/templates/storycraft/game_index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% autoescape on %}
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<link href='http://fonts.googleapis.com/css?family=Cardo&subset=latin' rel='stylesheet' type='text/css' />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>dr.botzo — Storycraft #{{ game.id }}</title>
|
||||
<link rel="stylesheet" href="/static/storycraft/page.css" type="text/css" />
|
||||
<!-- Required CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="http://yui.yahooapis.com/2.8.2r1/build/progressbar/assets/skins/sam/progressbar.css">
|
||||
|
||||
<!-- Dependency source file -->
|
||||
<script src = "http://yui.yahooapis.com/2.8.2r1/build/yahoo-dom-event/yahoo-dom-event.js" ></script>
|
||||
<script src = "http://yui.yahooapis.com/2.8.2r1/build/element/element-min.js" ></script>
|
||||
<!-- Optional dependency source file -->
|
||||
<script src="http://yui.yahooapis.com/2.8.2r1/build/animation/animation-min.js" type="text/javascript"></script>
|
||||
|
||||
<!-- ProgressBar source file -->
|
||||
<script src = "http://yui.yahooapis.com/2.8.2r1/build/progressbar/progressbar-min.js" ></script>
|
||||
</head>
|
||||
<body>
|
||||
{% if messages %}
|
||||
<div id="messages">
|
||||
<ul class="messagelist">
|
||||
{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="container">
|
||||
<div id="mainpage">
|
||||
<div id="title">
|
||||
<h1>Storycraft #{{ game.id }}</h1>
|
||||
</div>
|
||||
<div id="gamesummary">
|
||||
{% include 'storycraft/tmpl_game_summary.html' %}
|
||||
</div>
|
||||
{% include 'storycraft/tmpl_game_story.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'storycraft/tmpl_footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!--
|
||||
vi:tabstop=2:expandtab:autoindent
|
||||
-->
|
||||
{% endautoescape %}
|
||||
52
ircbot/old-web/templates/storycraft/index.html
Normal file
52
ircbot/old-web/templates/storycraft/index.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% autoescape on %}
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<link href='http://fonts.googleapis.com/css?family=Cardo&subset=latin' rel='stylesheet' type='text/css' />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>dr.botzo — Storycraft</title>
|
||||
<link rel="stylesheet" href="/static/storycraft/page.css" type="text/css" />
|
||||
<!-- Required CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="http://yui.yahooapis.com/2.8.2r1/build/progressbar/assets/skins/sam/progressbar.css">
|
||||
|
||||
<!-- Dependency source file -->
|
||||
<script src = "http://yui.yahooapis.com/2.8.2r1/build/yahoo-dom-event/yahoo-dom-event.js" ></script>
|
||||
<script src = "http://yui.yahooapis.com/2.8.2r1/build/element/element-min.js" ></script>
|
||||
<!-- Optional dependency source file -->
|
||||
<script src="http://yui.yahooapis.com/2.8.2r1/build/animation/animation-min.js" type="text/javascript"></script>
|
||||
|
||||
<!-- ProgressBar source file -->
|
||||
<script src = "http://yui.yahooapis.com/2.8.2r1/build/progressbar/progressbar-min.js" ></script>
|
||||
</head>
|
||||
<body>
|
||||
{% if messages %}
|
||||
<div id="messages">
|
||||
<ul class="messagelist">
|
||||
{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="container">
|
||||
<div id="mainpage">
|
||||
<div id="title">
|
||||
<h1>Storycraft</h1>
|
||||
</div>
|
||||
<div id="gamelist">
|
||||
{% for game in games %}
|
||||
{% include 'storycraft/tmpl_game_summary.html' %}
|
||||
{% empty %}
|
||||
<p>No storycraft games. :(</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'storycraft/tmpl_footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!--
|
||||
vi:tabstop=2:expandtab:autoindent
|
||||
-->
|
||||
{% endautoescape %}
|
||||
9
ircbot/old-web/templates/storycraft/tmpl_footer.html
Normal file
9
ircbot/old-web/templates/storycraft/tmpl_footer.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% autoescape on %}
|
||||
<div id="footer">
|
||||
<p>Storycraft is a game of collaborative storytelling, intentionally leading to much insanity and nonsense. All stories were made to be funny but may not even be coherent. Void where prohibited.</p>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
vi:tabstop=2:expandtab:autoindent
|
||||
-->
|
||||
{% endautoescape %}
|
||||
15
ircbot/old-web/templates/storycraft/tmpl_game_story.html
Normal file
15
ircbot/old-web/templates/storycraft/tmpl_game_story.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% autoescape on %}
|
||||
<div class="storyblock">
|
||||
{% if game.is_completed %}
|
||||
<div class="gamestory">{{ prettystory|safe }}</div>
|
||||
{% else %}
|
||||
<div class="gamenostory">
|
||||
<p>This game has not been completed yet. Come back later (or get to work).</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
vi:tabstop=2:expandtab:autoindent
|
||||
-->
|
||||
{% endautoescape %}
|
||||
28
ircbot/old-web/templates/storycraft/tmpl_game_summary.html
Normal file
28
ircbot/old-web/templates/storycraft/tmpl_game_summary.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% autoescape on %}
|
||||
<div class="gameitem">
|
||||
<h3 class="hidden">Game Info</h3>
|
||||
<ul class="gameinfo">
|
||||
<li class="gameid"><span><a href="{% url game_index game.id %}">{{ game.id }}: {{ game.start_time }} – {{ game.end_time }}</a></span></li>
|
||||
<li class="gamestatus"><span>{{ game.status }}</span></li>
|
||||
<li class="gamecode"><span>o:{{ game.round_mode }}[{{ game.game_length }}],{{ game.line_length }},{{ game.random_method }},{{ game.lines_per_turn }}</span></li>
|
||||
</ul>
|
||||
{% if players %}
|
||||
<h3 class="hidden">Player List</h3>
|
||||
<ul class="gameplayers">
|
||||
{% for player in players %}
|
||||
<li>{{ player.nick }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if lines %}
|
||||
<div class="progressblock" id="progressblock{{game.id}}">
|
||||
<div class="progressbar" id="progressbar{{game.id}}"></div>
|
||||
</div>
|
||||
<script>var pb = new YAHOO.widget.ProgressBar().render('progressbar{{game.id}}'); var space = YAHOO.util.Dom.getRegion('progressblock{{game.id}}'); pb.set('width', space.width-35); pb.set('minValue', 0); pb.set('maxValue', {{ game.game_length }}); pb.set('value', 0); if (0) { pb.set('anim', true); var anim = pb.get('anim'); anim.duration = 2; } pb.set('value', {{ lines|length }}-1);</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
vi:tabstop=2:expandtab:autoindent
|
||||
-->
|
||||
{% endautoescape %}
|
||||
20
ircbot/old-web/urls.py
Normal file
20
ircbot/old-web/urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
(r'^$', 'index.views.index'),
|
||||
(r'^karma/', include('karma.urls')),
|
||||
(r'^storycraft/', include('storycraft.urls')),
|
||||
# Example:
|
||||
# (r'^web/', include('web.foo.urls')),
|
||||
|
||||
# Uncomment the admin/doc line below to enable admin documentation:
|
||||
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
|
||||
# Uncomment the next line to enable the admin:
|
||||
# (r'^admin/', include(admin.site.urls)),
|
||||
)
|
||||
Reference in New Issue
Block a user