chiark / gitweb /
Found in /home/matthew/programming/irc/bot - live version, all files as found
authorIan Jackson <ijackson@rapun.sel.cam.ac.uk>
Wed, 5 Aug 2009 12:15:57 +0000 (13:15 +0100)
committerIan Jackson <ijackson@rapun.sel.cam.ac.uk>
Wed, 5 Aug 2009 12:15:57 +0000 (13:15 +0100)
28 files changed:
Servus-artichoke.py [new file with mode: 0644]
Servus-artichoke.py~ [new file with mode: 0644]
Servus-chiark.py [new file with mode: 0644]
Servus-chiark.py~ [new file with mode: 0644]
acrobat.py
assassins.py [new file with mode: 0644]
assassins.py~ [new file with mode: 0644]
commands.py
commands.py.orig [new file with mode: 0644]
commands.py.rej [new file with mode: 0644]
commands.py~ [new file with mode: 0644]
config.py
config.py~ [new file with mode: 0644]
define.patch [new file with mode: 0644]
flirts.~1.12.~ [new file with mode: 0644]
ircbot.py
karmaconvert.py [new file with mode: 0755]
launch.sh [new file with mode: 0755]
methsoc-freenode.py [new file with mode: 0644]
methsoc.py [new file with mode: 0644]
rjk.diff [new file with mode: 0644]
slashes.~1.7.~ [new file with mode: 0644]
slashes~ [new file with mode: 0644]
socbi.py [new file with mode: 0644]
test-chiark.py [new file with mode: 0644]
test-chiark.py~ [new file with mode: 0644]
trouts.~1.49.~ [new file with mode: 0644]
urllib.py [new file with mode: 0644]

