actually i need to call this ircbot

so i don't collide with the django dr_botzo
This commit is contained in:
2014-03-16 09:18:17 -05:00
parent 43a73f368f
commit e7b132348f
53 changed files with 0 additions and 0 deletions

814
ircbot/DrBotIRC.py Normal file
View 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
View 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 &lt;, &gt;, &amp; 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('&lt;', '<')
text = text.replace('&gt;', '>')
text = text.replace('&amp;', '&')
return text
# vi:tabstop=4:expandtab:autoindent
# kate: indent-mode python;indent-width 4;replace-tabs on;

View 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
View 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;

View File

1711
ircbot/extlib/irclib.py Normal file

File diff suppressed because it is too large Load Diff

58
ircbot/logging.cfg Normal file
View 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 =

View 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
View 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

View 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('&quot;', '"')
translation = translation.replace('&amp;', '&')
translation = translation.replace('&lt;', '<')
translation = translation.replace('&gt;', '>')
translation = translation.replace('&#039;', '\'')
translation = translation.replace('&#039', '\'')
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
View 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
View 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
View 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
View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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'\\2', weather_str)
# vi:tabstop=4:expandtab:autoindent
# kate: indent-mode python;indent-width 4;replace-tabs on;

View File

View File

View 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()

View 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

View File

View 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

View 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
View 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'),
)

View 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
View 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
View 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

View 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
*/

View File

View 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

View 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

View 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

View 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>

View 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 %}

View 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 %}

View 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 &mdash; Karma &mdash; {{ 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 %}

View 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 %}

View 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&amp;subset=latin' rel='stylesheet' type='text/css' />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>dr.botzo &mdash; 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 %}

View 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&amp;subset=latin' rel='stylesheet' type='text/css' />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>dr.botzo &mdash; 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 %}

View 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 %}

View 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 %}

View 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 }} &ndash; {{ 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
View 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)),
)