diff --git a/Servus-artichoke.py b/Servus-artichoke.py
new file mode 100644 (file)
index 0000000..b737b89
--- /dev/null
@@ -0,0 +1,205 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+# Richard Kettlewell <rjk@greenend.org.uk
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "irc.nsict.org"
+port = 6667
+nickname = "Servus"
+channel = "#artichoke"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+import os, time
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+slashcfg= ( 
+       __load("slashes"),
+       ' (while %s watches)',
+       "Sorry, but %s stole my pen.",
+       "Who do you want to slash?",
+       "I have writer's block!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "artichoke-karma"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+    savekarma()
+def savekarma():
+    tmp = "%s.tmp" % karmafilename
+    try:
+        f = open(tmp, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+       os.rename(tmp, karmafilename)
+    except IOError, e:
+        sys.stderr.write("error writing karma: %s" % e)
+
+def quit(bot,cmd,nick,conn,public):
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    c.reloadq(bot,cmd,nick,conn,public)
+
+#The game...
+class game:
+       trigger="Servus"
+       grace=time.time()
+       minlose=24*60*60 #1 day
+       maxlose=14*minlose #2 weeks
+       losetime=time.time()+300000
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "slash": (c.slashq, slashcfg),
+           "rot13": c.rot13q,
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+#          "define": c.defineq,
+#          "game": (c.gameq,game),
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command.lower()]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
+#    elif public:
+#        if cmd.find("GAME")!=-1: #someone else lost
+#          grace.grace=time.time()+60*20
+#      elif cmd.find(game.trigger)!=-1 and len(game.trigger)>2: #we lost!
+#          c.gameq(bot,"pad "+game.trigger,bot.owner,conn,False,game)
+#      elif time.time()>game.losetime: #we randomly lost, take new trigger
+#          c.gameq(bot,cmd,bot.owner,conn,False,game)
+#          
diff --git a/Servus-artichoke.py~ b/Servus-artichoke.py~
new file mode 100644 (file)
index 0000000..c57ee1b
--- /dev/null
@@ -0,0 +1,205 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+# Richard Kettlewell <rjk@greenend.org.uk
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "Servus"
+channel = "#chiark"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+import os, time
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+slashcfg= ( 
+       __load("slashes"),
+       ' (while %s watches)',
+       "Sorry, but %s stole my pen.",
+       "Who do you want to slash?",
+       "I have writer's block!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "chiark-karma"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+    savekarma()
+def savekarma():
+    tmp = "%s.tmp" % karmafilename
+    try:
+        f = open(tmp, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+       os.rename(tmp, karmafilename)
+    except IOError, e:
+        sys.stderr.write("error writing karma: %s" % e)
+
+def quit(bot,cmd,nick,conn,public):
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    c.reloadq(bot,cmd,nick,conn,public)
+
+#The game...
+class game:
+       trigger="Servus"
+       grace=time.time()
+       minlose=24*60*60 #1 day
+       maxlose=14*minlose #2 weeks
+       losetime=time.time()+300000
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "slash": (c.slashq, slashcfg),
+           "rot13": c.rot13q,
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+#          "define": c.defineq,
+#          "game": (c.gameq,game),
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command.lower()]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
+#    elif public:
+#        if cmd.find("GAME")!=-1: #someone else lost
+#          grace.grace=time.time()+60*20
+#      elif cmd.find(game.trigger)!=-1 and len(game.trigger)>2: #we lost!
+#          c.gameq(bot,"pad "+game.trigger,bot.owner,conn,False,game)
+#      elif time.time()>game.losetime: #we randomly lost, take new trigger
+#          c.gameq(bot,cmd,bot.owner,conn,False,game)
+#          
diff --git a/Servus-chiark.py b/Servus-chiark.py
new file mode 100644 (file)
index 0000000..128300a
--- /dev/null
@@ -0,0 +1,206 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+# Richard Kettlewell <rjk@greenend.org.uk
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "Servus"
+channel = "#chiark"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+import os, time
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+slashcfg= ( 
+       __load("slashes"),
+       ' (while %s watches)',
+       "Sorry, but %s stole my pen.",
+       "Who do you want to slash?",
+       "I have writer's block!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "chiark-karma"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+    savekarma()
+def savekarma():
+    tmp = "%s.tmp" % karmafilename
+    try:
+        f = open(tmp, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+       os.rename(tmp, karmafilename)
+    except IOError, e:
+        sys.stderr.write("error writing karma: %s" % e)
+
+def quit(bot,cmd,nick,conn,public):
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    c.reloadq(bot,cmd,nick,conn,public)
+
+#The game...
+class game:
+       trigger="Servus"
+       grace=time.time()
+       minlose=24*60*60 #1 day
+       maxlose=14*minlose #2 weeks
+       losetime=time.time()+300000
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "slash": (c.slashq, slashcfg),
+           "rot13": c.rot13q,
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+           "units": c.unitq,
+#          "define": c.defineq,
+#          "game": (c.gameq,game),
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command.lower()]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
+#    elif public:
+#        if cmd.find("GAME")!=-1: #someone else lost
+#          grace.grace=time.time()+60*20
+#      elif cmd.find(game.trigger)!=-1 and len(game.trigger)>2: #we lost!
+#          c.gameq(bot,"pad "+game.trigger,bot.owner,conn,False,game)
+#      elif time.time()>game.losetime: #we randomly lost, take new trigger
+#          c.gameq(bot,cmd,bot.owner,conn,False,game)
+#          
diff --git a/Servus-chiark.py~ b/Servus-chiark.py~
new file mode 100644 (file)
index 0000000..128300a
--- /dev/null
@@ -0,0 +1,206 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+# Richard Kettlewell <rjk@greenend.org.uk
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "Servus"
+channel = "#chiark"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+import os, time
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+slashcfg= ( 
+       __load("slashes"),
+       ' (while %s watches)',
+       "Sorry, but %s stole my pen.",
+       "Who do you want to slash?",
+       "I have writer's block!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "chiark-karma"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+    savekarma()
+def savekarma():
+    tmp = "%s.tmp" % karmafilename
+    try:
+        f = open(tmp, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+       os.rename(tmp, karmafilename)
+    except IOError, e:
+        sys.stderr.write("error writing karma: %s" % e)
+
+def quit(bot,cmd,nick,conn,public):
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    c.reloadq(bot,cmd,nick,conn,public)
+
+#The game...
+class game:
+       trigger="Servus"
+       grace=time.time()
+       minlose=24*60*60 #1 day
+       maxlose=14*minlose #2 weeks
+       losetime=time.time()+300000
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "slash": (c.slashq, slashcfg),
+           "rot13": c.rot13q,
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+           "units": c.unitq,
+#          "define": c.defineq,
+#          "game": (c.gameq,game),
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command.lower()]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
+#    elif public:
+#        if cmd.find("GAME")!=-1: #someone else lost
+#          grace.grace=time.time()+60*20
+#      elif cmd.find(game.trigger)!=-1 and len(game.trigger)>2: #we lost!
+#          c.gameq(bot,"pad "+game.trigger,bot.owner,conn,False,game)
+#      elif time.time()>game.losetime: #we randomly lost, take new trigger
+#          c.gameq(bot,cmd,bot.owner,conn,False,game)
+#          
index 59b9c86..6bed11a 100755 (executable)
@@ -9,6 +9,8 @@
 # Contributors:
 # Peter Corbett <ptc24@cam.ac.uk>
 # Matthew Vernon <matthew@debian.org>
+#
+# Stephen Early <steve@greenend.org.uk> mostly deleted stuff
 # 
 # This file is part of Acrobat.
 #
 Acrobat - an extensible, minmalist irc bot.
 """
 
-import string, urllib, sys, cPickle, os, random
+import string, sys
 from ircbot import SingleServerIRCBot
 from irclib import nm_to_n, irc_lower
-import config
-
-#splitting out the configuration to a separate (source, but this is incidental-
-#it's just the nearest free parser) file.
-
-import config
-
-class Karma:
-    def __init__(self):
-        self.dict = {}
 
 class Acrobat(SingleServerIRCBot):
-    def __init__(self, channel, nickname, server, owner, port=6667):
+    def __init__(self,config):
+       server=config.server
+       port=config.port
+       nickname=config.nickname
         SingleServerIRCBot.__init__(self,
                                     [(server, port)], nickname, nickname)
-        self.channel = channel
-        self.owner = owner
+        self.channel = config.channel
+        self.owner = config.owner
         self.revision = "$Revision: 1.1 $" # global version number
-        self.trouts = config.trouts
-        self.karmafilename = config.karmafilename
         self.config = config
         
-        # load the karma db
-        try:
-            f = open(self.karmafilename, "r")
-            self.karma = cPickle.load(f)
-            f.close()
-        except IOError:
-            self.karma = Karma()
-        
     ## EVENT HANDLERS
             
     def on_welcome(self, conn, evt):
@@ -77,80 +62,32 @@ class Acrobat(SingleServerIRCBot):
         nc = string.split(payload, " ", 1)
         if len(nc) > 1 and (irc_lower(nc[0]).startswith(
             irc_lower(self.connection.get_nickname()))):
-            self.do_command(self.channel, nc[1].strip(), public = 1)
-        elif payload[0] in config.triggers and len(payload)>1:
-            self.do_command(self.channel, payload[1:].strip(), public=1)
-        elif payload.find("++") != -1 or payload.find("--") != -1:
-            self.do_command(self.channel, payload.strip(), public=1)
+            self.do_command(nm_to_n(evt.source()), nc[1].strip(), public = 1)
+        elif len(payload)>1:
+            self.do_command(nm_to_n(evt.source()), payload.strip(), public = 1)
     # General query handler
     def do_command(self, nick, cmd, public=0):
-        conn = self.connection
-        command = cmd.split()[0]
-        sys.stderr.write(command)
-        args = (self, cmd, nick, conn, public)
+       self.config.command(self,cmd,nick,self.connection,public)
 
-        # regrettably, these (and anything else with special triggers)
-        # must be special-cased, which is aesthetically unsatisfying.
-        
-        # karma: up
-        if command.endswith("++"):
-            self.karmaup(cmd)
-        # karma: down
-        if command.endswith("--"):
-            self.karmadown(cmd)
-
-        # and in the general case (this is slightly magical)
-        if command.lower() in config.commands.keys():
-            config.commands[command](*args)
-        # else do nothing.
-
-        # What this _means_ is: you write a
-        # function(bot, cmd, nick, conn, public), where bot is the bot class
-        # (ie self here), and drop it in commands.py; then add a trigger command
-        # to config.py for it, in the dictionary "commands", and it will
-        # just start working on bot restart or config reload.
-
-        # This is, IMO, quite nifty. :)
-
-    # And now the karma commands, as these pretty much have to be here :(
-    # increment karma
-    def karmaup(self, cmd):
-        if self.karma.dict.has_key(cmd.split()[0][:-2]):
-            self.karma.dict[cmd.split()[0][:-2]] += 1
-        else:
-            self.karma.dict[cmd.split()[0][:-2]] = 1
-    #decrement karma
-    def karmadown(self, cmd):
-        if self.karma.dict.has_key(cmd.split()[0][:-2]):
-            self.karma.dict[cmd.split()[0][:-2]] -= 1
-        else:
-            self.karma.dict[cmd.split()[0][:-2]] = -1
-                
+    # Convenience function - reply to a public message publicly, or
+    # a private message privately
+    def automsg(self,public,nick,msg):
+       if public:
+           self.connection.privmsg(self.channel,msg)
+       else:
+           self.connection.notice(nick, msg)
 
 def main():
-    # initialize the bot
-    bot = Acrobat(config.channel, config.nickname, config.server,
-                  config.owner, config.port)
-    sys.stderr.write("Trying to connect...\n")
-    # and the event loop
+    if len(sys.argv)!=2:
+       print "acrobat: provide configuration module name on command line"
+       sys.exit(1)
+    try:
+       c=__import__(sys.argv[1])
+    except ImportError:
+       print "acrobat: configuration module "+sys.argv[1]+".py not found"
+       sys.exit(1)
+    bot = Acrobat(c)
     bot.start()
 
-#    if len(sys.argv) != 5: # insufficient arguments
-#        print "Usage: acrobat <server[:port]> <channel> <nickname> owner"
-#        sys.exit(1)
-#    sv_port = string.split(sys.argv[1], ":", 1) # tuple; (server, port)
-#    server = sv_port[0]
-#    if len(sv_port) == 2:
-#        try:
-#            port = int(sv_port[1])
-#        except ValueError:
-#            print "Error: Erroneous port."
-#            sys.exit(1)
-#    else:
-#        port = 6667 # default irc port
-#    channel = sys.argv[2]
-#    nickname = sys.argv[3]
-#    owner = sys.argv[4]
-
 if __name__ == "__main__":
     main()
diff --git a/assassins.py b/assassins.py
new file mode 100644 (file)
index 0000000..b6ed957
--- /dev/null
@@ -0,0 +1,173 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "kern.srcf.ucam.org"
+port = 6667
+nickname = "Servus"
+channel = "#assassins"
+owner = "Atreic"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "karmadump.assassins"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+def savekarma():
+    try:
+        f = open(karmafilename, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+    except IOError:
+        sys.stderr.write("Problems dumping karma: probably lost :(")
+
+# When the bot exits we should save the karma db
+def quit(bot,cmd,nick,conn,public):
+    savekarma()
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    savekarma()
+    c.reloadq(bot,cmd,nick,conn,public)
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
diff --git a/assassins.py~ b/assassins.py~
new file mode 100644 (file)
index 0000000..6e7678f
--- /dev/null
@@ -0,0 +1,173 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "Servus"
+channel = "#chiark"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "karmadump"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+def savekarma():
+    try:
+        f = open(karmafilename, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+    except IOError:
+        sys.stderr.write("Problems dumping karma: probably lost :(")
+
+# When the bot exits we should save the karma db
+def quit(bot,cmd,nick,conn,public):
+    savekarma()
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    savekarma()
+    c.reloadq(bot,cmd,nick,conn,public)
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
index 6f6d1f7..4bdc45c 100644 (file)
 # Part of Acrobat.
-import string, cPickle, random, urllib, sys
+import string, cPickle, random, urllib, sys, time, re, os
 from irclib import irc_lower, nm_to_n
 
 # query karma
-def karmaq(bot, cmd, nick, conn, public):
-    # in public
-    if public == 1:
-        try:
-            if bot.karma.dict.has_key(cmd.split()[1]):
-                conn.privmsg(bot.channel, "%s has karma %s."
-                             %(cmd.split()[1],
-                                   bot.karma.dict[cmd.split()[1]]))
-            else:
-                conn.privmsg(bot.channel, "%s has no karma set." %
-                             cmd.split()[1])
-        except IndexError:
-            conn.privmsg(bot.channel, "I have karma on %s items." %
-                         len(bot.karma.dict.keys()))
-    # in private
-    else:
-        try:
-            if bot.karma.dict.has_key(cmd.split()[1]):
-                conn.notice(nick, "%s has karma %s." %
-                            (cmd.split()[1],
-                             bot.karma.dict[cmd.split()[1]]))
-            else:
-                conn.notice(nick, "I have karma on %s items." %
-                            len(bot.karma.dict.keys()))
-        except IndexError:
-            conn.notice(nick, "I have karma on %s items." %
-                        len(bot.karma.dict.keys()))
-# query bot status
-def infoq(bot, cmd, nick, conn, public):
-    if public == 1:
-        conn.privmsg(bot.channel,
-                     "I am Acrobat %s, on %s, as nick %s." %
-                    (bot.revision.split()[1], bot.channel, conn.get_nickname()))
-        conn.privmsg(bot.channel,
-                     "My owner is %s; I have karma on %s items." %
-                     (bot.owner, len(bot.karma.dict.keys())))
+def karmaq(bot, cmd, nick, conn, public, karma):
+    try:
+       item=cmd.split()[1].lower()
+    except IndexError:
+       item=None
+    if item==None:
+       bot.automsg(public,nick,"I have karma on %s items." %
+                        len(karma.keys()))
+    elif karma.has_key(item):
+        bot.automsg(public,nick,"%s has karma %s."
+                     %(item,karma[item]))
     else:
-        conn.notice(nick, "I am Acrobat %s, on %s, as nick %s." %
-                    (bot.revision.split()[1], bot.channel, conn.get_nickname()))
-        conn.notice(nick, "My owner is %s; I have karma on %s items." %
-                    (bot.owner, len(bot.karma.dict.keys())))
+        bot.automsg(public,nick, "%s has no karma set." % item)
 
-# trout someone
-def troutq(bot, cmd, nick, conn, public):
+# delete karma
+def karmadelq(bot, cmd, nick, conn, public, karma):
     try:
-        target = string.join(cmd.split()[1:])
-        me = bot.connection.get_nickname()
-        trout_msg = random.choice(bot.trouts)
-        # The bot won't trout itself;
-        if irc_lower(me) == irc_lower(target):
-            target = nick
-        conn.action(bot.channel, trout_msg % target)
-        if public == 0:
-            if random.random() <= bot.config.selftroutrisk:
-                conn.action(bot.channel,
-                 "notes %s is conducting a whispering campaign." % nick)
+       item=cmd.split()[1].lower()
     except IndexError:
-        conn.notice(nick, "Who do you wish me to trout?")
+       conn.notice(nick, "What should I delete?")
+       return
+    if nick != bot.owner:
+       conn.notice(nick, "You are not my owner.")
+       return
+    if karma.has_key(item):
+       del karma[item]
+       conn.notice(nick, "Item %s deleted."%item)
+    else:
+       conn.notice(nick, "There is no karma stored for %s."%item)
+
+# help - provides the URL of the help file
+def helpq(bot, cmd, nick, conn, public):
+    bot.automsg(public,nick,
+                "For help see http://www.pick.ucam.org/~matthew/irc/servus.html")
+
+
+# query bot status
+def infoq(bot, cmd, nick, conn, public, karma):
+    bot.automsg(public,nick,
+       ("I am Acrobat %s, on %s, as nick %s.  "+
+       "My owner is %s; I have karma on %s items.") %
+       (bot.revision.split()[1], bot.channel, conn.get_nickname(),
+        bot.owner, len(karma.keys())))
+
+# Check on fish stocks
+def fish_quota(pond):
+    if pond.DoS:
+        if time.time()>=pond.quotatime:
+            pond.DoS=0
+        else:
+            return
+    if (time.time()-pond.quotatime)>pond.fish_time_inc:
+        pond.cur_fish+=(((time.time()-pond.quotatime)
+                         /pond.fish_time_inc)*pond.fish_inc)
+        if pond.cur_fish>pond.max_fish:
+            pond.cur_fish=pond.max_fish
+        pond.quotatime=time.time()
+
+# trout someone, or flirt with them
+def troutq(bot, cmd, nick, conn, public, cfg):
+    fishlist=cfg[0]
+    selftrout=cfg[1]
+    quietmsg=cfg[2]
+    notargetmsg=cfg[3]
+    nofishmsg=cfg[4]
+    fishpond=cfg[5]
+    selftroutchance=cfg[6]
+
+    fish_quota(fishpond)
+    if fishpond.DoS:
+        conn.notice(nick, quietmsg%fishpond.Boring_Git)
+       return
+    if fishpond.cur_fish<=0:
+        conn.notice(nick, nofishmsg)
+       return
+    target = string.join(cmd.split()[1:])
+    if len(target)==0:
+       conn.notice(nick, notargetmsg)
+       return
+    me = bot.connection.get_nickname()
+    trout_msg = random.choice(fishlist)
+    # The bot won't trout or flirt with itself;
+    if irc_lower(me) == irc_lower(target):
+        target = nick
+    # There's a chance the game may be given away if the request was not
+    # public...
+    if not public:
+       if random.random()<=selftroutchance:
+           trout_msg=trout_msg+(selftrout%nick)
+
+    conn.action(bot.channel, trout_msg % target)
+    fishpond.cur_fish-=1
+
+# slash a pair
+def slashq(bot, cmd, nick, conn, public, cfg):
+    fishlist=cfg[0]
+    selfslash=cfg[1]
+    quietmsg=cfg[2]
+    notargetmsg=cfg[3]
+    nofishmsg=cfg[4]
+    fishpond=cfg[5]
+    selfslashchance=cfg[6]
+
+    fish_quota(fishpond)
+    if fishpond.DoS:
+        conn.notice(nick, quietmsg%fishpond.Boring_Git)
+       return
+    if fishpond.cur_fish<=0:
+        conn.notice(nick, nofishmsg)
+       return
+    target = string.join(cmd.split()[1:])
+    #who = cmd.split()[1:]
+    who = ' '.join(cmd.split()[1:]).split(' / ')
+    if len(who) < 2:
+       conn.notice(nick, "it takes two to tango!")
+       return
+    elif len(who) > 2:
+       conn.notice(nick, "we'll have none of that round here")
+       return
+    me = bot.connection.get_nickname()
+    slash_msg = random.choice(fishlist)
+    # The bot won't slash people with themselves
+    if irc_lower(who[0]) == irc_lower(who[1]):
+       conn.notice(nick, "oooooh no missus!")
+       return
+    # The bot won't slash with itself, instead slashing the requester
+    for n in [0,1]:
+       if irc_lower(me) == irc_lower(who[n]):
+           who[n] = nick
+    # Perhaps someone asked to slash themselves with the bot then we get
+    if irc_lower(who[0]) == irc_lower(who[1]):
+       conn.notice(nick, "you wish!")
+       return
+    # There's a chance the game may be given away if the request was not
+    # public...
+    if not public:
+       if random.random()<=selfslashchance:
+           slash_msg=slash_msg+(selfslash%nick)
+
+    conn.action(bot.channel, slash_msg % (who[0], who[1]))
+    fishpond.cur_fish-=1
+
+#query units
+def unitq(bot, cmd, nick, conn, public):
+    args = ' '.join(cmd.split()[1:]).split(' as ')
+    if len(args) != 2:
+        args = ' '.join(cmd.split()[1:]).split(' / ')
+        if len(args) != 2:
+            conn.notice(nick, "syntax: units arg1 as arg2")
+            return
+    if args[1]=='?':
+        sin,sout=os.popen2(["units","--verbose",args[0]],"r")
+    else:
+        sin,sout=os.popen2(["units","--verbose",args[0],args[1]],"r")
+    sin.close()
+    res=sout.readlines()
+    #popen2 doesn't clean up the child properly. Do this by hand
+    child=os.wait()
+    if os.WEXITSTATUS(child[1])==0:
+        bot.automsg(public,nick,res[0].strip())
+    else:
+        conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
+
+# Shut up trouting for a minute
+def nofishq(bot, cmd, nick, conn, public, fish):
+    fish.cur_fish=0
+    fish.DoS=1
+    fish.Boring_Git=nick
+    fish.quotatime=time.time()
+    fish.quotatime+=fish.nofish_time
+    conn.notice(nick, "Fish stocks depleted, as you wish.")
 
 # rehash bot config
 def reloadq(bot, cmd, nick, conn, public):
-    if irc_lower(nick) == irc_lower(bot.owner):
+    if not public and irc_lower(nick) == irc_lower(bot.owner):
         try:
             reload(bot.config)
-            bot.trouts = bot.config.trouts
-            conn.privmsg(nick, "Config reloaded.")
+            conn.notice(nick, "Config reloaded.")
         except ImportError:
             conn.notice(nick, "Config reloading failed!")
     else:
-        conn.notice(nick, "This command can only be invoked by my owner.")
+        bot.automsg(public,nick,
+               "Configuration can only be reloaded by my owner, by /msg.")
+
+# lose the game and/or install a new trigger word
+def gameq(bot, cmd, nick, conn, public, game):
+    #only install a new trigger if it's not too short.
+    if len(' '.join(cmd.split()[1:]))>2:
+        game.trigger=' '.join(cmd.split()[1:])
+    if (time.time()> game.grace):
+        if not public:
+            if irc_lower(nick) == irc_lower(bot.owner):
+                conn.action(bot.channel,"loses the game!")
+            else:
+                conn.privmsg(bot.channel,nick+" just lost the game!")
+    else:
+        if not public:
+            conn.notice(nick, "It's a grace period!")
+    game.grace=time.time()+60*20 #20 minutes' grace
+    game.losetime=time.time()+random.randrange(game.minlose,game.maxlose)
+    conn.notice(bot.owner, str(game.losetime-time.time())+" "+game.trigger)
 
 # quit irc
 def quitq(bot, cmd, nick, conn, public):
     if irc_lower(nick) == irc_lower(bot.owner):
-        try:
-            f = open(bot.karmafilename, "w")
-            cPickle.dump(bot.karma, f)
-            f.close()
-        except IOError:
-            sys.stderr.write("Problems dumping karma: probably lost :(")
         bot.die(msg = "I have been chosen!")
-    elif public == 1:
-        conn.privmsg(nick, "Such aggression in public!")
+    elif public:
+        conn.notice(nick, "Such aggression in public!")
     else:
         conn.notice(nick, "You're not my owner.")
 
 # google for something
 def googleq(bot, cmd, nick, conn, public):
     cmdrest = string.join(cmd.split()[1:])
-    #sys.stderr.write(conn)
     # "I'm Feeling Lucky" rather than try and parse the html
     targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
             % urllib.quote_plus(cmdrest))
@@ -100,40 +224,69 @@ def googleq(bot, cmd, nick, conn, public):
         # get redirected and grab the resulting url for returning
         gsearch = urllib.urlopen(targ).geturl()
         if gsearch != targ: # we've found something
-            if public == 0:
-                conn.notice(nick, str(gsearch))
-            else: # we haven't found anything.
-                conn.privmsg(nick, str(gsearch))
-        else:
-            if public == 0:
-                conn.notice(nick, "No pages found.")
-            else:
-                conn.privmsg(nick, "No pages found.")
+            bot.automsg(public,nick,str(gsearch))
+        else: # we haven't found anything.
+           bot.automsg(public,nick,"No pages found.")
     except IOError: # if the connection times out. This blocks. :(
-        if public == 0:
-            conn.notice(nick, "The web's broken. Waah!")
+       bot.automsg(public,nick,"The web's broken. Waah!")
+
+# Look up the definition of something using google
+def defineq(bot, cmd, nick, conn, public):
+    cmdrest = string.join(cmd.split()[1:])
+    targ = ("http://www.google.com/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
+            % urllib.quote_plus(cmdrest))
+    try:
+        # Just slurp everything into a string
+        defnpage = urllib.urlopen(targ).read()
+        # For definitions we really do have to parse the HTML, sadly.
+        # This is of course going to be a bit fragile. We first look for
+        # 'Definitions of %s on the Web' -- if this isn't present we
+        # assume we have the 'no definitions found page'.
+        # The first defn starts after the following <p> tag.
+        # Following that we assume that each definition is all the non-markup
+        # before a <br> tag. Currently we just dump out the first definition.
+        match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<p>\s*([^>]*)<br>",defnpage,re.MULTILINE)
+        if match == None:
+           bot.automsg(public,nick,"Some things defy definition.")
         else:
-            conn.privmsg(nick, "The web's broken. Waah!")
+           # We assume google has truncated the definition for us so this
+           # won't flood the channel with text...
+           defn = " ".join(match.group(1).split("\n"));
+           bot.automsg(public,nick,defn)
+    except IOError: # if the connection times out. This blocks. :(
+         bot.automsg(public,nick,"The web's broken. Waah!")
 
 ### say to msg/channel            
 def sayq(bot, cmd, nick, conn, public):
     if irc_lower(nick) == irc_lower(bot.owner):
         conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
     else:
-        conn.privmsg(nick, "You're not my owner!")
+        if not public:
+           conn.notice(nick, "You're not my owner!")
 
 ### action to msg/channel
 def doq(bot, cmd, nick, conn, public):
     sys.stderr.write(irc_lower(bot.owner))
     sys.stderr.write(irc_lower(nick))
-    if public == 0:
+    if not public:
         if irc_lower(nick) == irc_lower(bot.owner):
             conn.action(bot.channel, string.join(cmd.split()[1:]))
         else:
-            conn.privmsg(nick, "You're not my owner!")
+            conn.notice(nick, "You're not my owner!")
 
 ###disconnect
 def disconnq(bot, cmd, nick, conn, public):
     if cmd == "disconnect": # hop off for 60s
         bot.disconnect(msg="Be right back.")
 
+### list keys of a dictionary
+def listkeysq(bot, cmd, nick, conn, public, dict):
+    bot.automsg(public,nick,string.join(dict.keys()))
+
+### rot13 text (yes, I could have typed out the letters....)
+### also "foo".encode('rot13') would have worked
+def rot13q(bot, cmd, nick, conn, public):
+    a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
+    b=a[13:]+a[:13]
+    trans=string.maketrans(a+a.upper(),b+b.upper())
+    conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
diff --git a/commands.py.orig b/commands.py.orig
new file mode 100644 (file)
index 0000000..026861e
--- /dev/null
@@ -0,0 +1,164 @@
+# Part of Acrobat.
+import string, cPickle, random, urllib, sys, time
+from irclib import irc_lower, nm_to_n
+
+# query karma
+def karmaq(bot, cmd, nick, conn, public, karma):
+    try:
+       item=cmd.split()[1].lower()
+    except IndexError:
+       item=None
+    if item==None:
+       bot.automsg(public,nick,"I have karma on %s items." %
+                        len(karma.keys()))
+    elif karma.has_key(item):
+        bot.automsg(public,nick,"%s has karma %s."
+                     %(item,karma[item]))
+    else:
+        bot.automsg(public,nick, "%s has no karma set." % item)
+
+# delete karma
+def karmadelq(bot, cmd, nick, conn, public, karma):
+    try:
+       item=cmd.split()[1].lower()
+    except IndexError:
+       conn.notice(nick, "What should I delete?")
+       return
+    if nick != bot.owner:
+       conn.notice(nick, "You are not my owner.")
+       return
+    if karma.has_key(item):
+       del karma[item]
+       conn.notice(nick, "Item %s deleted."%item)
+    else:
+       conn.notice(nick, "There is no karma stored for %s."%item)
+
+# query bot status
+def infoq(bot, cmd, nick, conn, public, karma):
+    bot.automsg(public,nick,
+       ("I am Acrobat %s, on %s, as nick %s.  "+
+       "My owner is %s; I have karma on %s items.") %
+       (bot.revision.split()[1], bot.channel, conn.get_nickname(),
+        bot.owner, len(karma.keys())))
+
+# Check on fish stocks
+def fish_quota(pond):
+    if pond.DoS:
+        if time.time()>=pond.quotatime:
+            pond.DoS=0
+        else:
+            return
+    if (time.time()-pond.quotatime)>pond.fish_time_inc:
+        pond.cur_fish+=(((time.time()-pond.quotatime)
+                         /pond.fish_time_inc)*pond.fish_inc)
+        if pond.cur_fish>pond.max_fish:
+            pond.cur_fish=pond.max_fish
+        pond.quotatime=time.time()
+
+# trout someone, or flirt with them
+def troutq(bot, cmd, nick, conn, public, cfg):
+    fishlist=cfg[0]
+    selftrout=cfg[1]
+    quietmsg=cfg[2]
+    notargetmsg=cfg[3]
+    nofishmsg=cfg[4]
+    fishpond=cfg[5]
+    selftroutchance=cfg[6]
+
+    fish_quota(fishpond)
+    if fishpond.DoS:
+        conn.notice(nick, quietmsg%fishpond.Boring_Git)
+       return
+    if fishpond.cur_fish<=0:
+        conn.notice(nick, nofishmsg)
+       return
+    target = string.join(cmd.split()[1:])
+    if len(target)==0:
+       conn.notice(nick, notargetmsg)
+       return
+    me = bot.connection.get_nickname()
+    trout_msg = random.choice(fishlist)
+    # The bot won't trout or flirt with itself;
+    if irc_lower(me) == irc_lower(target):
+        target = nick
+    # There's a chance the game may be given away if the request was not
+    # public...
+    if not public:
+       if random.random()<=selftroutchance:
+           trout_msg=trout_msg+(selftrout%nick)
+
+    conn.action(bot.channel, trout_msg % target)
+    fishpond.cur_fish-=1
+
+# Shut up trouting for a minute
+def nofishq(bot, cmd, nick, conn, public, fish):
+    fish.cur_fish=0
+    fish.DoS=1
+    fish.Boring_Git=nick
+    fish.quotatime=time.time()
+    fish.quotatime+=fish.nofish_time
+    conn.notice(nick, "Fish stocks depleted, as you wish.")
+
+# rehash bot config
+def reloadq(bot, cmd, nick, conn, public):
+    if not public and irc_lower(nick) == irc_lower(bot.owner):
+        try:
+            reload(bot.config)
+            conn.notice(nick, "Config reloaded.")
+        except ImportError:
+            conn.notice(nick, "Config reloading failed!")
+    else:
+        bot.automsg(public,nick,
+               "Configuration can only be reloaded by my owner, by /msg.")
+
+# quit irc
+def quitq(bot, cmd, nick, conn, public):
+    if irc_lower(nick) == irc_lower(bot.owner):
+        bot.die(msg = "I have been chosen!")
+    elif public:
+        conn.notice(nick, "Such aggression in public!")
+    else:
+        conn.notice(nick, "You're not my owner.")
+
+# google for something
+def googleq(bot, cmd, nick, conn, public):
+    cmdrest = string.join(cmd.split()[1:])
+    # "I'm Feeling Lucky" rather than try and parse the html
+    targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
+            % urllib.quote_plus(cmdrest))
+    try:
+        # get redirected and grab the resulting url for returning
+        gsearch = urllib.urlopen(targ).geturl()
+        if gsearch != targ: # we've found something
+            bot.automsg(public,nick,str(gsearch))
+        else: # we haven't found anything.
+           bot.automsg(public,nick,"No pages found.")
+    except IOError: # if the connection times out. This blocks. :(
+       bot.automsg(public,nick,"The web's broken. Waah!")
+
+### say to msg/channel            
+def sayq(bot, cmd, nick, conn, public):
+    if irc_lower(nick) == irc_lower(bot.owner):
+        conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
+    else:
+        if not public:
+           conn.notice(nick, "You're not my owner!")
+
+### action to msg/channel
+def doq(bot, cmd, nick, conn, public):
+    sys.stderr.write(irc_lower(bot.owner))
+    sys.stderr.write(irc_lower(nick))
+    if not public:
+        if irc_lower(nick) == irc_lower(bot.owner):
+            conn.action(bot.channel, string.join(cmd.split()[1:]))
+        else:
+            conn.notice(nick, "You're not my owner!")
+
+###disconnect
+def disconnq(bot, cmd, nick, conn, public):
+    if cmd == "disconnect": # hop off for 60s
+        bot.disconnect(msg="Be right back.")
+
+### list keys of a dictionary
+def listkeysq(bot, cmd, nick, conn, public, dict):
+    bot.automsg(public,nick,string.join(dict.keys()))
diff --git a/commands.py.rej b/commands.py.rej
new file mode 100644 (file)
index 0000000..271ff08
--- /dev/null
@@ -0,0 +1,42 @@
+***************
+*** 133,138 ****
+              bot.automsg(public,nick,str(gsearch))
+          else: # we haven't found anything.
+             bot.automsg(public,nick,"No pages found.")
+      except IOError: # if the connection times out. This blocks. :(
+         bot.automsg(public,nick,"The web's broken. Waah!")
+  
+--- 133,165 ----
+              bot.automsg(public,nick,str(gsearch))
+          else: # we haven't found anything.
+             bot.automsg(public,nick,"No pages found.")
++     except IOError: # if the connection times out. This blocks. :(
++        bot.automsg(public,nick,"The web's broken. Waah!")
++ 
++ # Look up the definition of something using google
++ def defineq(bot, cmd, nick, conn, public):
++     cmdrest = string.join(cmd.split()[1:])
++     targ = ("http://www.google.com/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
++             % urllib.quote_plus(cmdrest))
++     try:
++         # Just slurp everything into a string
++         defnpage = urllib.urlopen(targ).read()
++         # For definitions we really do have to parse the HTML, sadly.
++         # This is of course going to be a bit fragile. We first look for
++         # 'Definitions of %s on the Web' -- if this isn't present we
++         # assume we have the 'no definitions found page'.
++         # The first defn starts after the following <p> tag.
++         # Following that we assume that each definition is all the non-markup
++         # before a <br> tag. Currently we just dump out the first definition.
++         match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<p>\s*([^>]*)<br>",defnpage,re.MULTILINE)
++         if match == None:
++            bot.automsg(public,nick,"Some things defy definition.")
++         else:
++            # We assume google has truncated the definition for us so this
++            # won't flood the channel with text...
++            defn = " ".join(match.group(1).split("\n"));
++            bot.automsg(public,nick,defn)
++ 
+      except IOError: # if the connection times out. This blocks. :(
+         bot.automsg(public,nick,"The web's broken. Waah!")
+  
diff --git a/commands.py~ b/commands.py~
new file mode 100644 (file)
index 0000000..2bf2a98
--- /dev/null
@@ -0,0 +1,291 @@
+# Part of Acrobat.
+import string, cPickle, random, urllib, sys, time, re, os
+from irclib import irc_lower, nm_to_n
+
+# query karma
+def karmaq(bot, cmd, nick, conn, public, karma):
+    try:
+       item=cmd.split()[1].lower()
+    except IndexError:
+       item=None
+    if item==None:
+       bot.automsg(public,nick,"I have karma on %s items." %
+                        len(karma.keys()))
+    elif karma.has_key(item):
+        bot.automsg(public,nick,"%s has karma %s."
+                     %(item,karma[item]))
+    else:
+        bot.automsg(public,nick, "%s has no karma set." % item)
+
+# delete karma
+def karmadelq(bot, cmd, nick, conn, public, karma):
+    try:
+       item=cmd.split()[1].lower()
+    except IndexError:
+       conn.notice(nick, "What should I delete?")
+       return
+    if nick != bot.owner:
+       conn.notice(nick, "You are not my owner.")
+       return
+    if karma.has_key(item):
+       del karma[item]
+       conn.notice(nick, "Item %s deleted."%item)
+    else:
+       conn.notice(nick, "There is no karma stored for %s."%item)
+
+# help - provides the URL of the help file
+def helpq(bot, cmd, nick, conn, public):
+    bot.automsg(public,nick,
+                "For help see http://www.pick.ucam.org/~matthew/irc/servus.html")
+
+
+# query bot status
+def infoq(bot, cmd, nick, conn, public, karma):
+    bot.automsg(public,nick,
+       ("I am Acrobat %s, on %s, as nick %s.  "+
+       "My owner is %s; I have karma on %s items.") %
+       (bot.revision.split()[1], bot.channel, conn.get_nickname(),
+        bot.owner, len(karma.keys())))
+
+# Check on fish stocks
+def fish_quota(pond):
+    if pond.DoS:
+        if time.time()>=pond.quotatime:
+            pond.DoS=0
+        else:
+            return
+    if (time.time()-pond.quotatime)>pond.fish_time_inc:
+        pond.cur_fish+=(((time.time()-pond.quotatime)
+                         /pond.fish_time_inc)*pond.fish_inc)
+        if pond.cur_fish>pond.max_fish:
+            pond.cur_fish=pond.max_fish
+        pond.quotatime=time.time()
+
+# trout someone, or flirt with them
+def troutq(bot, cmd, nick, conn, public, cfg):
+    fishlist=cfg[0]
+    selftrout=cfg[1]
+    quietmsg=cfg[2]
+    notargetmsg=cfg[3]
+    nofishmsg=cfg[4]
+    fishpond=cfg[5]
+    selftroutchance=cfg[6]
+
+    fish_quota(fishpond)
+    if fishpond.DoS:
+        conn.notice(nick, quietmsg%fishpond.Boring_Git)
+       return
+    if fishpond.cur_fish<=0:
+        conn.notice(nick, nofishmsg)
+       return
+    target = string.join(cmd.split()[1:])
+    if len(target)==0:
+       conn.notice(nick, notargetmsg)
+       return
+    me = bot.connection.get_nickname()
+    trout_msg = random.choice(fishlist)
+    # The bot won't trout or flirt with itself;
+    if irc_lower(me) == irc_lower(target):
+        target = nick
+    # There's a chance the game may be given away if the request was not
+    # public...
+    if not public:
+       if random.random()<=selftroutchance:
+           trout_msg=trout_msg+(selftrout%nick)
+
+    conn.action(bot.channel, trout_msg % target)
+    fishpond.cur_fish-=1
+
+# slash a pair
+def slashq(bot, cmd, nick, conn, public, cfg):
+    fishlist=cfg[0]
+    selfslash=cfg[1]
+    quietmsg=cfg[2]
+    notargetmsg=cfg[3]
+    nofishmsg=cfg[4]
+    fishpond=cfg[5]
+    selfslashchance=cfg[6]
+
+    fish_quota(fishpond)
+    if fishpond.DoS:
+        conn.notice(nick, quietmsg%fishpond.Boring_Git)
+       return
+    if fishpond.cur_fish<=0:
+        conn.notice(nick, nofishmsg)
+       return
+    target = string.join(cmd.split()[1:])
+    #who = cmd.split()[1:]
+    who = ' '.join(cmd.split()[1:]).split(' / ')
+    if len(who) < 2:
+       conn.notice(nick, "it takes two to tango!")
+       return
+    elif len(who) > 2:
+       conn.notice(nick, "we'll have none of that round here")
+       return
+    me = bot.connection.get_nickname()
+    slash_msg = random.choice(fishlist)
+    # The bot won't slash people with themselves
+    if irc_lower(who[0]) == irc_lower(who[1]):
+       conn.notice(nick, "oooooh no missus!")
+       return
+    # The bot won't slash with itself, instead slashing the requester
+    for n in [0,1]:
+       if irc_lower(me) == irc_lower(who[n]):
+           who[n] = nick
+    # Perhaps someone asked to slash themselves with the bot then we get
+    if irc_lower(who[0]) == irc_lower(who[1]):
+       conn.notice(nick, "you wish!")
+       return
+    # There's a chance the game may be given away if the request was not
+    # public...
+    if not public:
+       if random.random()<=selfslashchance:
+           slash_msg=slash_msg+(selfslash%nick)
+
+    conn.action(bot.channel, slash_msg % (who[0], who[1]))
+    fishpond.cur_fish-=1
+
+#query units
+def unitq(bot, cmd, nick, conn, public):
+    args = ' '.join(cmd.split()[1:]).split(' as ')
+    if len(args) != 2:
+        args = ' '.join(cmd.split()[1:]).split(' / ')
+        if len(args) != 2:
+            conn.notice(nick, "syntax: units arg1 as arg2")
+            return
+    if args[1]=='?':
+        sin,sout=os.popen2(["units","--verbose",args[0]],"r")
+    else:
+        sin,sout=os.popen2(["units","--verbose",args[0],args[1]],"r")
+    sin.close()
+    res=sout.readlines()
+    #popen2 doesn't clean up the child properly. Do this by hand
+    child=os.wait()
+    if os.WEXITSTATUS(child[1])==0:
+        bot.automsg(public,nick,res[0].strip())
+    else:
+        conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
+
+# Shut up trouting for a minute
+def nofishq(bot, cmd, nick, conn, public, fish):
+    fish.cur_fish=0
+    fish.DoS=1
+    fish.Boring_Git=nick
+    fish.quotatime=time.time()
+    fish.quotatime+=fish.nofish_time
+    conn.notice(nick, "Fish stocks depleted, as you wish.")
+
+# rehash bot config
+def reloadq(bot, cmd, nick, conn, public):
+    if not public and irc_lower(nick) == irc_lower(bot.owner):
+        try:
+            reload(bot.config)
+            conn.notice(nick, "Config reloaded.")
+        except ImportError:
+            conn.notice(nick, "Config reloading failed!")
+    else:
+        bot.automsg(public,nick,
+               "Configuration can only be reloaded by my owner, by /msg.")
+
+# lose the game and/or install a new trigger word
+def gameq(bot, cmd, nick, conn, public, game):
+    #only install a new trigger if it's not too short.
+    if len(' '.join(cmd.split()[1:]))>2:
+        game.trigger=' '.join(cmd.split()[1:])
+    if (time.time()> game.grace):
+        if not public:
+            if irc_lower(nick) == irc_lower(bot.owner):
+                conn.action(bot.channel,"loses the game!")
+            else:
+                conn.privmsg(bot.channel,nick+" just lost the game!")
+    else:
+        if not public:
+            conn.notice(nick, "It's a grace period!")
+    game.grace=time.time()+60*20 #20 minutes' grace
+    game.losetime=time.time()+random.randrange(game.minlose,game.maxlose)
+    conn.notice(bot.owner, str(game.losetime-time.time())+" "+game.trigger)
+
+# quit irc
+def quitq(bot, cmd, nick, conn, public):
+    if irc_lower(nick) == irc_lower(bot.owner):
+        bot.die(msg = "I have been chosen!")
+    elif public:
+        conn.notice(nick, "Such aggression in public!")
+    else:
+        conn.notice(nick, "You're not my owner.")
+
+# google for something
+def googleq(bot, cmd, nick, conn, public):
+    cmdrest = string.join(cmd.split()[1:])
+    # "I'm Feeling Lucky" rather than try and parse the html
+    targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
+            % urllib.quote_plus(cmdrest))
+    try:
+        # get redirected and grab the resulting url for returning
+        gsearch = urllib.urlopen(targ).geturl()
+        if gsearch != targ: # we've found something
+            bot.automsg(public,nick,str(gsearch))
+        else: # we haven't found anything.
+           bot.automsg(public,nick,"No pages found.")
+    except IOError: # if the connection times out. This blocks. :(
+       bot.automsg(public,nick,"The web's broken. Waah!")
+
+# Look up the definition of something using google
+def defineq(bot, cmd, nick, conn, public):
+    cmdrest = string.join(cmd.split()[1:])
+    targ = ("http://www.google.com/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
+            % urllib.quote_plus(cmdrest))
+    try:
+        # Just slurp everything into a string
+        defnpage = urllib.urlopen(targ).read()
+        # For definitions we really do have to parse the HTML, sadly.
+        # This is of course going to be a bit fragile. We first look for
+        # 'Definitions of %s on the Web' -- if this isn't present we
+        # assume we have the 'no definitions found page'.
+        # The first defn starts after the following <p> tag.
+        # Following that we assume that each definition is all the non-markup
+        # before a <br> tag. Currently we just dump out the first definition.
+        match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<p>\s*([^>]*)<br>",defnpage,re.MULTILINE)
+        if match == None:
+           bot.automsg(public,nick,"Some things defy definition.")
+        else:
+           # We assume google has truncated the definition for us so this
+           # won't flood the channel with text...
+           defn = " ".join(match.group(1).split("\n"));
+           bot.automsg(public,nick,defn)
+    except IOError: # if the connection times out. This blocks. :(
+         bot.automsg(public,nick,"The web's broken. Waah!")
+
+### say to msg/channel            
+def sayq(bot, cmd, nick, conn, public):
+    if irc_lower(nick) == irc_lower(bot.owner):
+        conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
+    else:
+        if not public:
+           conn.notice(nick, "You're not my owner!")
+
+### action to msg/channel
+def doq(bot, cmd, nick, conn, public):
+    sys.stderr.write(irc_lower(bot.owner))
+    sys.stderr.write(irc_lower(nick))
+    if not public:
+        if irc_lower(nick) == irc_lower(bot.owner):
+            conn.action(bot.channel, string.join(cmd.split()[1:]))
+        else:
+            conn.notice(nick, "You're not my owner!")
+
+###disconnect
+def disconnq(bot, cmd, nick, conn, public):
+    if cmd == "disconnect": # hop off for 60s
+        bot.disconnect(msg="Be right back.")
+
+### list keys of a dictionary
+def listkeysq(bot, cmd, nick, conn, public, dict):
+    bot.automsg(public,nick,string.join(dict.keys()))
+
+### rot13 text (yes, I could have typed out the letters....)
+def rot13q(bot, cmd, nick, conn, public):
+    a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
+    b=a[13:]+a[:13]
+    trans=string.maketrans(a+a.upper(),b+b.upper())
+    conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
index df074e0..1f989c6 100644 (file)
--- a/config.py
+++ b/config.py
 # Andrew Walkingshaw <andrew@lexical.org.uk>
 # Peter Corbett <ptc24@cam.ac.uk>
 # Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
 
 # Acrobat configuration file
-# this is just python source, so has to be formatted as such
-from commands import * 
 
-karmafilename = "karmadump"
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "Testbot"
+channel = "#test"
+owner = "Emperor"
+# Also a function called "command"; see later.
 
-trouts = ("questions %s's parentage.",
-          "slaps %s about a bit with a wet trout",
-          "thumbs its nose at %s.")
+# Everything else in this file is configuration-specific.
 
-selftroutrisk = 0.1 # some number between 0 and 1...
-                    # the risk of a trout rebounding on the trouter!
-                    
-server = "irc.barrysworld.com"
+# Most command implementations are stored in a separate module.
+import commands as c
 
-port = 6667
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
 
-nickname = "Acrobat"
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
 
-channel = "#acrotest"
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
 
-owner = "Acronym"
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                                   
+# Karma implementation
+import cPickle
+karmafilename = "karmadump"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+def savekarma():
+    try:
+        f = open(karmafilename, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+    except IOError:
+        sys.stderr.write("Problems dumping karma: probably lost :(")
+
+# When the bot exits we should save the karma db
+def quit(bot,cmd,nick,conn,public):
+    savekarma()
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    savekarma()
+    c.reloadq(bot,cmd,nick,conn,public)
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+            "flirt": (c.troutq,flirtcfg),
+           "fish": (fishq,fish),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+            "google": c.googleq,
+           "define": c.defineq,
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
 
 triggers = ("!", "~") # what character should the bot be invoked by:
                       # eg !trout, ~trout etc.
 
-# these are "command": (function in commands.py) pairs.
-
-commands = {"karma": karmaq,
-            "info": infoq,
-            "trout": troutq,
-            "reload": reloadq,
-            "quit": quitq,
-            "google": googleq,
-            "say": sayq,
-            "do": doq,
-            "disconnect": disconnq,
-            "hop": disconnq }
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
diff --git a/config.py~ b/config.py~
new file mode 100644 (file)
index 0000000..5701288
--- /dev/null
@@ -0,0 +1,172 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "Testbot"
+channel = "#test"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                                   
+# Karma implementation
+import cPickle
+karmafilename = "karmadump"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+def savekarma():
+    try:
+        f = open(karmafilename, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+    except IOError:
+        sys.stderr.write("Problems dumping karma: probably lost :(")
+
+# When the bot exits we should save the karma db
+def quit(bot,cmd,nick,conn,public):
+    savekarma()
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    savekarma()
+    c.reloadq(bot,cmd,nick,conn,public)
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+            "flirt": (c.troutq,flirtcfg),
+           "fish": (fishq,fish),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+            "google": c.googleq,
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
diff --git a/define.patch b/define.patch
new file mode 100644 (file)
index 0000000..eed52c4
--- /dev/null
@@ -0,0 +1,43 @@
+--- servus-orig/commands.py     Thu Nov 21 01:26:06 2002
++++ servus/commands.py  Sat Jan 29 18:13:37 2005
+@@ -1,5 +1,5 @@
+ # Part of Acrobat.
+-import string, cPickle, random, urllib, sys, time
++import string, cPickle, random, urllib, sys, time, re
+ from irclib import irc_lower, nm_to_n
+
+ # query karma
+@@ -133,6 +133,33 @@
+             bot.automsg(public,nick,str(gsearch))
+         else: # we haven't found anything.
+            bot.automsg(public,nick,"No pages found.")
++    except IOError: # if the connection times out. This blocks. :(
++       bot.automsg(public,nick,"The web's broken. Waah!")
++
++# Look up the definition of something using google
++def defineq(bot, cmd, nick, conn, public):
++    cmdrest = string.join(cmd.split()[1:])
++    targ = ("http://www.google.com/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
++            % urllib.quote_plus(cmdrest))
++    try:
++        # Just slurp everything into a string
++        defnpage = urllib.urlopen(targ).read()
++        # For definitions we really do have to parse the HTML, sadly.
++        # This is of course going to be a bit fragile. We first look for
++        # 'Definitions of %s on the Web' -- if this isn't present we
++        # assume we have the 'no definitions found page'.
++        # The first defn starts after the following <p> tag.
++        # Following that we assume that each definition is all the non-markup
++        # before a <br> tag. Currently we just dump out the first definition.
++        match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<p>\s*([^>]*)<br>",defnpage,re.MULTILINE)
++        if match == None:
++           bot.automsg(public,nick,"Some things defy definition.")
++        else:
++           # We assume google has truncated the definition for us so this
++           # won't flood the channel with text...
++           defn = " ".join(match.group(1).split("\n"));
++           bot.automsg(public,nick,defn)
++
+     except IOError: # if the connection times out. This blocks. :(
+        bot.automsg(public,nick,"The web's broken. Waah!")
+
diff --git a/flirts.~1.12.~ b/flirts.~1.12.~
new file mode 100644 (file)
index 0000000..06a16e2
--- /dev/null
@@ -0,0 +1,65 @@
+covers %s in blancmange
+jumps for joy at the thought of %s
+offers %s a bunch of flowers
+blows kisses at %s
+falls head over heels in love with %s
+offers to take %s out for dinner
+is %s's kind of robot
+offers %s a Turkish Delight
+gazes contentedly at %s
+offers %s a cherry
+imagines %s in a tutu
+catches %s's eye across a crowded room
+kisses %s's hand
+wonders what a nice person like %s is doing in a place like this
+licks chocolate sauce off %s
+gets down on one knee in front of %s
+bats its eyelashes at %s
+winks suggestively at %s
+blabbers incoherently at the sight of %s
+mentally undresses %s
+daydreams about %s
+wonders what %s is really thinking
+talks dirty to %s
+gazes mutely into %s's eyes
+wonders if %s might reciprocate its feelings
+whispers sweet nothings to %s
+sends %s a red rose
+toasts %s with champagne
+offers %s its last Rolo
+invites %s up for coffee
+promises %s a night to remember
+invites %s in to see its etchings
+wonders if %s is really that innocent
+serenades %s
+puts a rose between its teeth and tangoes seductively with %s
+plays with its hair whilst gazing at %s
+draws a magic circle around itself and %s
+tickles %s playfully
+draws a flattering portrait of %s
+poings happily at the sight of %s
+gives %s a high-five
+brushes lint off %s's clothing
+dims the lights for %s
+invites %s behind the bike sheds
+rides a tightrope on a unicycle while juggling chainsaws in an attempt to impress %s
+composes haiku for %s
+makes the tea for %s
+strokes %s's hair
+sprinkles %s with hundreds and thousands.
+flagellates %s with a birch branch
+<3s %s
+admires %s's boots
+invites %s to a candle-lit dinner
+pole dances for %s
+hopes %s packed their toothbrush!
+showers %s in rose petals
+holds a candle for %s
+admires %s's physique
+gives %s a nice relaxing foot massage
+treks through the jungle to deliver Milk Tray to %s
+searches for a rhyme for its poem about %s
+compares %s's eyes to stars
+wonders whether %s comes here often?
+compares %s to a summer's day
+curls up at %s's feet
index ce229f2..cfb5bf8 100644 (file)
--- a/ircbot.py
+++ b/ircbot.py
@@ -205,7 +205,7 @@ class SingleServerIRCBot(SimpleIRCClient):
 \r
         Used when answering a CTCP VERSION request.\r
         """\r
-        return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"\r
+        return "VERSION ircbot.py by Joel Rosdahl <joel@rosdahl.net>, Matthew Vernon <matthew@debian.org> and others"\r
 \r
     def jump_server(self):\r
         """Connect to a new server, possible disconnecting from the current.\r
diff --git a/karmaconvert.py b/karmaconvert.py
new file mode 100755 (executable)
index 0000000..0971a23
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python2
+
+import cPickle,sys
+
+class Karma:
+    def __init__(self):
+        self.dict = {}
+    
+f=open(sys.argv[1],"r")
+karma=cPickle.load(f)
+f.close()
+
+f=open(sys.argv[2],"w")
+cPickle.dump(karma.dict,f)
+f.close()
diff --git a/launch.sh b/launch.sh
new file mode 100755 (executable)
index 0000000..5fd988e
--- /dev/null
+++ b/launch.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+python2.2 acrobat.py Servus-chiark &
+python2.2 acrobat.py socbi &
+python2.2 acrobat.py methsoc &
+python2.2 acrobat.py methsoc-freenode &
+python2.2 acrobat.py assassins &
diff --git a/methsoc-freenode.py b/methsoc-freenode.py
new file mode 100644 (file)
index 0000000..91d170c
--- /dev/null
@@ -0,0 +1,173 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "irc.eu.freenode.net"
+port = 6667
+nickname = "Methsbot"
+channel = "#methsoc"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "karmadump"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+def savekarma():
+    try:
+        f = open(karmafilename, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+    except IOError:
+        sys.stderr.write("Problems dumping karma: probably lost :(")
+
+# When the bot exits we should save the karma db
+def quit(bot,cmd,nick,conn,public):
+    savekarma()
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    savekarma()
+    c.reloadq(bot,cmd,nick,conn,public)
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
diff --git a/methsoc.py b/methsoc.py
new file mode 100644 (file)
index 0000000..e1f340c
--- /dev/null
@@ -0,0 +1,173 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "Meths"
+channel = "#methsoc"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "karmadump"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+def savekarma():
+    try:
+        f = open(karmafilename, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+    except IOError:
+        sys.stderr.write("Problems dumping karma: probably lost :(")
+
+# When the bot exits we should save the karma db
+def quit(bot,cmd,nick,conn,public):
+    savekarma()
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    savekarma()
+    c.reloadq(bot,cmd,nick,conn,public)
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
diff --git a/rjk.diff b/rjk.diff
new file mode 100644 (file)
index 0000000..5bd6bc8
--- /dev/null
+++ b/rjk.diff
@@ -0,0 +1,167 @@
+diff -ruN --exclude {arch} --exclude .arch-ids servus--dev--0--patch-2/ChangeLog.d/servus--dev--0 servus--dev--0--patch-4/ChangeLog.d/servus--dev--0
+--- servus--dev--0--patch-2/ChangeLog.d/servus--dev--0 2005-07-22 23:17:44.000000000 +0100
++++ servus--dev--0--patch-4/ChangeLog.d/servus--dev--0 2005-07-22 23:17:45.000000000 +0100
+@@ -2,6 +2,35 @@
+ # arch-tag: automatic-ChangeLog--rjk@greenend.org.uk--2004/servus--dev--0
+ #
++2005-07-22 22:02:48 GMT       Richard Kettlewell <rjk@greenend.org.uk>        patch-4
++
++    Summary:
++      bloody slashers
++    Revision:
++      servus--dev--0--patch-4
++
++    * commands.py: slashq command implements slashing of a pair of names
++    * Servus-chiark.py: configuration and setup for slash command
++
++    modified files:
++     ChangeLog.d/servus--dev--0 Servus-chiark.py commands.py
++
++
++2005-07-22 21:36:36 GMT       Richard Kettlewell <rjk@greenend.org.uk>        patch-3
++
++    Summary:
++      fix karma storage
++    Revision:
++      servus--dev--0--patch-3
++
++    * Servus-chiark.py: save karma file each time it is changed, not just on
++      graceful termination; and rename it into place.
++      This prevents unnecessary forgetting of karma.
++
++    modified files:
++     ChangeLog.d/servus--dev--0 Servus-chiark.py
++
++
+ 2005-07-22 21:29:40 GMT       Richard Kettlewell <rjk@greenend.org.uk>        patch-2
+     Summary:
+diff -ruN --exclude {arch} --exclude .arch-ids servus--dev--0--patch-2/Servus-chiark.py servus--dev--0--patch-4/Servus-chiark.py
+--- servus--dev--0--patch-2/Servus-chiark.py   2005-07-22 23:17:43.000000000 +0100
++++ servus--dev--0--patch-4/Servus-chiark.py   2005-07-22 23:17:45.000000000 +0100
+@@ -19,6 +19,7 @@
+ # Peter Corbett <ptc24@cam.ac.uk>
+ # Matthew Vernon <matthew@debian.org>
+ # Stephen Early <steve@greenend.org.uk>
++# Richard Kettlewell <rjk@greenend.org.uk
+ # Acrobat configuration file
+@@ -32,6 +33,8 @@
+ # Everything else in this file is configuration-specific.
++import os
++
+ # Most command implementations are stored in a separate module.
+ import commands as c
+@@ -76,6 +79,15 @@
+       fish,
+       0.1)
++slashcfg= ( 
++      __load("slashes"),
++      ' (while %s watches)',
++      "Sorry, but %s stole my pen.",
++      "Who do you want to slash?",
++      "I have writer's block!",
++      fish,
++      0.1)
++
+ # Hacky command to output the current fishpond state
+ def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+@@ -104,20 +116,20 @@
+         karmadb[thing] += amount
+     else:
+         karmadb[thing] = amount
++    savekarma()
+ def savekarma():
++    tmp = "%s.tmp" % karmafilename
+     try:
+-        f = open(karmafilename, "w")
++        f = open(tmp, "w")
+         cPickle.dump(karmadb, f)
+         f.close()
+-    except IOError:
+-        sys.stderr.write("Problems dumping karma: probably lost :(")
++      os.rename(tmp, karmafilename)
++    except IOError, e:
++        sys.stderr.write("error writing karma: %s" % e)
+-# When the bot exits we should save the karma db
+ def quit(bot,cmd,nick,conn,public):
+-    savekarma()
+     c.quitq(bot,cmd,nick,conn,public)
+ def reload(bot,cmd,nick,conn,public):
+-    savekarma()
+     c.reloadq(bot,cmd,nick,conn,public)
+ # Command processing: whenever something is said that the bot can hear,
+@@ -130,6 +142,7 @@
+           "karmadel": (c.karmadelq,karmadb),
+             "info": (c.infoq,karmadb),
+             "trout": (c.troutq,troutcfg),
++          "slash": (c.slashq, slashcfg),
+           "fish": (fishq,fish),
+             "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+diff -ruN --exclude {arch} --exclude .arch-ids servus--dev--0--patch-2/commands.py servus--dev--0--patch-4/commands.py
+--- servus--dev--0--patch-2/commands.py        2005-07-22 23:17:43.000000000 +0100
++++ servus--dev--0--patch-4/commands.py        2005-07-22 23:17:45.000000000 +0100
+@@ -90,6 +90,53 @@
+     conn.action(bot.channel, trout_msg % target)
+     fishpond.cur_fish-=1
++# slash a pair
++def slashq(bot, cmd, nick, conn, public, cfg):
++    fishlist=cfg[0]
++    selfslash=cfg[1]
++    quietmsg=cfg[2]
++    notargetmsg=cfg[3]
++    nofishmsg=cfg[4]
++    fishpond=cfg[5]
++
++    fish_quota(fishpond)
++    if fishpond.DoS:
++        conn.notice(nick, quietmsg%fishpond.Boring_Git)
++      return
++    if fishpond.cur_fish<=0:
++        conn.notice(nick, nofishmsg)
++      return
++    target = string.join(cmd.split()[1:])
++    who = cmd.split()[1:]
++    if len(who) < 2:
++      conn.notice(nick, "it takes two to tango!")
++      return
++    elif len(who) > 2:
++      conn.notice(nick, "we'll have none of that round here")
++      return
++    me = bot.connection.get_nickname()
++    slash_msg = random.choice(fishlist)
++    # The bot won't slash people with themselves
++    if irc_lower(who[0]) == irc_lower(who[1]):
++      conn.notice(nick, "oooooh no missus!")
++      return
++    # The bot won't slash with itself, instead slashing the requester
++    for n in [0,1]:
++      if irc_lower(me) == irc_lower(who[n]):
++          who[n] = nick
++    # Perhaps someone asked to slash themselves with the bot then we get
++    if irc_lower(who[0]) == irc_lower(who[1]):
++      conn.notice(nick, "you wish!")
++      return
++    # There's a chance the game may be given away if the request was not
++    # public...
++    if not public:
++      if random.random()<=selfslashchance:
++          slash_msg=slash_msg+(selfslash%nick)
++
++    bot.automsg(public,nick, slash_msg % (who[0], who[1]))
++    fishpond.cur_fish-=1
++
+ # Shut up trouting for a minute
+ def nofishq(bot, cmd, nick, conn, public, fish):
+     fish.cur_fish=0
diff --git a/slashes.~1.7.~ b/slashes.~1.7.~
new file mode 100644 (file)
index 0000000..93125e6
--- /dev/null
@@ -0,0 +1,22 @@
+wonders what %s and %s are doing together in a locked room
+wishes %s and %s would stop doing that in public
+draws chibi fan art of %s and %s
+embarks on the Good Ship %s-%s
+watches %s and %s making the beast with two backs
+thinks %s/%s is better than Legolas/Aragorn
+is jealous because %s loves %s instead of itself
+daydreams about %s and %s's wedding
+writes plotless smut about %s's steamy secret liaison with %s
+starts a Yahoo group devoted to Photoshops of %s and %s naked
+gets horny just thinking about %s and %s together
+thinks %s and %s should grow old together surrounded by their children
+wonders who would be the godparents when %s and %s had children
+wonders who would get pregnant, %s or %s
+makes LJ banners proclaiming '%s and %s Is Love!'
+makes animated LJ icons of %s and %s making out
+wonders how %s and %s got together
+wonders what %s and %s's friends would think of their relationship
+writes dirty lyrics about %s and %s
+sings, "%s and %s, sitting in a tree, k-i-s-s-i-n-g"
+sets up %s and %s on a blind date
+thinks %s and %s should get a room!
diff --git a/slashes~ b/slashes~
new file mode 100644 (file)
index 0000000..5b3d0e8
--- /dev/null
+++ b/slashes~
@@ -0,0 +1,2 @@
+wonders what %s and %s are doing together in a locked room
+wishes %s and %s would stop doing that in public
diff --git a/socbi.py b/socbi.py
new file mode 100644 (file)
index 0000000..da7d343
--- /dev/null
+++ b/socbi.py
@@ -0,0 +1,173 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "irc.eu.freenode.net"
+port = 6667
+nickname = "HBB"
+channel = "#soc.bi"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "karmadump"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+def savekarma():
+    try:
+        f = open(karmafilename, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+    except IOError:
+        sys.stderr.write("Problems dumping karma: probably lost :(")
+
+# When the bot exits we should save the karma db
+def quit(bot,cmd,nick,conn,public):
+    savekarma()
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    savekarma()
+    c.reloadq(bot,cmd,nick,conn,public)
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
diff --git a/test-chiark.py b/test-chiark.py
new file mode 100644 (file)
index 0000000..f985079
--- /dev/null
@@ -0,0 +1,207 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+# Richard Kettlewell <rjk@greenend.org.uk
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "testbot"
+channel = "#test"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+import os, time
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+slashcfg= ( 
+       __load("slashes"),
+       ' (while %s watches)',
+       "Sorry, but %s stole my pen.",
+       "Who do you want to slash?",
+       "I have writer's block!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "chiark-test-karma"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+    savekarma()
+def savekarma():
+    tmp = "%s.tmp" % karmafilename
+    try:
+        f = open(tmp, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+       os.rename(tmp, karmafilename)
+    except IOError, e:
+        sys.stderr.write("error writing karma: %s" % e)
+
+def quit(bot,cmd,nick,conn,public):
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    c.reloadq(bot,cmd,nick,conn,public)
+
+#The game...
+class game:
+       trigger="Servus"
+       grace=time.time()
+       minlose=24*60*60 #1 day
+       maxlose=14*minlose #2 weeks
+       losetime=time.time()
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "help": (c.helpq),
+            "trout": (c.troutq,troutcfg),
+           "slash": (c.slashq, slashcfg),
+           "rot13": c.rot13q,
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+           "units": c.unitq,
+#          "define": c.defineq,
+#          "game": (c.gameq,game),
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command.lower()]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
+    elif public:
+        if cmd.find("GAME")!=-1: #someone else lost
+           grace.grace=time.time()+60*20
+       elif cmd.find(game.trigger)!=-1: #we lost!
+           c.gameq(bot,"pad "+game.trigger,bot.owner,conn,False,game)
+       elif time.time()>game.losetime: #we randomly lost, take new trigger
+           c.gameq(bot,cmd,bot.owner,conn,False,game)
+           
diff --git a/test-chiark.py~ b/test-chiark.py~
new file mode 100644 (file)
index 0000000..9c6d2ef
--- /dev/null
@@ -0,0 +1,206 @@
+# This file is part of Acrobat.
+#
+# Acrobat 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 2 of the License,
+# or (at your option) any later version.
+#
+# Acrobat 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 Acrobat; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA.
+
+# Andrew Walkingshaw <andrew@lexical.org.uk>
+# Peter Corbett <ptc24@cam.ac.uk>
+# Matthew Vernon <matthew@debian.org>
+# Stephen Early <steve@greenend.org.uk>
+# Richard Kettlewell <rjk@greenend.org.uk
+
+# Acrobat configuration file
+
+# The following definitions are required to be present in this module:
+server = "rapun"
+port = 6667
+nickname = "testbot"
+channel = "#test"
+owner = "Emperor"
+# Also a function called "command"; see later.
+
+# Everything else in this file is configuration-specific.
+
+import os, time
+
+# Most command implementations are stored in a separate module.
+import commands as c
+
+# This fishpond is shared between trouts and flirts.  It doesn't have to be;
+# you can define as many ponds as you like.
+class fish:
+       cur_fish=5
+       max_fish=5
+       nofish_time=60
+       fish_time_inc=60
+       fish_inc=2
+       DoS=0
+       Boring_Git='Nobody'
+       quotatime=0
+
+# load a file full of flirts or trouts
+def __load(filename):
+    try:
+       f = open(filename, "r")
+        r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
+        f.close()
+    except IOError:
+        r = [ "doesn't know what to do about %s." ]
+    return r
+
+# (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
+troutcfg = (
+       __load("trouts"),
+       ' (at the instigation of %s)',
+       "Sorry, but %s is being a spoilsport.",
+       "Who do you wish me to trout?",
+       "Fish stocks exhausted.",
+       fish,
+       0.1)
+
+flirtcfg = (
+       __load("flirts"),
+       ' (but %s is their secret admirer)',
+       "Sorry, but %s made me take Holy Orders.",
+       "Who do you wish me to flirt with?",
+       "My libido is over-used!",
+       fish,
+       0.1)
+
+slashcfg= ( 
+       __load("slashes"),
+       ' (while %s watches)',
+       "Sorry, but %s stole my pen.",
+       "Who do you want to slash?",
+       "I have writer's block!",
+       fish,
+       0.1)
+
+# Hacky command to output the current fishpond state
+def fishq(bot, cmd, nick, conn, public,f):
+       from irclib import irc_lower
+       if not public and irc_lower(nick) == irc_lower(bot.owner):
+               state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
+                      +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
+                      +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
+                                        f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
+                                        f.quotatime)
+               bot.automsg(public,nick,state)
+                    
+# Karma implementation
+import cPickle
+karmafilename = "chiark-test-karma"
+# load the karma db
+try:
+    f = open(karmafilename, "r")
+    karmadb = cPickle.load(f)
+    f.close()
+except IOError:
+    karmadb = {}
+# Modify karma
+def karma(cmd, amount):
+    thing=cmd.split()[0][:-2].lower()
+    if karmadb.has_key(thing):
+        karmadb[thing] += amount
+    else:
+        karmadb[thing] = amount
+    savekarma()
+def savekarma():
+    tmp = "%s.tmp" % karmafilename
+    try:
+        f = open(tmp, "w")
+        cPickle.dump(karmadb, f)
+        f.close()
+       os.rename(tmp, karmafilename)
+    except IOError, e:
+        sys.stderr.write("error writing karma: %s" % e)
+
+def quit(bot,cmd,nick,conn,public):
+    c.quitq(bot,cmd,nick,conn,public)
+def reload(bot,cmd,nick,conn,public):
+    c.reloadq(bot,cmd,nick,conn,public)
+
+#The game...
+class game:
+       trigger="Servus"
+       grace=time.time()
+       minlose=24*60*60 #1 day
+       maxlose=14*minlose #2 weeks
+       losetime=time.time()
+
+# Command processing: whenever something is said that the bot can hear,
+# "command" is invoked and must decide what to do.  This configuration
+# defines a couple of special cases (for karma) but is otherwise driven
+# by a dictionary of commands.
+
+commands = {"karma": (c.karmaq,karmadb),
+           "karmalist": (c.listkeysq,karmadb),
+           "karmadel": (c.karmadelq,karmadb),
+            "info": (c.infoq,karmadb),
+            "trout": (c.troutq,troutcfg),
+           "slash": (c.slashq, slashcfg),
+           "rot13": c.rot13q,
+           "fish": (fishq,fish),
+            "flirt": (c.troutq,flirtcfg),
+           "quiet": (c.nofishq,fish),
+            "reload": reload,
+            "quit": quit,
+           "die": quit,
+            "google": c.googleq,
+           "units": c.unitq,
+#          "define": c.defineq,
+#          "game": (c.gameq,game),
+            "say": c.sayq,
+            "do": c.doq }
+# disconnect and hop annoy people
+#            "disconnect": c.disconnq,
+#            "hop": c.disconnq }
+commands["list"]=(c.listkeysq,commands)
+
+triggers = ("!", "~") # what character should the bot be invoked by:
+                      # eg !trout, ~trout etc.
+
+def command(bot, cmd, nick, conn, public):
+    ours=0
+    try:
+           if public and cmd[0] in triggers:
+                   ours=1
+                   cmd=cmd[1:]
+           if not public:
+                   ours=1
+           command = cmd.split()[0]
+    except IndexError:
+           command=""
+    # karma: up
+    if command.endswith("++"):
+        karma(cmd,1)
+    # karma: down
+    if command.endswith("--"):
+        karma(cmd,-1)
+
+    if ours and command.lower() in commands.keys():
+       e=commands[command.lower()]
+       if callable(e):
+           e(bot,cmd,nick,conn,public)
+       else:
+           e[0](bot,cmd,nick,conn,public,*e[1:])
+    elif public:
+        if cmd.find("GAME")!=-1: #someone else lost
+           grace.grace=time.time()+60*20
+       elif cmd.find(game.trigger)!=-1: #we lost!
+           c.gameq(bot,"pad "+game.trigger,bot.owner,conn,False,game)
+       elif time.time()>game.losetime: #we randomly lost, take new trigger
+           c.gameq(bot,cmd,bot.owner,conn,False,game)
+           
diff --git a/trouts.~1.49.~ b/trouts.~1.49.~
new file mode 100644 (file)
index 0000000..1d1c614
--- /dev/null
@@ -0,0 +1,159 @@
+hits %s with a wet trout.
+thwaps %s with a five-mile-long spacestation.
+sacrifices %s to dark gods.
+bites his thumb at %s.
+replaces %s with a 2-line shell script
+takes off and nukes %s from orbit
+treats %s with the contempt they deserve
+grumbles sonorously at %s
+offers %s a one way ticket to Mars
+decapitates %s
+slaps %s with a wet haddock
+plays the xylophone on %s's kneecaps
+lands a minke whale on %s
+inverts %s
+sends %s to Cuba
+searches frantically for %s's brain
+introduces %s to their antimatter twin
+opens a trapdoor under %s
+lands a 10 ton weight on %s
+pokes %s with a spoon
+hits %s with a squeaky hammer
+wonders if %s has a return-to-base warranty
+mutters darkly about %s
+thwaps %s unimaginatively
+staples %s to Gordon Brown's nose
+growls at %s
+catapults %s over the Atlantic
+replaces %s with a papier mache replica
+attaches an extra three noses to %s
+eviscerates %s with a trowel
+annihilates %s
+wonders what God was thinking of when He made %s
+removes %s from the gene pool
+performs a lube-less rectal on %s
+uses %s to practice his orthopedic surgery on
+thinks %s would make excellent road-kill
+plays basketball with %s's head
+drops %s down a deep well
+covers %s in blancmange
+napalms %s
+finds %s particularly irksome
+feeds %s to his pet dog
+reminds %s that life is not a game of Nethack
+thinks that %s would give a cannibal nausea
+wonders if %s is ever going to become a swan
+drops an anvil on %s from a great height
+disparages %s's mother
+blows raspberries, blackberries and oranges at %s
+would eat %s's brains but can't find them
+thinks %s could do with less coffee
+stuffs %s into a small box and nails down the lid
+bites %s's ankles
+pulls a moonie at %s
+throws %s a bone
+invites %s to go jump
+throws jelly at %s
+throws %s from the top of the empire state building
+drops a small hydrogen bomb on %s
+curses %s unto the seventh generation
+asks if %s is a man or a mouse
+questions the lineage of %s
+accuses %s of being a goth
+creeps up on %s wielding a crush emasculator
+watches %s splashing around in the shallow end of the gene pool
+swashbuckles the +2 Blessed Trout of Doom in %s's face
+runs over %s with a tank
+throws a rotten head of cabbage at %s
+flagellates %s with a birch branch
+sends %s to Coventry
+recoils from %s as if they have the plague
+kicks %s into touch
+teleports %s to the Dimension of Pain
+pours sour milk into %s's coffee
+drops a ferret into %s's clothing
+throws %s into the Cam
+tries to steal %s's dinner money
+attaches a debugger to %s
+introduces %s to the business end of half a wasp
+drives a manure spreader past %s
+goes flying with %s tied to a wing
+throws an explosive banana at %s
+subscribes %s to 1001 junk mail lists
+points at %s and laughs
+dices %s and feeds them to Ross Anderson
+follows %s around in a black helicopter
+dissects %s in public
+threatens %s with a rectal thermometer
+thwaps %s with IWJ's +10 rod of deprecation
+buys %s a fake RAF moustache
+earnestly discusses railway routing regulations with %s
+folds %s into an elaborate origami representation of a mackerel
+reports %s to the RIAA
+hands %s a live grenade
+throws a cat at %s
+kidnaps %s and fills their shoes with custard
+plays Britney Spears songs at %s
+fills %s's ears with butter
+inverts %s
+throws %s to the lions
+sacrifices %s for the greater good
+patents %s's genome.
+covers %s in attack hamsters.
+brandishes a spoon at %s.
+searches %s for weapons of mass destruction.
+smears %s in blood and waits for the sharks.
+cackles malevolently at %s.
+clones %s and eats the original.
+teaches %s to think inside a very small box.
+looms at %s.
+calls down an airstrike upon %s.
+feeds %s to the all-powerful Sarlacc.
+affixes live crocodile clips to %s.
+causes %s to have acute razor burn.
+places the fleas of a thousand camels in %s's armpit.
+sticks %s's head on a spike.
+brings %s's haddock to paddock
+breaks %s up for spare parts
+tags %s as 'wontfix'
+tags %s as 'obsolete'
+boils %s up for glue
+nails %s into a barrel and pushes them over Niagara Falls
+accuses %s of losing The Game
+douses %s in liquid nitrogen
+turns %s inside-out
+paints %s red and waits for the bulls
+signs %s up for a Scientology course
+shrieks hysterically at %s
+declares %s an unlawful combatant
+vetoes %s
+rubs bubblegum in %s's hair
+steals all %s's coffee
+heckles %s
+inflates %s and brandishes a pin
+thinks %s is worse than Microsoft Windows
+wonders why %s bothered getting out of bed today
+trolls %s
+runs %s through
+plays the harmonica at %s
+bricks %s up with a cask of Amontillado
+lands a TARDIS on %s
+practises acupuncture on %s
+rugby-tackles %s to the ground
+passes %s the underpants of shame
+removes %s's willy with a chainsaw
+throws a custard pie at %s
+violates %s's mind (and body) with the Necronomicon
+applies TCP_CORK to %s
+performs a capsaicin enema upon %s.
+rejects %s in favour of the null hypothesis.
+makes %s illegal.
+compiles %s with -trigraphs
+compiles %s with -Wtrigraphs
+turns %s into dogfood
+><> ><> %s
+sets %s and robhu up on a blind date
+gently taps %s with a menhir.
+invests all of %s's money in a high-grade structured credit strategy enhanced leverage fund.
+plays the viola at %s.
+thinks %s is comparable to iprocurement at its best
diff --git a/urllib.py b/urllib.py
new file mode 100644 (file)
index 0000000..cd4876d
--- /dev/null
+++ b/urllib.py
@@ -0,0 +1,1461 @@
+"""Open an arbitrary URL.
+
+See the following document for more info on URLs:
+"Names and Addresses, URIs, URLs, URNs, URCs", at
+http://www.w3.org/pub/WWW/Addressing/Overview.html
+
+See also the HTTP spec (from which the error codes are derived):
+"HTTP - Hypertext Transfer Protocol", at
+http://www.w3.org/pub/WWW/Protocols/
+
+Related standards and specs:
+- RFC1808: the "relative URL" spec. (authoritative status)
+- RFC1738 - the "URL standard". (authoritative status)
+- RFC1630 - the "URI spec". (informational status)
+
+The object returned by URLopener().open(file) will differ per
+protocol.  All you know is that is has methods read(), readline(),
+readlines(), fileno(), close() and info().  The read*(), fileno()
+and close() methods work like those of open files.
+The info() method returns a mimetools.Message object which can be
+used to query various info about the object, if available.
+(mimetools.Message objects are queried with the getheader() method.)
+"""
+
+import string
+import socket
+import os
+import stat
+import time
+import sys
+import types
+
+__all__ = ["urlopen", "URLopener", "FancyURLopener", "urlretrieve",
+           "urlcleanup", "quote", "quote_plus", "unquote", "unquote_plus",
+           "urlencode", "url2pathname", "pathname2url", "splittag",
+           "localhost", "thishost", "ftperrors", "basejoin", "unwrap",
+           "splittype", "splithost", "splituser", "splitpasswd", "splitport",
+           "splitnport", "splitquery", "splitattr", "splitvalue",
+           "splitgophertype", "getproxies"]
+
+__version__ = '1.15'    # XXX This version is not always updated :-(
+
+MAXFTPCACHE = 10        # Trim the ftp cache beyond this size
+
+# Helper for non-unix systems
+if os.name == 'mac':
+    from macurl2path import url2pathname, pathname2url
+elif os.name == 'nt':
+    from nturl2path import url2pathname, pathname2url
+elif os.name == 'riscos':
+    from rourl2path import url2pathname, pathname2url
+else:
+    def url2pathname(pathname):
+        return unquote(pathname)
+    def pathname2url(pathname):
+        return quote(pathname)
+
+# This really consists of two pieces:
+# (1) a class which handles opening of all sorts of URLs
+#     (plus assorted utilities etc.)
+# (2) a set of functions for parsing URLs
+# XXX Should these be separated out into different modules?
+
+
+# Shortcut for basic usage
+_urlopener = None
+def urlopen(url, data=None):
+    """urlopen(url [, data]) -> open file-like object"""
+    global _urlopener
+    if not _urlopener:
+        _urlopener = FancyURLopener()
+    if data is None:
+        return _urlopener.open(url)
+    else:
+        return _urlopener.open(url, data)
+def urlretrieve(url, filename=None, reporthook=None, data=None):
+    global _urlopener
+    if not _urlopener:
+        _urlopener = FancyURLopener()
+    return _urlopener.retrieve(url, filename, reporthook, data)
+def urlcleanup():
+    if _urlopener:
+        _urlopener.cleanup()
+
+
+ftpcache = {}
+class URLopener:
+    """Class to open URLs.
+    This is a class rather than just a subroutine because we may need
+    more than one set of global protocol-specific options.
+    Note -- this is a base class for those who don't want the
+    automatic handling of errors type 302 (relocated) and 401
+    (authorization needed)."""
+
+    __tempfiles = None
+
+    version = "Python-urllib/%s" % __version__
+
+    # Constructor
+    def __init__(self, proxies=None, **x509):
+        if proxies is None:
+            proxies = getproxies()
+        assert hasattr(proxies, 'has_key'), "proxies must be a mapping"
+        self.proxies = proxies
+        self.key_file = x509.get('key_file')
+        self.cert_file = x509.get('cert_file')
+        self.addheaders = [('User-agent', 'Servus/0.2')]
+        self.__tempfiles = []
+        self.__unlink = os.unlink # See cleanup()
+        self.tempcache = None
+        # Undocumented feature: if you assign {} to tempcache,
+        # it is used to cache files retrieved with
+        # self.retrieve().  This is not enabled by default
+        # since it does not work for changing documents (and I
+        # haven't got the logic to check expiration headers
+        # yet).
+        self.ftpcache = ftpcache
+        # Undocumented feature: you can use a different
+        # ftp cache by assigning to the .ftpcache member;
+        # in case you want logically independent URL openers
+        # XXX This is not threadsafe.  Bah.
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        self.cleanup()
+
+    def cleanup(self):
+        # This code sometimes runs when the rest of this module
+        # has already been deleted, so it can't use any globals
+        # or import anything.
+        if self.__tempfiles:
+            for file in self.__tempfiles:
+                try:
+                    self.__unlink(file)
+                except OSError:
+                    pass
+            del self.__tempfiles[:]
+        if self.tempcache:
+            self.tempcache.clear()
+
+    def addheader(self, *args):
+        """Add a header to be used by the HTTP interface only
+        e.g. u.addheader('Accept', 'sound/basic')"""
+        self.addheaders.append(args)
+
+    # External interface
+    def open(self, fullurl, data=None):
+        """Use URLopener().open(file) instead of open(file, 'r')."""
+        fullurl = unwrap(toBytes(fullurl))
+        if self.tempcache and self.tempcache.has_key(fullurl):
+            filename, headers = self.tempcache[fullurl]
+            fp = open(filename, 'rb')
+            return addinfourl(fp, headers, fullurl)
+        urltype, url = splittype(fullurl)
+        if not urltype:
+            urltype = 'file'
+        if self.proxies.has_key(urltype):
+            proxy = self.proxies[urltype]
+            urltype, proxyhost = splittype(proxy)
+            host, selector = splithost(proxyhost)
+            url = (host, fullurl) # Signal special case to open_*()
+        else:
+            proxy = None
+        name = 'open_' + urltype
+        self.type = urltype
+        if '-' in name:
+            # replace - with _
+            name = '_'.join(name.split('-'))
+        if not hasattr(self, name):
+            if proxy:
+                return self.open_unknown_proxy(proxy, fullurl, data)
+            else:
+                return self.open_unknown(fullurl, data)
+        try:
+            if data is None:
+                return getattr(self, name)(url)
+            else:
+                return getattr(self, name)(url, data)
+        except socket.error, msg:
+            raise IOError, ('socket error', msg), sys.exc_info()[2]
+
+    def open_unknown(self, fullurl, data=None):
+        """Overridable interface to open unknown URL type."""
+        type, url = splittype(fullurl)
+        raise IOError, ('url error', 'unknown url type', type)
+
+    def open_unknown_proxy(self, proxy, fullurl, data=None):
+        """Overridable interface to open unknown URL type."""
+        type, url = splittype(fullurl)
+        raise IOError, ('url error', 'invalid proxy for %s' % type, proxy)
+
+    # External interface
+    def retrieve(self, url, filename=None, reporthook=None, data=None):
+        """retrieve(url) returns (filename, None) for a local object
+        or (tempfilename, headers) for a remote object."""
+        url = unwrap(toBytes(url))
+        if self.tempcache and self.tempcache.has_key(url):
+            return self.tempcache[url]
+        type, url1 = splittype(url)
+        if not filename and (not type or type == 'file'):
+            try:
+                fp = self.open_local_file(url1)
+                hdrs = fp.info()
+                del fp
+                return url2pathname(splithost(url1)[1]), hdrs
+            except IOError, msg:
+                pass
+        fp = self.open(url, data)
+        headers = fp.info()
+        if not filename:
+            import tempfile
+            garbage, path = splittype(url)
+            garbage, path = splithost(path or "")
+            path, garbage = splitquery(path or "")
+            path, garbage = splitattr(path or "")
+            suffix = os.path.splitext(path)[1]
+            filename = tempfile.mktemp(suffix)
+            self.__tempfiles.append(filename)
+        result = filename, headers
+        if self.tempcache is not None:
+            self.tempcache[url] = result
+        tfp = open(filename, 'wb')
+        bs = 1024*8
+        size = -1
+        blocknum = 1
+        if reporthook:
+            if headers.has_key("content-length"):
+                size = int(headers["Content-Length"])
+            reporthook(0, bs, size)
+        block = fp.read(bs)
+        if reporthook:
+            reporthook(1, bs, size)
+        while block:
+            tfp.write(block)
+            block = fp.read(bs)
+            blocknum = blocknum + 1
+            if reporthook:
+                reporthook(blocknum, bs, size)
+        fp.close()
+        tfp.close()
+        del fp
+        del tfp
+        return result
+
+    # Each method named open_<type> knows how to open that type of URL
+
+    def open_http(self, url, data=None):
+        """Use HTTP protocol."""
+        import httplib
+        user_passwd = None
+        if type(url) is types.StringType:
+            host, selector = splithost(url)
+            if host:
+                user_passwd, host = splituser(host)
+                host = unquote(host)
+            realhost = host
+        else:
+            host, selector = url
+            urltype, rest = splittype(selector)
+            url = rest
+            user_passwd = None
+            if urltype.lower() != 'http':
+                realhost = None
+            else:
+                realhost, rest = splithost(rest)
+                if realhost:
+                    user_passwd, realhost = splituser(realhost)
+                if user_passwd:
+                    selector = "%s://%s%s" % (urltype, realhost, rest)
+                if proxy_bypass(realhost):
+                    host = realhost
+
+            #print "proxy via http:", host, selector
+        if not host: raise IOError, ('http error', 'no host given')
+        if user_passwd:
+            import base64
+            auth = base64.encodestring(user_passwd).strip()
+        else:
+            auth = None
+        h = httplib.HTTP(host)
+        if data is not None:
+            h.putrequest('POST', selector)
+            h.putheader('Content-type', 'application/x-www-form-urlencoded')
+            h.putheader('Content-length', '%d' % len(data))
+        else:
+            h.putrequest('GET', selector)
+        if auth: h.putheader('Authorization', 'Basic %s' % auth)
+        if realhost: h.putheader('Host', realhost)
+        for args in self.addheaders: apply(h.putheader, args)
+        h.endheaders()
+        if data is not None:
+            h.send(data)
+        errcode, errmsg, headers = h.getreply()
+        fp = h.getfile()
+        if errcode == 200:
+            return addinfourl(fp, headers, "http:" + url)
+        else:
+            if data is None:
+                return self.http_error(url, fp, errcode, errmsg, headers)
+            else:
+                return self.http_error(url, fp, errcode, errmsg, headers, data)
+
+    def http_error(self, url, fp, errcode, errmsg, headers, data=None):
+        """Handle http errors.
+        Derived class can override this, or provide specific handlers
+        named http_error_DDD where DDD is the 3-digit error code."""
+        # First check if there's a specific handler for this error
+        name = 'http_error_%d' % errcode
+        if hasattr(self, name):
+            method = getattr(self, name)
+            if data is None:
+                result = method(url, fp, errcode, errmsg, headers)
+            else:
+                result = method(url, fp, errcode, errmsg, headers, data)
+            if result: return result
+        return self.http_error_default(url, fp, errcode, errmsg, headers)
+
+    def http_error_default(self, url, fp, errcode, errmsg, headers):
+        """Default error handler: close the connection and raise IOError."""
+        void = fp.read()
+        fp.close()
+        raise IOError, ('http error', errcode, errmsg, headers)
+
+    if hasattr(socket, "ssl"):
+        def open_https(self, url, data=None):
+            """Use HTTPS protocol."""
+            import httplib
+            user_passwd = None
+            if type(url) is types.StringType:
+                host, selector = splithost(url)
+                if host:
+                    user_passwd, host = splituser(host)
+                    host = unquote(host)
+                realhost = host
+            else:
+                host, selector = url
+                urltype, rest = splittype(selector)
+                url = rest
+                user_passwd = None
+                if urltype.lower() != 'https':
+                    realhost = None
+                else:
+                    realhost, rest = splithost(rest)
+                    if realhost:
+                        user_passwd, realhost = splituser(realhost)
+                    if user_passwd:
+                        selector = "%s://%s%s" % (urltype, realhost, rest)
+                #print "proxy via https:", host, selector
+            if not host: raise IOError, ('https error', 'no host given')
+            if user_passwd:
+                import base64
+                auth = base64.encodestring(user_passwd).strip()
+            else:
+                auth = None
+            h = httplib.HTTPS(host, 0,
+                              key_file=self.key_file,
+                              cert_file=self.cert_file)
+            if data is not None:
+                h.putrequest('POST', selector)
+                h.putheader('Content-type',
+                            'application/x-www-form-urlencoded')
+                h.putheader('Content-length', '%d' % len(data))
+            else:
+                h.putrequest('GET', selector)
+            if auth: h.putheader('Authorization: Basic %s' % auth)
+            if realhost: h.putheader('Host', realhost)
+            for args in self.addheaders: apply(h.putheader, args)
+            h.endheaders()
+            if data is not None:
+                h.send(data)
+            errcode, errmsg, headers = h.getreply()
+            fp = h.getfile()
+            if errcode == 200:
+                return addinfourl(fp, headers, "https:" + url)
+            else:
+                if data is None:
+                    return self.http_error(url, fp, errcode, errmsg, headers)
+                else:
+                    return self.http_error(url, fp, errcode, errmsg, headers,
+                                           data)
+
+    def open_gopher(self, url):
+        """Use Gopher protocol."""
+        import gopherlib
+        host, selector = splithost(url)
+        if not host: raise IOError, ('gopher error', 'no host given')
+        host = unquote(host)
+        type, selector = splitgophertype(selector)
+        selector, query = splitquery(selector)
+        selector = unquote(selector)
+        if query:
+            query = unquote(query)
+            fp = gopherlib.send_query(selector, query, host)
+        else:
+            fp = gopherlib.send_selector(selector, host)
+        return addinfourl(fp, noheaders(), "gopher:" + url)
+
+    def open_file(self, url):
+        """Use local file or FTP depending on form of URL."""
+        if url[:2] == '//' and url[2:3] != '/':
+            return self.open_ftp(url)
+        else:
+            return self.open_local_file(url)
+
+    def open_local_file(self, url):
+        """Use local file."""
+        import mimetypes, mimetools, rfc822, StringIO
+        host, file = splithost(url)
+        localname = url2pathname(file)
+        try:
+            stats = os.stat(localname)
+        except OSError, e:
+            raise IOError(e.errno, e.strerror, e.filename)
+        size = stats[stat.ST_SIZE]
+        modified = rfc822.formatdate(stats[stat.ST_MTIME])
+        mtype = mimetypes.guess_type(url)[0]
+        headers = mimetools.Message(StringIO.StringIO(
+            'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' %
+            (mtype or 'text/plain', size, modified)))
+        if not host:
+            urlfile = file
+            if file[:1] == '/':
+                urlfile = 'file://' + file
+            return addinfourl(open(localname, 'rb'),
+                              headers, urlfile)
+        host, port = splitport(host)
+        if not port \
+           and socket.gethostbyname(host) in (localhost(), thishost()):
+            urlfile = file
+            if file[:1] == '/':
+                urlfile = 'file://' + file
+            return addinfourl(open(localname, 'rb'),
+                              headers, urlfile)
+        raise IOError, ('local file error', 'not on local host')
+
+    def open_ftp(self, url):
+        """Use FTP protocol."""
+        import mimetypes, mimetools, StringIO
+        host, path = splithost(url)
+        if not host: raise IOError, ('ftp error', 'no host given')
+        host, port = splitport(host)
+        user, host = splituser(host)
+        if user: user, passwd = splitpasswd(user)
+        else: passwd = None
+        host = unquote(host)
+        user = unquote(user or '')
+        passwd = unquote(passwd or '')
+        host = socket.gethostbyname(host)
+        if not port:
+            import ftplib
+            port = ftplib.FTP_PORT
+        else:
+            port = int(port)
+        path, attrs = splitattr(path)
+        path = unquote(path)
+        dirs = path.split('/')
+        dirs, file = dirs[:-1], dirs[-1]
+        if dirs and not dirs[0]: dirs = dirs[1:]
+        if dirs and not dirs[0]: dirs[0] = '/'
+        key = user, host, port, '/'.join(dirs)
+        # XXX thread unsafe!
+        if len(self.ftpcache) > MAXFTPCACHE:
+            # Prune the cache, rather arbitrarily
+            for k in self.ftpcache.keys():
+                if k != key:
+                    v = self.ftpcache[k]
+                    del self.ftpcache[k]
+                    v.close()
+        try:
+            if not self.ftpcache.has_key(key):
+                self.ftpcache[key] = \
+                    ftpwrapper(user, passwd, host, port, dirs)
+            if not file: type = 'D'
+            else: type = 'I'
+            for attr in attrs:
+                attr, value = splitvalue(attr)
+                if attr.lower() == 'type' and \
+                   value in ('a', 'A', 'i', 'I', 'd', 'D'):
+                    type = value.upper()
+            (fp, retrlen) = self.ftpcache[key].retrfile(file, type)
+            mtype = mimetypes.guess_type("ftp:" + url)[0]
+            headers = ""
+            if mtype:
+                headers += "Content-Type: %s\n" % mtype
+            if retrlen is not None and retrlen >= 0:
+                headers += "Content-Length: %d\n" % retrlen
+            headers = mimetools.Message(StringIO.StringIO(headers))
+            return addinfourl(fp, headers, "ftp:" + url)
+        except ftperrors(), msg:
+            raise IOError, ('ftp error', msg), sys.exc_info()[2]
+
+    def open_data(self, url, data=None):
+        """Use "data" URL."""
+        # ignore POSTed data
+        #
+        # syntax of data URLs:
+        # dataurl   := "data:" [ mediatype ] [ ";base64" ] "," data
+        # mediatype := [ type "/" subtype ] *( ";" parameter )
+        # data      := *urlchar
+        # parameter := attribute "=" value
+        import StringIO, mimetools, time
+        try:
+            [type, data] = url.split(',', 1)
+        except ValueError:
+            raise IOError, ('data error', 'bad data URL')
+        if not type:
+            type = 'text/plain;charset=US-ASCII'
+        semi = type.rfind(';')
+        if semi >= 0 and '=' not in type[semi:]:
+            encoding = type[semi+1:]
+            type = type[:semi]
+        else:
+            encoding = ''
+        msg = []
+        msg.append('Date: %s'%time.strftime('%a, %d %b %Y %T GMT',
+                                            time.gmtime(time.time())))
+        msg.append('Content-type: %s' % type)
+        if encoding == 'base64':
+            import base64
+            data = base64.decodestring(data)
+        else:
+            data = unquote(data)
+        msg.append('Content-length: %d' % len(data))
+        msg.append('')
+        msg.append(data)
+        msg = '\n'.join(msg)
+        f = StringIO.StringIO(msg)
+        headers = mimetools.Message(f, 0)
+        f.fileno = None     # needed for addinfourl
+        return addinfourl(f, headers, url)
+
+
+class FancyURLopener(URLopener):
+    """Derived class with handlers for errors we can handle (perhaps)."""
+
+    def __init__(self, *args):
+        apply(URLopener.__init__, (self,) + args)
+        self.auth_cache = {}
+        self.tries = 0
+        self.maxtries = 10
+
+    def http_error_default(self, url, fp, errcode, errmsg, headers):
+        """Default error handling -- don't raise an exception."""
+        return addinfourl(fp, headers, "http:" + url)
+
+    def http_error_302(self, url, fp, errcode, errmsg, headers, data=None):
+        """Error 302 -- relocated (temporarily)."""
+        self.tries += 1
+        if self.maxtries and self.tries >= self.maxtries:
+            if hasattr(self, "http_error_500"):
+                meth = self.http_error_500
+            else:
+                meth = self.http_error_default
+            self.tries = 0
+            return meth(url, fp, 500,
+                        "Internal Server Error: Redirect Recursion", headers)
+        result = self.redirect_internal(url, fp, errcode, errmsg, headers,
+                                        data)
+        self.tries = 0
+        return result
+
+    def redirect_internal(self, url, fp, errcode, errmsg, headers, data):
+        if headers.has_key('location'):
+            newurl = headers['location']
+        elif headers.has_key('uri'):
+            newurl = headers['uri']
+        else:
+            return
+        void = fp.read()
+        fp.close()
+        # In case the server sent a relative URL, join with original:
+        newurl = basejoin(self.type + ":" + url, newurl)
+        if data is None:
+            return self.open(newurl)
+        else:
+            return self.open(newurl, data)
+
+    def http_error_301(self, url, fp, errcode, errmsg, headers, data=None):
+        """Error 301 -- also relocated (permanently)."""
+        return self.http_error_302(url, fp, errcode, errmsg, headers, data)
+
+    def http_error_401(self, url, fp, errcode, errmsg, headers, data=None):
+        """Error 401 -- authentication required.
+        See this URL for a description of the basic authentication scheme:
+        http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt"""
+        if not headers.has_key('www-authenticate'):
+            URLopener.http_error_default(self, url, fp,
+                                         errcode, errmsg, headers)
+        stuff = headers['www-authenticate']
+        import re
+        match = re.match('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', stuff)
+        if not match:
+            URLopener.http_error_default(self, url, fp,
+                                         errcode, errmsg, headers)
+        scheme, realm = match.groups()
+        if scheme.lower() != 'basic':
+            URLopener.http_error_default(self, url, fp,
+                                         errcode, errmsg, headers)
+        name = 'retry_' + self.type + '_basic_auth'
+        if data is None:
+            return getattr(self,name)(url, realm)
+        else:
+            return getattr(self,name)(url, realm, data)
+
+    def retry_http_basic_auth(self, url, realm, data=None):
+        host, selector = splithost(url)
+        i = host.find('@') + 1
+        host = host[i:]
+        user, passwd = self.get_user_passwd(host, realm, i)
+        if not (user or passwd): return None
+        host = quote(user, safe='') + ':' + quote(passwd, safe='') + '@' + host
+        newurl = 'http://' + host + selector
+        if data is None:
+            return self.open(newurl)
+        else:
+            return self.open(newurl, data)
+
+    def retry_https_basic_auth(self, url, realm, data=None):
+        host, selector = splithost(url)
+        i = host.find('@') + 1
+        host = host[i:]
+        user, passwd = self.get_user_passwd(host, realm, i)
+        if not (user or passwd): return None
+        host = quote(user, safe='') + ':' + quote(passwd, safe='') + '@' + host
+        newurl = '//' + host + selector
+        return self.open_https(newurl, data)
+
+    def get_user_passwd(self, host, realm, clear_cache = 0):
+        key = realm + '@' + host.lower()
+        if self.auth_cache.has_key(key):
+            if clear_cache:
+                del self.auth_cache[key]
+            else:
+                return self.auth_cache[key]
+        user, passwd = self.prompt_user_passwd(host, realm)
+        if user or passwd: self.auth_cache[key] = (user, passwd)
+        return user, passwd
+
+    def prompt_user_passwd(self, host, realm):
+        """Override this in a GUI environment!"""
+        import getpass
+        try:
+            user = raw_input("Enter username for %s at %s: " % (realm,
+                                                                host))
+            passwd = getpass.getpass("Enter password for %s in %s at %s: " %
+                (user, realm, host))
+            return user, passwd
+        except KeyboardInterrupt:
+            print
+            return None, None
+
+
+# Utility functions
+
+_localhost = None
+def localhost():
+    """Return the IP address of the magic hostname 'localhost'."""
+    global _localhost
+    if not _localhost:
+        _localhost = socket.gethostbyname('localhost')
+    return _localhost
+
+_thishost = None
+def thishost():
+    """Return the IP address of the current host."""
+    global _thishost
+    if not _thishost:
+        _thishost = socket.gethostbyname(socket.gethostname())
+    return _thishost
+
+_ftperrors = None
+def ftperrors():
+    """Return the set of errors raised by the FTP class."""
+    global _ftperrors
+    if not _ftperrors:
+        import ftplib
+        _ftperrors = ftplib.all_errors
+    return _ftperrors
+
+_noheaders = None
+def noheaders():
+    """Return an empty mimetools.Message object."""
+    global _noheaders
+    if not _noheaders:
+        import mimetools
+        import StringIO
+        _noheaders = mimetools.Message(StringIO.StringIO(), 0)
+        _noheaders.fp.close()   # Recycle file descriptor
+    return _noheaders
+
+
+# Utility classes
+
+class ftpwrapper:
+    """Class used by open_ftp() for cache of open FTP connections."""
+
+    def __init__(self, user, passwd, host, port, dirs):
+        self.user = user
+        self.passwd = passwd
+        self.host = host
+        self.port = port
+        self.dirs = dirs
+        self.init()
+
+    def init(self):
+        import ftplib
+        self.busy = 0
+        self.ftp = ftplib.FTP()
+        self.ftp.connect(self.host, self.port)
+        self.ftp.login(self.user, self.passwd)
+        for dir in self.dirs:
+            self.ftp.cwd(dir)
+
+    def retrfile(self, file, type):
+        import ftplib
+        self.endtransfer()
+        if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1
+        else: cmd = 'TYPE ' + type; isdir = 0
+        try:
+            self.ftp.voidcmd(cmd)
+        except ftplib.all_errors:
+            self.init()
+            self.ftp.voidcmd(cmd)
+        conn = None
+        if file and not isdir:
+            # Use nlst to see if the file exists at all
+            try:
+                self.ftp.nlst(file)
+            except ftplib.error_perm, reason:
+                raise IOError, ('ftp error', reason), sys.exc_info()[2]
+            # Restore the transfer mode!
+            self.ftp.voidcmd(cmd)
+            # Try to retrieve as a file
+            try:
+                cmd = 'RETR ' + file
+                conn = self.ftp.ntransfercmd(cmd)
+            except ftplib.error_perm, reason:
+                if str(reason)[:3] != '550':
+                    raise IOError, ('ftp error', reason), sys.exc_info()[2]
+        if not conn:
+            # Set transfer mode to ASCII!
+            self.ftp.voidcmd('TYPE A')
+            # Try a directory listing
+            if file: cmd = 'LIST ' + file
+            else: cmd = 'LIST'
+            conn = self.ftp.ntransfercmd(cmd)
+        self.busy = 1
+        # Pass back both a suitably decorated object and a retrieval length
+        return (addclosehook(conn[0].makefile('rb'),
+                             self.endtransfer), conn[1])
+    def endtransfer(self):
+        if not self.busy:
+            return
+        self.busy = 0
+        try:
+            self.ftp.voidresp()
+        except ftperrors():
+            pass
+
+    def close(self):
+        self.endtransfer()
+        try:
+            self.ftp.close()
+        except ftperrors():
+            pass
+
+class addbase:
+    """Base class for addinfo and addclosehook."""
+
+    def __init__(self, fp):
+        self.fp = fp
+        self.read = self.fp.read
+        self.readline = self.fp.readline
+        if hasattr(self.fp, "readlines"): self.readlines = self.fp.readlines
+        if hasattr(self.fp, "fileno"): self.fileno = self.fp.fileno
+
+    def __repr__(self):
+        return '<%s at %s whose fp = %s>' % (self.__class__.__name__,
+                                             `id(self)`, `self.fp`)
+
+    def close(self):
+        self.read = None
+        self.readline = None
+        self.readlines = None
+        self.fileno = None
+        if self.fp: self.fp.close()
+        self.fp = None
+
+class addclosehook(addbase):
+    """Class to add a close hook to an open file."""
+
+    def __init__(self, fp, closehook, *hookargs):
+        addbase.__init__(self, fp)
+        self.closehook = closehook
+        self.hookargs = hookargs
+
+    def close(self):
+        addbase.close(self)
+        if self.closehook:
+            apply(self.closehook, self.hookargs)
+            self.closehook = None
+            self.hookargs = None
+
+class addinfo(addbase):
+    """class to add an info() method to an open file."""
+
+    def __init__(self, fp, headers):
+        addbase.__init__(self, fp)
+        self.headers = headers
+
+    def info(self):
+        return self.headers
+
+class addinfourl(addbase):
+    """class to add info() and geturl() methods to an open file."""
+
+    def __init__(self, fp, headers, url):
+        addbase.__init__(self, fp)
+        self.headers = headers
+        self.url = url
+
+    def info(self):
+        return self.headers
+
+    def geturl(self):
+        return self.url
+
+
+def basejoin(base, url):
+    """Utility to combine a URL with a base URL to form a new URL."""
+    type, path = splittype(url)
+    if type:
+        # if url is complete (i.e., it contains a type), return it
+        return url
+    host, path = splithost(path)
+    type, basepath = splittype(base) # inherit type from base
+    if host:
+        # if url contains host, just inherit type
+        if type: return type + '://' + host + path
+        else:
+            # no type inherited, so url must have started with //
+            # just return it
+            return url
+    host, basepath = splithost(basepath) # inherit host
+    basepath, basetag = splittag(basepath) # remove extraneous cruft
+    basepath, basequery = splitquery(basepath) # idem
+    if path[:1] != '/':
+        # non-absolute path name
+        if path[:1] in ('#', '?'):
+            # path is just a tag or query, attach to basepath
+            i = len(basepath)
+        else:
+            # else replace last component
+            i = basepath.rfind('/')
+        if i < 0:
+            # basepath not absolute
+            if host:
+                # host present, make absolute
+                basepath = '/'
+            else:
+                # else keep non-absolute
+                basepath = ''
+        else:
+            # remove last file component
+            basepath = basepath[:i+1]
+        # Interpret ../ (important because of symlinks)
+        while basepath and path[:3] == '../':
+            path = path[3:]
+            i = basepath[:-1].rfind('/')
+            if i > 0:
+                basepath = basepath[:i+1]
+            elif i == 0:
+                basepath = '/'
+                break
+            else:
+                basepath = ''
+
+        path = basepath + path
+    if host and path and path[0] != '/':
+        path = '/' + path
+    if type and host: return type + '://' + host + path
+    elif type: return type + ':' + path
+    elif host: return '//' + host + path # don't know what this means
+    else: return path
+
+
+# Utilities to parse URLs (most of these return None for missing parts):
+# unwrap('<URL:type://host/path>') --> 'type://host/path'
+# splittype('type:opaquestring') --> 'type', 'opaquestring'
+# splithost('//host[:port]/path') --> 'host[:port]', '/path'
+# splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'
+# splitpasswd('user:passwd') -> 'user', 'passwd'
+# splitport('host:port') --> 'host', 'port'
+# splitquery('/path?query') --> '/path', 'query'
+# splittag('/path#tag') --> '/path', 'tag'
+# splitattr('/path;attr1=value1;attr2=value2;...') ->
+#   '/path', ['attr1=value1', 'attr2=value2', ...]
+# splitvalue('attr=value') --> 'attr', 'value'
+# splitgophertype('/Xselector') --> 'X', 'selector'
+# unquote('abc%20def') -> 'abc def'
+# quote('abc def') -> 'abc%20def')
+
+def toBytes(url):
+    """toBytes(u"URL") --> 'URL'."""
+    # Most URL schemes require ASCII. If that changes, the conversion
+    # can be relaxed
+    if type(url) is types.UnicodeType:
+        try:
+            url = url.encode("ASCII")
+        except UnicodeError:
+            raise UnicodeError("URL " + repr(url) +
+                               " contains non-ASCII characters")
+    return url
+
+def unwrap(url):
+    """unwrap('<URL:type://host/path>') --> 'type://host/path'."""
+    url = url.strip()
+    if url[:1] == '<' and url[-1:] == '>':
+        url = url[1:-1].strip()
+    if url[:4] == 'URL:': url = url[4:].strip()
+    return url
+
+_typeprog = None
+def splittype(url):
+    """splittype('type:opaquestring') --> 'type', 'opaquestring'."""
+    global _typeprog
+    if _typeprog is None:
+        import re
+        _typeprog = re.compile('^([^/:]+):')
+
+    match = _typeprog.match(url)
+    if match:
+        scheme = match.group(1)
+        return scheme.lower(), url[len(scheme) + 1:]
+    return None, url
+
+_hostprog = None
+def splithost(url):
+    """splithost('//host[:port]/path') --> 'host[:port]', '/path'."""
+    global _hostprog
+    if _hostprog is None:
+        import re
+        _hostprog = re.compile('^//([^/]*)(.*)$')
+
+    match = _hostprog.match(url)
+    if match: return match.group(1, 2)
+    return None, url
+
+_userprog = None
+def splituser(host):
+    """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
+    global _userprog
+    if _userprog is None:
+        import re
+        _userprog = re.compile('^([^@]*)@(.*)$')
+
+    match = _userprog.match(host)
+    if match: return map(unquote, match.group(1, 2))
+    return None, host
+
+_passwdprog = None
+def splitpasswd(user):
+    """splitpasswd('user:passwd') -> 'user', 'passwd'."""
+    global _passwdprog
+    if _passwdprog is None:
+        import re
+        _passwdprog = re.compile('^([^:]*):(.*)$')
+
+    match = _passwdprog.match(user)
+    if match: return match.group(1, 2)
+    return user, None
+
+# splittag('/path#tag') --> '/path', 'tag'
+_portprog = None
+def splitport(host):
+    """splitport('host:port') --> 'host', 'port'."""
+    global _portprog
+    if _portprog is None:
+        import re
+        _portprog = re.compile('^(.*):([0-9]+)$')
+
+    match = _portprog.match(host)
+    if match: return match.group(1, 2)
+    return host, None
+
+_nportprog = None
+def splitnport(host, defport=-1):
+    """Split host and port, returning numeric port.
+    Return given default port if no ':' found; defaults to -1.
+    Return numerical port if a valid number are found after ':'.
+    Return None if ':' but not a valid number."""
+    global _nportprog
+    if _nportprog is None:
+        import re
+        _nportprog = re.compile('^(.*):(.*)$')
+
+    match = _nportprog.match(host)
+    if match:
+        host, port = match.group(1, 2)
+        try:
+            if not port: raise ValueError, "no digits"
+            nport = int(port)
+        except ValueError:
+            nport = None
+        return host, nport
+    return host, defport
+
+_queryprog = None
+def splitquery(url):
+    """splitquery('/path?query') --> '/path', 'query'."""
+    global _queryprog
+    if _queryprog is None:
+        import re
+        _queryprog = re.compile('^(.*)\?([^?]*)$')
+
+    match = _queryprog.match(url)
+    if match: return match.group(1, 2)
+    return url, None
+
+_tagprog = None
+def splittag(url):
+    """splittag('/path#tag') --> '/path', 'tag'."""
+    global _tagprog
+    if _tagprog is None:
+        import re
+        _tagprog = re.compile('^(.*)#([^#]*)$')
+
+    match = _tagprog.match(url)
+    if match: return match.group(1, 2)
+    return url, None
+
+def splitattr(url):
+    """splitattr('/path;attr1=value1;attr2=value2;...') ->
+        '/path', ['attr1=value1', 'attr2=value2', ...]."""
+    words = url.split(';')
+    return words[0], words[1:]
+
+_valueprog = None
+def splitvalue(attr):
+    """splitvalue('attr=value') --> 'attr', 'value'."""
+    global _valueprog
+    if _valueprog is None:
+        import re
+        _valueprog = re.compile('^([^=]*)=(.*)$')
+
+    match = _valueprog.match(attr)
+    if match: return match.group(1, 2)
+    return attr, None
+
+def splitgophertype(selector):
+    """splitgophertype('/Xselector') --> 'X', 'selector'."""
+    if selector[:1] == '/' and selector[1:2]:
+        return selector[1], selector[2:]
+    return None, selector
+
+def unquote(s):
+    """unquote('abc%20def') -> 'abc def'."""
+    mychr = chr
+    myatoi = int
+    list = s.split('%')
+    res = [list[0]]
+    myappend = res.append
+    del list[0]
+    for item in list:
+        if item[1:2]:
+            try:
+                myappend(mychr(myatoi(item[:2], 16))
+                     + item[2:])
+            except ValueError:
+                myappend('%' + item)
+        else:
+            myappend('%' + item)
+    return "".join(res)
+
+def unquote_plus(s):
+    """unquote('%7e/abc+def') -> '~/abc def'"""
+    if '+' in s:
+        # replace '+' with ' '
+        s = ' '.join(s.split('+'))
+    return unquote(s)
+
+always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+               'abcdefghijklmnopqrstuvwxyz'
+               '0123456789' '_.-')
+
+_fast_safe_test = always_safe + '/'
+_fast_safe = None
+
+def _fast_quote(s):
+    global _fast_safe
+    if _fast_safe is None:
+        _fast_safe = {}
+        for c in _fast_safe_test:
+            _fast_safe[c] = c
+    res = list(s)
+    for i in range(len(res)):
+        c = res[i]
+        if not _fast_safe.has_key(c):
+            res[i] = '%%%02X' % ord(c)
+    return ''.join(res)
+
+def quote(s, safe = '/'):
+    """quote('abc def') -> 'abc%20def'
+
+    Each part of a URL, e.g. the path info, the query, etc., has a
+    different set of reserved characters that must be quoted.
+
+    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
+    the following reserved characters.
+
+    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+                  "$" | ","
+
+    Each of these characters is reserved in some component of a URL,
+    but not necessarily in all of them.
+
+    By default, the quote function is intended for quoting the path
+    section of a URL.  Thus, it will not encode '/'.  This character
+    is reserved, but in typical usage the quote function is being
+    called on a path where the existing slash characters are used as
+    reserved characters.
+    """
+    safe = always_safe + safe
+    if _fast_safe_test == safe:
+        return _fast_quote(s)
+    res = list(s)
+    for i in range(len(res)):
+        c = res[i]
+        if c not in safe:
+            res[i] = '%%%02X' % ord(c)
+    return ''.join(res)
+
+def quote_plus(s, safe = ''):
+    """Quote the query fragment of a URL; replacing ' ' with '+'"""
+    if ' ' in s:
+        l = s.split(' ')
+        for i in range(len(l)):
+            l[i] = quote(l[i], safe)
+        return '+'.join(l)
+    else:
+        return quote(s, safe)
+
+def urlencode(query,doseq=0):
+    """Encode a sequence of two-element tuples or dictionary into a URL query string.
+
+    If any values in the query arg are sequences and doseq is true, each
+    sequence element is converted to a separate parameter.
+
+    If the query arg is a sequence of two-element tuples, the order of the
+    parameters in the output will match the order of parameters in the
+    input.
+    """
+
+    if hasattr(query,"items"):
+        # mapping objects
+        query = query.items()
+    else:
+        # it's a bother at times that strings and string-like objects are
+        # sequences...
+        try:
+            # non-sequence items should not work with len()
+            x = len(query)
+            # non-empty strings will fail this
+            if len(query) and type(query[0]) != types.TupleType:
+                raise TypeError
+            # zero-length sequences of all types will get here and succeed,
+            # but that's a minor nit - since the original implementation
+            # allowed empty dicts that type of behavior probably should be
+            # preserved for consistency
+        except TypeError:
+            ty,va,tb = sys.exc_info()
+            raise TypeError, "not a valid non-string sequence or mapping object", tb
+
+    l = []
+    if not doseq:
+        # preserve old behavior
+        for k, v in query:
+            k = quote_plus(str(k))
+            v = quote_plus(str(v))
+            l.append(k + '=' + v)
+    else:
+        for k, v in query:
+            k = quote_plus(str(k))
+            if type(v) == types.StringType:
+                v = quote_plus(v)
+                l.append(k + '=' + v)
+            elif type(v) == types.UnicodeType:
+                # is there a reasonable way to convert to ASCII?
+                # encode generates a string, but "replace" or "ignore"
+                # lose information and "strict" can raise UnicodeError
+                v = quote_plus(v.encode("ASCII","replace"))
+                l.append(k + '=' + v)
+            else:
+                try:
+                    # is this a sufficient test for sequence-ness?
+                    x = len(v)
+                except TypeError:
+                    # not a sequence
+                    v = quote_plus(str(v))
+                    l.append(k + '=' + v)
+                else:
+                    # loop over the sequence
+                    for elt in v:
+                        l.append(k + '=' + quote_plus(str(elt)))
+    return '&'.join(l)
+
+# Proxy handling
+def getproxies_environment():
+    """Return a dictionary of scheme -> proxy server URL mappings.
+
+    Scan the environment for variables named <scheme>_proxy;
+    this seems to be the standard convention.  If you need a
+    different way, you can pass a proxies dictionary to the
+    [Fancy]URLopener constructor.
+
+    """
+    proxies = {}
+    for name, value in os.environ.items():
+        name = name.lower()
+        if value and name[-6:] == '_proxy':
+            proxies[name[:-6]] = value
+    return proxies
+
+if os.name == 'mac':
+    def getproxies():
+        """Return a dictionary of scheme -> proxy server URL mappings.
+
+        By convention the mac uses Internet Config to store
+        proxies.  An HTTP proxy, for instance, is stored under
+        the HttpProxy key.
+
+        """
+        try:
+            import ic
+        except ImportError:
+            return {}
+
+        try:
+            config = ic.IC()
+        except ic.error:
+            return {}
+        proxies = {}
+        # HTTP:
+        if config.has_key('UseHTTPProxy') and config['UseHTTPProxy']:
+            try:
+                value = config['HTTPProxyHost']
+            except ic.error:
+                pass
+            else:
+                proxies['http'] = 'http://%s' % value
+        # FTP: XXXX To be done.
+        # Gopher: XXXX To be done.
+        return proxies
+
+    def proxy_bypass(x):
+        return 0
+
+elif os.name == 'nt':
+    def getproxies_registry():
+        """Return a dictionary of scheme -> proxy server URL mappings.
+
+        Win32 uses the registry to store proxies.
+
+        """
+        proxies = {}
+        try:
+            import _winreg
+        except ImportError:
+            # Std module, so should be around - but you never know!
+            return proxies
+        try:
+            internetSettings = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER,
+                r'Software\Microsoft\Windows\CurrentVersion\Internet Settings')
+            proxyEnable = _winreg.QueryValueEx(internetSettings,
+                                               'ProxyEnable')[0]
+            if proxyEnable:
+                # Returned as Unicode but problems if not converted to ASCII
+                proxyServer = str(_winreg.QueryValueEx(internetSettings,
+                                                       'ProxyServer')[0])
+                if '=' in proxyServer:
+                    # Per-protocol settings
+                    for p in proxyServer.split(';'):
+                        protocol, address = p.split('=', 1)
+                        # See if address has a type:// prefix
+                        import re
+                        if not re.match('^([^/:]+)://', address):
+                            address = '%s://%s' % (protocol, address)
+                        proxies[protocol] = address
+                else:
+                    # Use one setting for all protocols
+                    if proxyServer[:5] == 'http:':
+                        proxies['http'] = proxyServer
+                    else:
+                        proxies['http'] = 'http://%s' % proxyServer
+                        proxies['ftp'] = 'ftp://%s' % proxyServer
+            internetSettings.Close()
+        except (WindowsError, ValueError, TypeError):
+            # Either registry key not found etc, or the value in an
+            # unexpected format.
+            # proxies already set up to be empty so nothing to do
+            pass
+        return proxies
+
+    def getproxies():
+        """Return a dictionary of scheme -> proxy server URL mappings.
+
+        Returns settings gathered from the environment, if specified,
+        or the registry.
+
+        """
+        return getproxies_environment() or getproxies_registry()
+
+    def proxy_bypass(host):
+        try:
+            import _winreg
+            import re
+            import socket
+        except ImportError:
+            # Std modules, so should be around - but you never know!
+            return 0
+        try:
+            internetSettings = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER,
+                r'Software\Microsoft\Windows\CurrentVersion\Internet Settings')
+            proxyEnable = _winreg.QueryValueEx(internetSettings,
+                                               'ProxyEnable')[0]
+            proxyOverride = str(_winreg.QueryValueEx(internetSettings,
+                                                     'ProxyOverride')[0])
+            # ^^^^ Returned as Unicode but problems if not converted to ASCII
+        except WindowsError:
+            return 0
+        if not proxyEnable or not proxyOverride:
+            return 0
+        # try to make a host list from name and IP address.
+        host = [host]
+        try:
+            addr = socket.gethostbyname(host[0])
+            if addr != host:
+                host.append(addr)
+        except socket.error:
+            pass
+        # make a check value list from the registry entry: replace the
+        # '<local>' string by the localhost entry and the corresponding
+        # canonical entry.
+        proxyOverride = proxyOverride.split(';')
+        i = 0
+        while i < len(proxyOverride):
+            if proxyOverride[i] == '<local>':
+                proxyOverride[i:i+1] = ['localhost',
+                                        '127.0.0.1',
+                                        socket.gethostname(),
+                                        socket.gethostbyname(
+                                            socket.gethostname())]
+            i += 1
+        # print proxyOverride
+        # now check if we match one of the registry values.
+        for test in proxyOverride:
+            test = test.replace(".", r"\.")     # mask dots
+            test = test.replace("*", r".*")     # change glob sequence
+            test = test.replace("?", r".")      # change glob char
+            for val in host:
+                # print "%s <--> %s" %( test, val )
+                if re.match(test, val, re.I):
+                    return 1
+        return 0
+
+else:
+    # By default use environment variables
+    getproxies = getproxies_environment
+
+    def proxy_bypass(host):
+        return 0
+
+# Test and time quote() and unquote()
+def test1():
+    import time
+    s = ''
+    for i in range(256): s = s + chr(i)
+    s = s*4
+    t0 = time.time()
+    qs = quote(s)
+    uqs = unquote(qs)
+    t1 = time.time()
+    if uqs != s:
+        print 'Wrong!'
+    print `s`
+    print `qs`
+    print `uqs`
+    print round(t1 - t0, 3), 'sec'
+
+
+def reporthook(blocknum, blocksize, totalsize):
+    # Report during remote transfers
+    print "Block number: %d, Block size: %d, Total size: %d" % (
+        blocknum, blocksize, totalsize)
+
+# Test program
+def test(args=[]):
+    if not args:
+        args = [
+            '/etc/passwd',
+            'file:/etc/passwd',
+            'file://localhost/etc/passwd',
+            'ftp://ftp.python.org/pub/python/README',
+##          'gopher://gopher.micro.umn.edu/1/',
+            'http://www.python.org/index.html',
+            ]
+        if hasattr(URLopener, "open_https"):
+            args.append('https://synergy.as.cmu.edu/~geek/')
+    try:
+        for url in args:
+            print '-'*10, url, '-'*10
+            fn, h = urlretrieve(url, None, reporthook)
+            print fn
+            if h:
+                print '======'
+                for k in h.keys(): print k + ':', h[k]
+                print '======'
+            fp = open(fn, 'rb')
+            data = fp.read()
+            del fp
+            if '\r' in data:
+                table = string.maketrans("", "")
+                data = data.translate(table, "\r")
+            print data
+            fn, h = None, None
+        print '-'*40
+    finally:
+        urlcleanup()
+
+def main():
+    import getopt, sys
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "th")
+    except getopt.error, msg:
+        print msg
+        print "Use -h for help"
+        return
+    t = 0
+    for o, a in opts:
+        if o == '-t':
+            t = t + 1
+        if o == '-h':
+            print "Usage: python urllib.py [-t] [url ...]"
+            print "-t runs self-test;",
+            print "otherwise, contents of urls are printed"
+            return
+    if t:
+        if t > 1:
+            test1()
+        test(args)
+    else:
+        if not args:
+            print "Use -h for help"
+        for url in args:
+            print urlopen(url).read(),
+
+# Run test program when run as a script
+if __name__ == '__main__':
+    main()