diff --git a/cfgfile.py b/cfgfile.py new file mode 100644 index 0000000..077657a --- /dev/null +++ b/cfgfile.py @@ -0,0 +1,117 @@ +# +# PyBorg: The python AI bot. +# +# Copyright (c) 2000, 2006 Tom Morton, Sebastien Dailly +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +import string + +def load_config(filename): + """ + Load a config file returning dictionary of variables. + """ + try: + f = open(filename, "r") + except IOError, e: + return None + + stuff = {} + line = 0 + + while 1: + line = line + 1 + s = f.readline() + if s=="": + break + if s[0]=="#": + continue + + #read if the string is above multiple lines + while s.rfind("\\") > -1: + s = s[:s.rfind("\\")] + f.readline() + line = line + 1 + + s = string.split(s, "=") + if len(s) != 2: + print "Malformed line in %s line %d" % (filename, line) + print s + continue + stuff[string.strip(s[0])] = eval(string.strip(string.join(s[1:], "="))) + return stuff + +def save_config(filename, fields): + """ + fields should be a dictionary. Keys as names of + variables containing tuple (string comment, value). + """ + f = open(filename, "w") + + # write the values with comments. this is a silly comment + for key in fields.keys(): + f.write("# "+fields[key][0]+"\n") + s = repr(fields[key][1]) + f.write(key+"\t= ") + if len(s) > 80: + cut_string = "" + while len(s) > 80: + position = s.find(",",75)+1 + cut_string = cut_string + s[:position] + "\\\n\t\t" + s = s[position:] + s = cut_string + s + f.write(s+"\n") + + f.close() + + +class cfgset: + def load(self, filename, defaults): + """ + Defaults should be key=variable name, value= + tuple of (comment, default value) + """ + self._defaults = defaults + self._filename = filename + + for i in defaults.keys(): + self.__dict__[i] = defaults[i][1] + + # try to laad saved ones + vars = load_config(filename) + if vars == None: + # none found. this is new + self.save() + return + for i in vars.keys(): + self.__dict__[i] = vars[i] + + def save(self): + """ + Save borg settings + """ + keys = {} + for i in self.__dict__.keys(): + # reserved + if i == "_defaults" or i == "_filename": + continue + if self._defaults.has_key(i): + comment = self._defaults[i][0] + else: + comment = "" + keys[i] = (comment, self.__dict__[i]) + # save to config file + save_config(self._filename, keys) + diff --git a/convert.py b/convert.py new file mode 100644 index 0000000..c183026 --- /dev/null +++ b/convert.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# Use to convert pyborg 0.9.10 and 0.9.11 dictionaries to the +# version 1.0.0+ format. +# +# Copyright (c) 2000, 2006 Tom Morton, Sebastien Dailly +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +import sys +import marshal +import struct +import string + +# Read the dictionary +print "Reading dictionary stage 1..." + +try: + f = open("lines.dat", "r") + s = f.read() + f.close() + lines = marshal.loads(s) + del s +except (EOFError, IOError), e: + print "Error reading dictionary." + sys.exit() + +print "working..." +for x in lines.keys(): + # clean up whitespace mess + line = lines[x] + words = string.split(line) + lines[x] = string.join(words, " ") + +print "Saving Dictionary..." + +f = open("lines.dat", "w") +s = marshal.dumps(lines) +f.write(s) +f.close() + +# Read the dictionary +print "Reading dictionary stage 2..." + +try: + f = open("words.dat", "r") + s = f.read() + f.close() + words = marshal.loads(s) + del s +except (EOFError, IOError), e: + print "Error reading dictionary." + sys.exit() + +print "working..." +for key in words.keys(): + # marshallise it: + y = [] + for i in words[key]: + y.append(struct.pack("iH", i[0], i[1])) + words[key] = y + +print "Saving Dictionary..." + +f = open("words.dat", "w") +s = marshal.dumps(words) +f.write(s) +f.close() +print "Done." + diff --git a/convert2.py b/convert2.py new file mode 100644 index 0000000..131a804 --- /dev/null +++ b/convert2.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# +# PyBorg: The python AI bot. +# +# +# Use to convert pyborg 1.0.6 dictionaries to the +# version 1.1.0+ format. +# +# Copyright (c) 2006 Sebastien Dailly +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +import sys +import marshal +import struct +import string + +print "Reading dictionary..." +try: + f = open("words.dat", "rb") + s = f.read() + f.close() + self.words = marshal.loads(s) + del s + f = open("lines.dat", "rb") + s = f.read() + f.close() + self.lines = marshal.loads(s) + del s +except (EOFError, IOError), e: + pass + + +#change the contexts here ! +compteur = 0 +for x in self.lines.keys(): + self.lines[x]=[self.lines[x],1] + compteur = 1 +if compteur != 0: + print "Contexts update done" + +print "Writing dictionary..." + +zfile = zipfile.ZipFile('archive.zip','r') +for filename in zfile.namelist(): + data = zfile.read(filename) + file = open(filename, 'w+b') + file.write(data) + file.close() + +f = open("words.dat", "wb") +s = marshal.dumps(self.words) +f.write(s) +f.close() +f = open("lines.dat", "wb") +s = marshal.dumps(self.lines) +f.write(s) +f.close() + +#zip the files +f = zipfile.ZipFile('archive.zip','w',zipfile.ZIP_DEFLATED) +f.write('words.dat') +f.write('lines.dat') +f.close() + +try: + os.remove('words.dat') + os.remove('lines.dat') +except (OSError, IOError), e: + print "could not remove the files" + +f.close() diff --git a/pyborg-filein.py b/pyborg-filein.py new file mode 100755 index 0000000..5f16dde --- /dev/null +++ b/pyborg-filein.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# +# PyBorg ascii file input module +# +# Copyright (c) 2000, 2001 Tom Morton +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +import string +import sys + +import pyborg + +class ModFileIn: + """ + Module for file input. Learning from ASCII text files. + """ + + # Command list for this module + commandlist = "FileIn Module Commands:\nNone" + commanddict = {} + + def __init__(self, Borg, args): + + f = open(args[1], "r") + buffer = f.read() + f.close() + + print "I knew "+`Borg.settings.num_words`+" words ("+`len(Borg.lines)`+" lines) before reading "+sys.argv[1] + buffer = pyborg.filter_message(buffer) + # Learn from input + try: + Borg.learn(buffer) + except KeyboardInterrupt, e: + # Close database cleanly + print "Premature termination :-(" + print "I know "+`Borg.settings.num_words`+" words ("+`len(Borg.lines)`+" lines) now." + del Borg + + def shutdown(self): + pass + + def start(self): + sys.exit() + + def output(self, message, args): + pass + +if __name__ == "__main__": + if len(sys.argv) < 2: + print "Specify a filename." + sys.exit() + # start the pyborg + my_pyborg = pyborg.pyborg() + ModFileIn(my_pyborg, sys.argv) + my_pyborg.save_all() + del my_pyborg + diff --git a/pyborg-irc.py b/pyborg-irc.py new file mode 100755 index 0000000..39288f9 --- /dev/null +++ b/pyborg-irc.py @@ -0,0 +1,602 @@ +#! /usr/bin/env python +# +# PyBorg IRC module +# +# Copyright (c) 2000, 2006 Tom Morton, Sebastien Dailly +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +import string +import sys + +try: + from ircbot import * + from irclib import * +except: + print "ERROR !!!!\nircbot.py and irclib.py not found, please install them" + sys.exit(1) + +#overide irclib function +def my_remove_connection(self, connection): + if self.fn_to_remove_socket: + self.fn_to_remove_socket(connection._get_socket()) + +IRC._remove_connection = my_remove_connection + +import os +import pyborg +import cfgfile +import random +import time +import traceback +import thread + +def get_time(): + """ + Return time as a nice yummy string + """ + return time.strftime("%H:%M:%S", time.localtime(time.time())) + + +class ModIRC(SingleServerIRCBot): + """ + Module to interface IRC input and output with the PyBorg learn + and reply modules. + """ + # The bot recieves a standard message on join. The standard part + # message is only used if the user doesn't have a part message. + join_msg = "%s"# is here" + part_msg = "%s"# has left" + + # For security the owner's host mask is stored + # DON'T CHANGE THIS + owner_mask = [] + + + # Command list for this module + commandlist = "IRC Module Commands:\n!chans, !ignore, \ +!join, !nick, !nocolour, !part, !quit, !quitmsg, !reply2ignored, !replyrate, !shutup, \ +!stealth, !unignore, !wakeup, !talk, !owner" + # Detailed command description dictionary + commanddict = { + "shutup": "Owner command. Usage: !shutup\nStop the bot talking", + "wakeup": "Owner command. Usage: !wakeup\nAllow the bot to talk", + "join": "Owner command. Usage: !join #chan1 [#chan2 [...]]\nJoin one or more channels", + "part": "Owner command. Usage: !part #chan1 [#chan2 [...]]\nLeave one or more channels", + "chans": "Owner command. Usage: !chans\nList channels currently on", + "nick": "Owner command. Usage: !nick nickname\nChange nickname", + "ignore": "Owner command. Usage: !ignore [nick1 [nick2 [...]]]\nIgnore one or more nicknames. Without arguments it lists ignored nicknames", + "unignore": "Owner command. Usage: !unignore nick1 [nick2 [...]]\nUnignores one or more nicknames", + "replyrate": "Owner command. Usage: !replyrate [rate%]\nSet rate of bot replies to rate%. Without arguments (not an owner-only command) shows the current reply rate", + "reply2ignored": "Owner command. Usage: !reply2ignored [on|off]\nAllow/disallow replying to ignored users. Without arguments shows the current setting", + "nocolour": "Owner command. Usage: !nocolour [on|off]\nTurn mIRC colour filtering on or off. Without arguments shows the current setting", + "stealth": "Owner command. Usage: !stealth [on|off]\nTurn stealth mode on or off (disable non-owner commands and don't return CTCP VERSION). Without arguments shows the current setting", + "quitmsg": "Owner command. Usage: !quitmsg [message]\nSet the quit message. Without arguments show the current quit message", + "talk": "Owner commande. Usage !talk nick message\nmake the bot send the sentence 'message' to 'nick'", + "quit": "Owner command. Usage: !quit\nMake the bot quit IRC", + "owner": "Usage: !owner password\nAllow to become owner of the bot" + } + + def __init__(self, my_pyborg, args): + """ + Args will be sys.argv (command prompt arguments) + """ + # PyBorg + self.pyborg = my_pyborg + # load settings + + self.settings = cfgfile.cfgset() + self.settings.load("pyborg-irc.cfg", + { "myname": ("The bot's nickname", "PyBorg"), + "realname": ("Reported 'real name'", "Pyborg"), + "owners": ("Owner(s) nickname", [ "OwnerNick" ]), + "servers": ("IRC Server to connect to", [("irc.starchat.net", 6667)]), + "chans": ("Channels to auto-join", ["#test"]), + "speaking": ("Allow the bot to talk on channels", 1), + "stealth": ("Hide the fact we are a bot", 0), + "nocolour": ("Remove mIRC colours", 0), + "ignorelist": ("Ignore these nicknames:", []), + "reply2ignored": ("Reply to ignored people", 0), + "reply_chance": ("Chance of reply (%) per message", 33), + "quitmsg": ("IRC quit message", "Bye :-("), + "password": ("password for control the bot (Edit manually !)", "") + } ) + + self.owners = self.settings.owners[:] + self.chans = self.settings.chans[:] + + # Parse command prompt parameters + + for x in range(1, len(args)): + # Specify servers + if args[x] == "-s": + self.settings.servers = [] + # Read list of servers + for y in range(x+1, len(args)): + if args[y][0] == "-": + break + server = string.split(args[y], ":") + # Default port if none specified + if len(server) == 1: + server.append("6667") + self.settings.servers.append( (server[0], int(server[1])) ) + # Channels + if args[x] == "-c": + self.settings.chans = [] + # Read list of channels + for y in range(x+1, len(args)): + if args[y][0] == "-": + break + self.settings.chans.append("#"+args[y]) + # Nickname + if args[x] == "-n": + try: + self.settings.myname = args[x+1] + except IndexError: + pass + + def our_start(self): + print "Connecting to server..." + SingleServerIRCBot.__init__(self, self.settings.servers, self.settings.myname, self.settings.realname, 2) + + self.start() + + def on_welcome(self, c, e): + print self.chans + for i in self.chans: + c.join(i) + + def shutdown(self): + try: + self.die() # disconnect from server + except AttributeError, e: + # already disconnected probably (pingout or whatever) + pass + + def get_version(self): + if self.settings.stealth: + # stealth mode. we shall be a windows luser today + return "VERSION mIRC32 v5.6 K.Mardam-Bey" + else: + return self.pyborg.ver_string + + def on_kick(self, c, e): + """ + Process leaving + """ + # Parse Nickname!username@host.mask.net to Nickname + kicked = e.arguments()[0] + kicker = string.split(e.source(), "!")[0] + target = e.target() #channel + if len(e.arguments()) >= 2: + reason = e.arguments()[1] + else: + reason = "" + + if kicked == self.settings.myname: + print "["+get_time()+"] <-- "+kicked+" was kicked off "+`target`+" by "+kicker+" ("+reason+")" + +# def on_join(self, c, e): +# """ +# Process joining +# """ +# # Parse Nickname!username@host.mask.net to Nickname +# source = string.split(e.source(), "!")[0] +# target = e.target() +# +# # Converts joins to "someone is here", to give the bot +# # something to work on. +# # eg. "%s is here" % source +# body = self.join_msg % source +# +# #print "["+get_time()+"] --> "+source+" joined "+target +# +# # We want reply rate % chance, if speaking is on +# replyrate = self.settings.speaking * self.settings.reply_chance / 3 +# +# # Learn from 'someone has left' +# self.pyborg.process_msg(self, body, 0, 1, (body, source, target, c, e)) +# # (possibly) reply to 'someone' +# self.pyborg.process_msg(self, source, replyrate, 0, (body, source, target, c, e)) + + def on_privmsg(self, c, e): + self.on_msg(c, e) + + def on_pubmsg(self, c, e): + self.on_msg(c, e) + + def on_ctcp(self, c, e): + ctcptype = e.arguments()[0] + if ctcptype == "ACTION": + self.on_msg(c, e) + else: + SingleServerIRCBot.on_ctcp(self, c, e) + + def _on_disconnect(self, c, e): +# self.channels = IRCDict() + print "deconnection" + self.connection.execute_delayed(self.reconnection_interval, self._connected_checker) + + + def on_msg(self, c, e): + """ + Process messages. + """ + # Parse Nickname!username@host.mask.net to Nickname + source = string.split(e.source(), "!")[0] + target = e.target() + + learn = 1 + + # First message from owner 'locks' the owner host mask + # se people can't change to the owner nick and do horrible + # stuff like '!unlearn the' :-) + if not e.source() in self.owner_mask and source in self.owners: + self.owner_mask.append(e.source()) + print "Locked owner as "+e.source() + + + # Ignore self. + if source == self.settings.myname: + print "["+get_time()+"] <"+source+" >> "+target+"> "+body + return + + # Message text + if len(e.arguments()) == 1: + # Normal message + body = e.arguments()[0] + else: + # A CTCP thing + if e.arguments()[0] == "ACTION": + body = source + " " + e.arguments()[1] + else: + # Ignore all the other CTCPs + return + + debut = body.rfind(",") + if 1 < debut and debut < 5: + x = 0 + for x in range(debut+1, len(body)): + if body[x].isdigit() == 0: + break + body = body[x:] + + # WHOOHOOO!! + if target == self.settings.myname: print "["+get_time()+"] <"+source+" >> "+target+"> "+body + + #replace nicknames by "#nick" + if e.eventtype() == "pubmsg": + for x in self.channels[target].users(): + body = body.replace(x, "#nick") + + # Ignore selected nicks + if self.settings.ignorelist.count(string.lower(source)) > 0 \ + and self.settings.reply2ignored == 1: + print "Nolearn from "+source + learn = 0 + elif self.settings.ignorelist.count(string.lower(source)) > 0: + print "Ignoring "+source + return + + # Stealth mode. disable commands for non owners + if (not source in self.owners) and self.settings.stealth: + while body[:1] == "!": + body = body[1:] + if body == "": + return + + # Parse ModIRC commands + if body[0] == "!": + if self.irc_commands(body, source, target, c, e) == 1: + return + + # Ignore quoted messages + if body[0] == "<" or body[0:1] == "\"" or body[0:1] == " <": + print "Ignoring quoted text" + return + + # We want replies reply_chance%, if speaking is on + replyrate = self.settings.speaking * self.settings.reply_chance + + # double reply chance if the text contains our nickname :-) + if string.find( string.lower(body), string.lower(self.settings.myname) ) != -1: + replyrate = replyrate * 2 + + # Always reply to private messages + if e.eventtype() == "privmsg": + replyrate = 100 + + # Pass message onto pyborg + if source in self.owners and self.owner_mask == e.source(): + self.pyborg.process_msg(self, body, replyrate, learn, (body, source, target, c, e), owner=1) + else: +# self.pyborg.process_msg(self, body, replyrate, learn, (body, source, target, c, e)) + thread.start_new_thread(self.pyborg.process_msg, (self, body, replyrate, learn, (body, source, target, c, e))) + + def irc_commands(self, body, source, target, c, e): + """ + Special IRC commands. + """ + msg = "" + + command_list = string.split(body) + command_list[0] = string.lower(command_list[0]) + + ### User commands + # Query replyrate + if command_list[0] == "!replyrate" and len(command_list)==1: + msg = "Reply rate is "+`self.settings.reply_chance`+"%." + + if command_list[0] == "!owner" and len(command_list) > 1 and source not in self.owners: + if command_list[1] == self.settings.password: + self.owners.append(source) + self.output("You've been added to owners list", ("", source, target, c, e)) + else: + self.output("try again", ("", source, target, c, e)) + + ### Owner commands + if source in self.owners and e.source() in self.owner_mask: + + # Change nick + if command_list[0] == "!nick": + try: + self.connection.nick(command_list[1]) + self.settings.myname = command_list[1] + except: + pass + # stealth mode + elif command_list[0] == "!stealth": + msg = "Stealth mode " + if len(command_list) == 1: + if self.settings.stealth == 0: + msg = msg + "off" + else: + msg = msg + "on" + else: + toggle = string.lower(command_list[1]) + if toggle == "on": + msg = msg + "on" + self.settings.stealth = 1 + else: + msg = msg + "off" + self.settings.stealth = 0 + # filter mirc colours out? + elif command_list[0] == "!nocolor" or command_list[0] == "!nocolour": + msg = "mIRC colours filtering " + if len(command_list) == 1: + if self.settings.nocolour == 0: + msg = msg + "off" + else: + msg = msg + "on" + else: + toggle = command_list[1] + if toggle == "on": + msg = msg + "on" + self.settings.nocolour = 1 + else: + msg = msg + "off" + self.settings.nocolour = 0 + # Allow/disallow replying to ignored nicks + # (they will never be learnt from) + elif command_list[0] == "!reply2ignored": + msg = "Replying to ignored users " + if len(command_list) == 1: + if self.settings.reply2ignored == 0: + msg = msg + "off" + else: + msg = msg + "on" + else: + toggle = command_list[1] + if toggle == "on": + msg = msg + "on" + self.settings.reply2ignored = 1 + else: + msg = msg + "off" + self.settings.reply2ignored = 0 + # Stop talking + elif command_list[0] == "!shutup": + if self.settings.speaking == 1: + msg = "I'll be quiet :-(" + self.settings.speaking = 0 + else: + msg = ":-o" + # Wake up again + elif command_list[0] == "!wakeup": + if self.settings.speaking == 0: + self.settings.speaking = 1 + msg = "Whoohoo!" + else: + msg = "But i'm already awake..." + + # Join a channel or list of channels + elif command_list[0] == "!join": + for x in range(1, len(command_list)): + if not command_list[x] in self.chans: + msg = "Attempting to join channel "+command_list[x] + self.chans.append(command_list[x]) + c.join(command_list[x]) + + # Part a channel or list of channels + elif command_list[0] == "!part": + for x in range(1, len(command_list)): + if command_list[x] in self.chans: + msg = "Leaving channel "+command_list[x] + self.chans.remove(command_list[x]) + c.part(command_list[x]) + + # List channels currently on + elif command_list[0] == "!chans": + if len(self.channels.keys())==0: + msg = "I'm currently on no channels" + else: + msg = "I'm currently on " + channels = self.channels.keys() + for x in range(0, len(channels)): + msg = msg+channels[x]+" " + # add someone to the ignore list + elif command_list[0] == "!ignore": + # if no arguments are given say who we are + # ignoring + if len(command_list) == 1: + msg = "I'm ignoring " + if len(self.settings.ignorelist) == 0: + msg = msg + "nobody" + else: + for x in range(0, len(self.settings.ignorelist)): + msg = msg + self.settings.ignorelist[x] + " " + # Add everyone listed to the ignore list + # eg !ignore tom dick harry + else: + for x in range(1, len(command_list)): + self.settings.ignorelist.append(string.lower(command_list[x])) + msg = "done" + # remove someone from the ignore list + elif command_list[0] == "!unignore": + # Remove everyone listed from the ignore list + # eg !unignore tom dick harry + for x in range(1, len(command_list)): + try: + self.settings.ignorelist.remove(string.lower(command_list[x])) + msg = "done" + except: + pass + # set the quit message + elif command_list[0] == "!quitmsg": + if len(command_list) > 1: + self.settings.quitmsg = string.split(body, " ", 1)[1] + msg = "New quit message is \"%s\"" % self.settings.quitmsg + else: + msg = "Quit message is \"%s\"" % self.settings.quitmsg + # make the pyborg quit + elif command_list[0] == "!quit": + sys.exit() + # Change reply rate + elif command_list[0] == "!replyrate": + try: + self.settings.reply_chance = int(command_list[1]) + msg = "Now replying to "+`int(command_list[1])`+"% of messages." + except: + msg = "Reply rate is "+`self.settings.reply_chance`+"%." + #make the bot talk + elif command_list[0] == "!talk": + if len(command_list) >= 2: + phrase="" + for x in range (2, len (command_list)): + phrase = phrase + str(command_list[x]) + " " + self.output(phrase, ("", command_list[1], "", c, e)) + # Save changes + self.pyborg.settings.save() + self.settings.save() + + if msg == "": + return 0 + else: + self.output(msg, ("", source, target, c, e)) + return 1 + + def output(self, message, args): + """ + Output a line of text. args = (body, source, target, c, e) + """ + if not self.connection.is_connected(): + print "output : not connected" + return + + # Unwrap arguments + body, source, target, c, e = args + + # mIRC colours filtering + if self.settings.nocolour == 1: + body = string.replace(body, "\003", "") + + # replace by the good nickname + if e.eventtype() == "privmsg": + message = message.replace("#nick", source) + else: + message = message.replace("#nick", "") + + # Decide. should we do a ctcp action? + if string.find(message, string.lower(self.settings.myname)+" ") == 0: + action = 1 + message = message[len(self.settings.myname)+1:] + else: + action = 0 + + # Joins replies and public messages + if e.eventtype() == "join" or e.eventtype() == "quit" or e.eventtype() == "part" or e.eventtype() == "pubmsg": + if action == 0: + print "["+get_time()+"] <"+self.settings.myname+" >> "+target+"> "+message + c.privmsg(target, message) + else: + print "["+get_time()+"] <"+self.settings.myname+" >> "+target+"> /me "+message + c.action(target, message) + # Private messages + elif e.eventtype() == "privmsg": + # Send copy of what the guy said to owner + if not source in self.owners: + # Send to all possible owner nicks. this is a + # SHITTY HACK XXX XXX XXX + for x in self.owners: + if action == 0: + c.privmsg(x, "(From "+source+") "+body) + else: + c.action(x, "(From "+source+") "+body) + # normal private msg + if action == 0: + print "["+get_time()+"] <"+self.settings.myname+" >> "+source+"> "+message + c.privmsg(source, message) + # send copy of bot reply to owner + if not source in self.owners: + for x in self.owners: + c.privmsg(x, "(To "+source+") "+message) + # ctcp action priv msg + else: + print "["+get_time()+"] <"+self.settings.myname+" >> "+source+"> /me "+message + c.action(source, message) + # send copy of bot reply to owner + if not source in self.owners: + for x in self.owners: + c.action(x, "(To "+source+") "+message) + +if __name__ == "__main__": + + if "--help" in sys.argv: + print "Pyborg irc bot. Usage:" + print " pyborg-irc.py [options]" + print " -s server:port" + print " -c channel" + print " -n nickname" + print "Defaults stored in pyborg-irc.cfg" + print + sys.exit(0) + # start the pyborg + my_pyborg = pyborg.pyborg() + bot = ModIRC(my_pyborg, sys.argv) + try: + bot.our_start() + except KeyboardInterrupt, e: + pass + except SystemExit, e: + pass + except: + traceback.print_exc() + c = raw_input("Ooops! It looks like Pyborg has crashed. Would you like to save its dictionary? (y/n) ") + if string.lower(c)[:1] == 'n': + sys.exit(0) + bot.disconnect(bot.settings.quitmsg) + my_pyborg.save_all() + del my_pyborg + diff --git a/pyborg-linein.py b/pyborg-linein.py new file mode 100755 index 0000000..4c2fd58 --- /dev/null +++ b/pyborg-linein.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# +# PyBorg Offline line input module +# +# Copyright (c) 2000, 2001 Tom Morton +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +import string +import sys + +import pyborg + +class ModLineIn: + """ + Module to interface console input and output with the PyBorg learn + and reply modules. Allows offline chat with PyBorg. + """ + # Command list for this module + commandlist = "LineIn Module Commands:\n!quit" + commanddict = { "quit": "Usage: !quit\nQuits pyborg and saves the dictionary" } + + def __init__(self, my_pyborg): + self.pyborg = my_pyborg + self.start() + + def start(self): + print "PyBorg offline chat!\n" + print "Type !quit to leave" + while 1: + try: + body = raw_input("> ") + except (KeyboardInterrupt, EOFError), e: + print + return + if body == "": + continue + if body[0] == "!": + if self.linein_commands(body): + continue + # Pass message to borg + self.pyborg.process_msg(self, body, 100, 1, ( None ), owner = 1) + + def linein_commands(self, body): + command_list = string.split(body) + command_list[0] = string.lower(command_list[0]) + + if command_list[0] == "!quit": + sys.exit(0) + + def output(self, message, args): + """ + Output a line of text. + """ + print message + +if __name__ == "__main__": + # start the pyborg + my_pyborg = pyborg.pyborg() + try: + ModLineIn(my_pyborg) + except SystemExit: + pass + my_pyborg.save_all() + del my_pyborg + diff --git a/pyborg-telnet.py b/pyborg-telnet.py new file mode 100644 index 0000000..ce483a6 --- /dev/null +++ b/pyborg-telnet.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# +# PyBorg Telnet module (fairly raw 'telnet'...) +# Defaults to listening on port 8489 +# +# Copyright (c) 2000, 2001 Tom Morton +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +import sys +import string +import socket +import SocketServer + +import pyborg + +class handler(SocketServer.BaseRequestHandler): + # Command list for this module + commandlist = "Telnet Module Commands:\n!quit" + commanddict = {} + + def handle(self): + #### + if ("-v" in sys.argv) or ("--verbose" in sys.argv): + self.opt_verbose = 1 + else: + self.opt_verbose = 0 + #### + print "Connection from ", self.request.getpeername() + self.request.send("\r\nPyborg. Type !quit to leave.\r\n") + while 1: + try: + self.request.send("> ") + body = "" + while 1: + new = self.request.recv(1000) + if new[-2:] != "\r\n": + if new == '\x08': + body = body[:-1] + else: + body = body + new + else: + body = body + new + break + except socket.error, e: + print "Closed connection to", self.request.getpeername(), ": ", e, e.args + return + else: + if self.opt_verbose: + print "%s --> \"%s\"" % (self.request.getpeername(), body[:-2]) + # Telnet module commands. + if string.lower(body[0:5]) == "!quit": + self.output("Bye", None) + print "Closed connection to", self.request.getpeername(), ". User quit." + return + else: + my_pyborg.process_msg(self, body, 100, 1, None, owner=0) + + def output(self, message, args): + """ + Output pyborg reply. + """ + if self.opt_verbose: + print "%s <-- \"%s\"" % (self.request.getpeername(), message) + try: + self.request.send(message+"\r\n") + except: + pass + +if __name__ == '__main__': + # start the damn server + if "--help" in sys.argv: + print "Pyborg telnet module." + print + print "-v --verbose" + print "-p --port n Listen on port n (Defaults to 8489)" + print + sys.exit(0) + + port = 8489 + if "-p" in sys.argv or "--port" in sys.argv: + try: + x = sys.argv.index("-p") + except ValueError, e: + x = sys.argv.index("--port") + if len(sys.argv) > x+1: + try: + port = int(sys.argv[x+1]) + except ValueError, e: + pass + try: + server = SocketServer.ThreadingTCPServer(("", port), handler) + except socket.error, e: + print "Socket error: ", e.args + else: + print "Starting pyborg..." + my_pyborg = pyborg.pyborg() + print "Awaiting connections..." + try: + server.serve_forever() + except KeyboardInterrupt, e: + print "Server shut down" + my_pyborg.save_all() + del my_pyborg + diff --git a/pyborg.py b/pyborg.py new file mode 100644 index 0000000..fa48c59 --- /dev/null +++ b/pyborg.py @@ -0,0 +1,1013 @@ +# +# PyBorg: The python AI bot. +# +# Copyright (c) 2000, 2006 Tom Morton, Sebastien Dailly +# +# +# This bot was inspired by the PerlBorg, by Eric Bock. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# Tom Morton +# Seb Dailly +# + +from random import * +import string +import sys +import os +import marshal # buffered marshal is bloody fast. wish i'd found this before :) +import struct +import time +import cfgfile +import zipfile +import thread + +def filter_message(message, aliases): + """ + Filter a message body so it is suitable for learning from and + replying to. This involves removing confusing characters, + padding ? and ! with ". " so they also terminate lines + and converting to lower case. + """ + # to lowercase + message = string.lower(message) + + # remove garbage + message = string.replace(message, "\"", "") # remove "s + message = string.replace(message, "\n", " ") # remove newlines + message = string.replace(message, "\r", " ") # remove carriage returns + + # remove matching brackets (unmatched ones are likely smileys :-) *cough* + # should except out when not found. + index = 0 + try: + while 1: + index = string.index(message, "(", index) + # Remove matching ) bracket + i = string.index(message, ")", index+1) + message = message[0:i]+message[i+1:] + # And remove the ( + message = message[0:index]+message[index+1:] + except ValueError, e: + pass + + #remove special irc fonts chars + message = message[message.rfind("\x02")+1:] + message = message[message.rfind("\xa0")+1:] + + message = string.replace(message, "?", " ? ") + message = string.replace(message, "!", " ! ") + message = string.replace(message, ".", " . ") + message = string.replace(message, ",", " , ") + message = string.replace(message, "'", " ' ") + message = string.replace(message, ":", " : ") + + # Find ! and ? and append full stops. + message = string.replace(message, "? ", "?. ") + message = string.replace(message, "! ", "!. ") + + + words = string.split(message) + for x in range(0, len(words)): + #is there aliases ? + for z in aliases.keys(): + for y in aliases[z]: + if y == words[x]: + words[x] = z + message = string.join(words, " ") + return message + + +class pyborg: + ver_string = "I am a version 1.1.0 PyBorg" + + # Main command list + commandlist = "Pyborg commands:\n!checkdict, !contexts, !help, !known, !learning, !rebuilddict, \ +!replace, !unlearn, !purge, !version, !words, !limit, !alias, !save, !censor, !uncensor, !owner" + commanddict = { + "help": "Owner command. Usage: !help [command]\nPrints information about using a command, or a list of commands if no command is given", + "version": "Usage: !version\nDisplay what version of Pyborg we are running", + "words": "Usage: !words\nDisplay how many words are known", + "known": "Usage: !known word1 [word2 [...]]\nDisplays if one or more words are known, and how many contexts are known", + "contexts": "Owner command. Usage: !contexts \nPrint contexts containing ", + "unlearn": "Owner command. Usage: !unlearn \nRemove all occurances of a word or phrase from the dictionary. For example '!unlearn of of' would remove all contexts containing double 'of's", + "purge": "Owner command. Usage: !purge [number]\nRemove all occurances of the words that appears in less than contexts", + "replace": "Owner command. Usage: !replace \nReplace all occurances of word in the dictionary with ", + "learning": "Owner command. Usage: !learning [on|off]\nToggle bot learning. Without arguments shows the current setting", + "checkdict": "Owner command. Usage: !checkdict\nChecks the dictionary for broken links. Shouldn't happen, but worth trying if you get KeyError crashes", + "rebuilddict": "Owner command. Usage: !rebuilddict\nRebuilds dictionary links from the lines of known text. Takes a while. You probably don't need to do it unless your dictionary is very screwed", + "censor": "Owner command. Usage: !censor [word1 [...]]\nPrevent the bot using one or more words. Without arguments lists the currently censored words", + "uncensor": "Owner command. Usage: !uncensor word1 [word2 [...]]\nRemove censorship on one or more words", + "limit": "Owner command. Usage: !limit [number]\nSet the number of words that pyBorg can learn", + "alias": "Owner command. Usage: !alias : Show the differents aliases\n!alias : show the words attached to this alias\n!alias : link the word to the alias", + "owner": "Usage : !owner password\nAdd the user in the owner list" + } + + def __init__(self): + """ + Open the dictionary. Resize as required. + """ + # Attempt to load settings + self.settings = cfgfile.cfgset() + self.settings.load("pyborg.cfg", + { "num_contexts": ("Total word contexts", 0), + "num_words": ("Total unique words known", 0), + "max_words": ("max limits in the number of words known", 6000), + "learning": ("Allow the bot to learn", 1), + "ignore_list":("Words that can be ignored for the answer", []), + "censored": ("These words may be learnt but will never be used", []), + "num_aliases":("Total of aliases known", 0), + "aliases": ("A list of similars words", {}) + } ) + + # Read the dictionary + print "Reading dictionary..." + try: + zfile = zipfile.ZipFile('archive.zip','r') + for filename in zfile.namelist(): + data = zfile.read(filename) + file = open(filename, 'w+b') + file.write(data) + file.close() + except (EOFError, IOError), e: + print "no zip found" + try: + f = open("words.dat", "rb") + s = f.read() + f.close() + self.words = marshal.loads(s) + del s + f = open("lines.dat", "rb") + s = f.read() + f.close() + self.lines = marshal.loads(s) + del s + except (EOFError, IOError), e: + # Create mew database + self.words = {} + self.lines = {} + print "Error reading saves. New database created." + + # Is a resizing required? + if len(self.words) != self.settings.num_words: + print "Updating dictionary information..." + self.settings.num_words = len(self.words) + num_contexts = 0 + # Get number of contexts + for x in self.lines.keys(): + num_contexts += len(string.split(self.lines[x][0])) + self.settings.num_contexts = num_contexts + # Save new values + self.settings.save() + + # Is an aliases update required ? + compteur = 0 + for x in self.settings.aliases.keys(): + compteur += len(self.settings.aliases[x]) + if compteur != self.settings.num_aliases: + self.settings.num_aliases = compteur + for x in self.settings.aliases.keys(): + for y in self.settings.aliases[x]: + if self.words.has_key(y): + print 'replace', y, ' with ', x + self.replace(y, x) + for x in self.words.keys(): + if not (x in self.settings.aliases.keys()) and x[0] == '~': + print "unlearn", x + self.settings.num_aliases -= 1 + self.unlearn(x) + print "unlearnd aliases", x + + + #unlearn words in the unlearn.txt file. + try: + f = open("unlearn.txt", "r") + while 1: + word = f.readline().strip('\n') + if word == "": + break + if self.words.has_key(word): + self.unlearn(word) + f.close() + except (EOFError, IOError), e: + # No words to unlearn + pass + + self.save_all() + + + + def save_all(self): + print "Writing dictionary..." + + try: + zfile = zipfile.ZipFile('archive.zip','r') + for filename in zfile.namelist(): + data = zfile.read(filename) + file = open(filename, 'w+b') + file.write(data) + file.close() + except (OSError, IOError), e: + print "no zip found. Is the programm launch for first time ?" + + + f = open("words.dat", "wb") + s = marshal.dumps(self.words) + f.write(s) + f.close() + f = open("lines.dat", "wb") + s = marshal.dumps(self.lines) + f.write(s) + f.close() + + #save the version + f = open("version", "w") + f.write("1.1.0") + f.close() + + + #zip the files + f = zipfile.ZipFile('archive.zip','w',zipfile.ZIP_DEFLATED) + f.write('words.dat') + f.write('lines.dat') + f.write('version') + f.close() + + try: + os.remove('words.dat') + os.remove('lines.dat') + os.remove('version') + except (OSError, IOError), e: + print "could not remove the files" + + # Save settings + self.settings.save() + + def process_msg(self, io_module, body, replyrate, learn, args, owner=0): + """ + Process message 'body' and pass back to IO module with args. + If owner==1 allow owner commands. + """ + + # add trailing space so sentences are broken up correctly + body = body + " " + + # Parse commands + if body[0] == "!": + self.do_commands(io_module, body, args, owner) + return + + # Filter out garbage and do some formatting + body = filter_message(body, self.settings.aliases) + + # Learn from input + if learn == 1: + self.learn(body) + + # Make a reply if desired + if randint(0, 99) < replyrate: + message = self.reply(body) + # single word reply: always output + if len(string.split(message)) == 1: + io_module.output(message, args) + return + # empty. do not output + if message == "": + return + # same as input. do not output +# if message == string.lower(string.strip(body)): +# return + # else output + if owner==0: + time.sleep(.2*len(message)) + io_module.output(message, args) + + def do_commands(self, io_module, body, args, owner): + """ + Respond to user comands. + """ + msg = "" + + command_list = string.split(body) + command_list[0] = string.lower(command_list[0]) + + # Guest commands. + + # Version string + if command_list[0] == "!version": + msg = self.ver_string + + # How many words do we know? + elif command_list[0] == "!words": + num_w = self.settings.num_words + num_c = self.settings.num_contexts + num_l = len(self.lines) + if num_w != 0: + num_cpw = num_c/float(num_w) # contexts per word + else: + num_cpw = 0.0 + msg = "I know "+str(num_w)+" words ("+str(num_c)+" contexts, %.2f" % num_cpw+" per word), "+str(num_l)+" lines." + + # Do i know this word + elif command_list[0] == "!known": + if len(command_list) == 2: + # single word specified + word = string.lower(command_list[1]) + if self.words.has_key(word): + c = len(self.words[word]) + msg = word+" is known ("+`c`+" contexts)" + else: + msg = word+" is unknown." + elif len(command_list) > 2: + # multiple words. + words = [] + for x in command_list[1:]: + words.append(string.lower(x)) + msg = "Number of contexts: " + for x in words: + if self.words.has_key(x): + c = len(self.words[x]) + msg += x+"/"+str(c)+" " + else: + msg += x+"/0 " + + # Owner commands + if owner == 1: + # Save dictionary + if command_list[0] == "!save": + self.save_all() + msg = "Dictionary saved" + + # Command list + elif command_list[0] == "!help": + if len(command_list) > 1: + # Help for a specific command + cmd = string.lower(command_list[1]) + dic = None + if cmd in self.commanddict.keys(): + dic = self.commanddict + elif cmd in io_module.commanddict.keys(): + dic = io_module.commanddict + if dic: + for i in string.split(dic[cmd], "\n"): + io_module.output(i, args) + else: + msg = "No help on command '%s'" % cmd + else: + for i in string.split(self.commandlist, "\n"): + io_module.output(i, args) + for i in string.split(io_module.commandlist, "\n"): + io_module.output(i, args) + + # Change the max_words setting + elif command_list[0] == "!limit": + msg = "The max limit is " + if len(command_list) == 1: + msg += str(self.settings.max_words) + else: + limit = int(string.lower(command_list[1])) + self.settings.max_words = limit + msg += "now " + command_list[1] + + + # Check for broken links in the dictionary + elif command_list[0] == "!checkdict": + t = time.time() + num_broken = 0 + num_bad = 0 + for w in self.words.keys(): + wlist = self.words[w] + + for i in xrange(len(wlist)-1, -1, -1): + line_idx, word_num = struct.unpack("iH", wlist[i]) + + # Nasty critical error we should fix + if not self.lines.has_key(line_idx): + print "Removing broken link '%s' -> %d" % (w, line_idx) + num_broken = num_broken + 1 + del wlist[i] + else: + # Check pointed to word is correct + split_line = string.split(self.lines[line_idx][0]) + if split_line[word_num] != w: + print "Line '%s' word %d is not '%s' as expected." % (self.lines[line_idx][0], word_num, w) + num_bad = num_bad + 1 + del wlist[i] + if len(wlist) == 0: + del self.words[w] + self.settings.num_words = self.settings.num_words - 1 + print "\""+w+"\" vaped totally" + + msg = "Checked dictionary in %0.2fs. Fixed links: %d broken, %d bad." % (time.time()-t, num_broken, num_bad) + + # Rebuild the dictionary by discarding the word links and + # re-parsing each line + elif command_list[0] == "!rebuilddict": + if self.settings.learning == 1: + t = time.time() + + old_lines = self.lines + old_num_words = self.settings.num_words + old_num_contexts = self.settings.num_contexts + + self.words = {} + self.lines = {} + self.settings.num_words = 0 + self.settings.num_contexts = 0 + + for k in old_lines.keys(): + self.learn(old_lines[k][0], old_lines[k][1]) + #self.lines[k][1] = old_lines[k][1] + + msg = "Rebuilt dictionary in %0.2fs. Words %d (%+d), contexts %d (%+d)" % \ + (time.time()-t, + old_num_words, + self.settings.num_words - old_num_words, + old_num_contexts, + self.settings.num_contexts - old_num_contexts) + + #Remove rares words + elif command_list[0] == "!purge": + t = time.time() + + liste = [] + compteur = 0 + + if len(command_list) == 2: + # limite d'occurences à effacer + c_max = string.lower(command_list[1]) + else: + c_max = 0 + + c_max = int(c_max) + + for w in self.words.keys(): + + digit = 0 + char = 0 + for c in w: + if c.isalpha(): + char += 1 + if c.isdigit(): + digit += 1 + + + #Compte les mots inférieurs a cette limite + c = len(self.words[w]) + if c < 2 or ( digit and char ): + liste.append(w) + compteur += 1 + if compteur == c_max: + break + + if c_max < 1: + io_module.output(str(compteur)+" words to remove", args) + return + + #supprime les mots + for w in liste[0:]: + self.unlearn(w) + + msg = "Purge dictionary in %0.2fs. %d words removed" % \ + (time.time()-t, + compteur) + + # Change a typo in the dictionary + elif command_list[0] == "!replace": + if len(command_list) < 3: + return + old = string.lower(command_list[1]) + new = string.lower(command_list[2]) + msg = self.replace(old, new) + + # Print contexts [flooding...:-] + elif command_list[0] == "!contexts": + # This is a large lump of data and should + # probably be printed, not module.output XXX + + # build context we are looking for + context = string.join(command_list[1:], " ") + context = string.lower(context) + if context == "": + return + io_module.output("Contexts containing \""+context+"\":", args) + # Build context list + # Pad it + context = " "+context+" " + c = [] + # Search through contexts + for x in self.lines.keys(): + # get context + ctxt = self.lines[x][0] + # add leading whitespace for easy sloppy search code + ctxt = " "+ctxt+" " + if string.find(ctxt, context) != -1: + # Avoid duplicates (2 of a word + # in a single context) + if len(c) == 0: + c.append(self.lines[x][0]) + elif c[len(c)-1] != self.lines[x][0]: + c.append(self.lines[x][0]) + x = 0 + while x < 5: + if x < len(c): + io_module.output(c[x], args) + x += 1 + if len(c) == 5: + return + if len(c) > 10: + io_module.output("...("+`len(c)-10`+" skipped)...", args) + x = len(c) - 5 + if x < 5: + x = 5 + while x < len(c): + io_module.output(c[x], args) + x += 1 + + # Remove a word from the vocabulary [use with care] + elif command_list[0] == "!unlearn": + # build context we are looking for + context = string.join(command_list[1:], " ") + context = string.lower(context) + if context == "": + return + print "Looking for: "+context + # Unlearn contexts containing 'context' + t = time.time() + self.unlearn(context) + # we don't actually check if anything was + # done.. + msg = "Unlearn done in %0.2fs" % (time.time()-t) + + # Query/toggle bot learning + elif command_list[0] == "!learning": + msg = "Learning mode " + if len(command_list) == 1: + if self.settings.learning == 0: + msg += "off" + else: + msg += "on" + else: + toggle = string.lower(command_list[1]) + if toggle == "on": + msg += "on" + self.settings.learning = 1 + else: + msg += "off" + self.settings.learning = 0 + + # add a word to the 'censored' list + elif command_list[0] == "!censor": + # no arguments. list censored words + if len(command_list) == 1: + if len(self.settings.censored) == 0: + msg = "No words censored" + else: + msg = "I will not use the word(s) " + msg += string.join(self.settings.censored, ", ") + # add every word listed to censored list + else: + for x in range(1, len(command_list)): + if command_list[x] in self.settings.censored: + msg += command_list[x] + " is already censored" + else: + self.settings.censored.append(string.lower(command_list[x])) + self.unlearn(command_list[x]) + msg += "done" + msg = msg + "\n" + + # remove a word from the censored list + elif command_list[0] == "!uncensor": + # Remove everyone listed from the ignore list + # eg !unignore tom dick harry + for x in range(1, len(command_list)): + try: + self.settings.censored.remove(string.lower(command_list[x])) + msg = "done" + except ValueError, e: + pass + + elif command_list[0] == "!alias": + # no arguments. list aliases words + if len(command_list) == 1: + if len(self.settings.aliases) == 0: + msg = "No aliases" + else: + msg = "I will alias the word(s) " + msg += string.join(self.settings.aliases.keys()) + # add every word listed to alias list + elif len(command_list) == 2: + if command_list[1][0] != '~': + command_list[1] = '~' + command_list[1] + if command_list[1] in self.settings.aliases.keys(): + msg = "Thoses words :" + string.join(self.settings.aliases[command_list[1]]) +\ + " are aliases to " + command_list[1] + else: + msg = "The alias " + command_list[1][1:] + " is not known" + elif len(command_list) > 2: + #create the aliases + msg = "The words : " + if command_list[1][0] != '~': + command_list[1] = '~' + command_list[1] + if not(command_list[1] in self.settings.aliases.keys()): + self.settings.aliases[command_list[1]] = [command_list[1][1:]] + self.replace(command_list[1][1:], command_list[1]) + msg += command_list[1][1:] + " " + for x in range(2, len(command_list)): + msg += command_list[x] + " " + self.settings.aliases[command_list[1]].append(command_list[x]) + #replace each words by his alias + self.replace(command_list[x], command_list[1]) + msg += "have been aliases to " + command_list[1] + + # Quit + elif command_list[0] == "!quit": + # Close the dictionary + self.save_all() + + f = open("words.txt", "w") + + # write each words known + wordlist = [] + #Sort the list befor to export + for key in self.words.keys(): + wordlist.append([key, len(self.words[key])]) + wordlist.sort(lambda x,y: cmp(x[1],y[1])) + + map( (lambda x: f.write(str(x[0])+"\n") ), wordlist) + + f.close() + + sys.exit() + + # Save changes + self.settings.save() + + if msg != "": + io_module.output(msg, args) + + def replace(self, old, new): + """ + Replace all occuraces of 'old' in the dictionary with + 'new'. Nice for fixing learnt typos. + """ + try: + pointers = self.words[old] + except KeyError, e: + return old+" not known." + changed = 0 + + for x in pointers: + # pointers consist of (line, word) to self.lines + l, w = struct.unpack("iH", x) + line = string.split(self.lines[l][0]) + number = self.lines[l][1] + if line[w] != old: + # fucked dictionary + print "Broken link: "+str(x)+" "+self.lines[l][0] + continue + else: + line[w] = new + self.lines[l][0] = string.join(line, " ") + self.lines[l][1] += number + changed += 1 + + if self.words.has_key(new): + self.settings.num_words -= 1 + self.words[new].extend(self.words[old]) + else: + self.words[new] = self.words[old] + del self.words[old] + return `changed`+" instances of "+old+" replaced with "+new + + def unlearn(self, context): + """ + Unlearn all contexts containing 'context'. If 'context' + is a single word then all contexts containing that word + will be removed, just like the old !unlearn + """ + # Pad thing to look for + # We pad so we don't match 'shit' when searching for 'hit', etc. + context = " "+context+" " + # Search through contexts + # count deleted items + dellist = [] + # words that will have broken context due to this + wordlist = [] + for x in self.lines.keys(): + # get context. pad + c = " "+self.lines[x][0]+" " + if string.find(c, context) != -1: + # Split line up + wlist = string.split(self.lines[x][0]) + # add touched words to list + for w in wlist: + if not w in wordlist: + wordlist.append(w) + dellist.append(x) + del self.lines[x] + words = self.words + unpack = struct.unpack + # update links + for x in wordlist: + word_contexts = words[x] + # Check all the word's links (backwards so we can delete) + for y in xrange(len(word_contexts)-1, -1, -1): + # Check for any of the deleted contexts + if unpack("iH", word_contexts[y])[0] in dellist: + del word_contexts[y] + self.settings.num_contexts = self.settings.num_contexts - 1 + if len(words[x]) == 0: + del words[x] + self.settings.num_words = self.settings.num_words - 1 + print "\""+x+"\" vaped totally" + + def reply(self, body): + """ + Reply to a line of text. + """ + # split sentences into licommand_list[0] = string.lower(command_list[0])st of words + _words = string.split(body, ". ") + words = [] + for i in _words: + words += string.split(i) + del _words + + if len(words) == 0: + return "" + + #remove words on the ignore list + words = filter((lambda x: x not in self.settings.ignore_list and not x.isdigit() ), words) + + # Find rarest word (excluding those unknown) + index = [] + known = -1 + #The word has to bee seen in already 3 contexts differents for being choosen + known_min = 3 + for x in range(0, len(words)): + if self.words.has_key(words[x]): + k = len(self.words[words[x]]) + else: + continue + if (known == -1 or k < known) and k > known_min: + index = [words[x]] + known = k + continue + elif k == known: + index.append(words[x]) + continue + # Index now contains list of rarest known words in sentence + if len(index)==0: + return "" + word = index[randint(0, len(index)-1)] + + # Build sentence backwards from "chosen" word + sentence = [word] + done = 0 + while done == 0: + #create a dictionary wich will contain all the words we can found before the "chosen" word + pre_words = {"" : 0} + #this is for prevent the case when we have an ignore_listed word + word = str(string.split(sentence[0], " ")[0]) + for x in range(0, len(self.words[word]) -1 ): + l, w = struct.unpack("iH", self.words[word][x]) + context = self.lines[l][0] + num_context = self.lines[l][1] + cwords = string.split(context) + #if the word is not the first of the context, look the previous one + if cwords[w] != word: + print context + if w: + #look if we can found a pair with the choosen word, and the previous one + if len(sentence) > 1 and len(cwords) > w+1: + if sentence[1] != cwords[w+1]: + continue + + #if the word is in ignore_list, look the previous word + look_for = cwords[w-1] + if look_for in self.settings.ignore_list and w > 1: + look_for = cwords[w-2]+" "+look_for + + #saves how many times we can found each word + if not(pre_words.has_key(look_for)): + pre_words[look_for] = num_context + else : + pre_words[look_for] += num_context + + + else: + pre_words[""] += num_context + + #Sort the words + liste = pre_words.items() + liste.sort(lambda x,y: cmp(y[1],x[1])) + + numbers = [liste[0][1]] + for x in range(1, len(liste) ): + numbers.append(liste[x][1] + numbers[x-1]) + + #take one them from the list ( randomly ) + mot = randint(0, numbers[len(numbers) -1]) + for x in range(0, len(numbers)): + if mot <= numbers[x]: + mot = liste[x][0] + break + + #if the word is already choosen, pick the next one + while mot in sentence: + x += 1 + if x >= len(liste) -1: + mot = '' + mot = liste[x][0] + + mot = string.split(mot, " ") + mot.reverse() + if mot == ['']: + done = 1 + else: + map( (lambda x: sentence.insert(0, x) ), mot ) + + pre_words = sentence + sentence = sentence[-2:] + + # Now build sentence forwards from "chosen" word + + #We've got + #cwords: ... cwords[w-1] cwords[w] cwords[w+1] cwords[w+2] + #sentence: ... sentence[-2] sentence[-1] look_for look_for ? + + #we are looking, for a cwords[w] known, and maybe a cwords[w-1] known, what will be the cwords[w+1] to choose. + #cwords[w+2] is need when cwords[w+1] is in ignored list + + + done = 0 + while done == 0: + #create a dictionary wich will contain all the words we can found before the "chosen" word + post_words = {"" : 0} + word = str(string.split(sentence[-1], " ")[-1]) + for x in range(0, len(self.words[word]) ): + l, w = struct.unpack("iH", self.words[word][x]) + context = self.lines[l][0] + num_context = self.lines[l][1] + cwords = string.split(context) + #look if we can found a pair with the choosen word, and the next one + if len(sentence) > 1: + if sentence[len(sentence)-2] != cwords[w-1]: + continue + else: + #print context + pass + + if w < len(cwords)-1: + #if the word is in ignore_list, look the previous word + look_for = cwords[w+1] + if look_for in self.settings.ignore_list and w < len(cwords) -2: + look_for = look_for+" "+cwords[w+2] +# if not(post_words.has_key(look_for)): +# print look_for + + if not(post_words.has_key(look_for)): + post_words[look_for] = num_context + else : + post_words[look_for] += num_context + else: + post_words[""] += num_context + #Sort the words + liste = post_words.items() + liste.sort(lambda x,y: cmp(y[1],x[1])) + numbers = [liste[0][1]] + + for x in range(1, len(liste) ): + numbers.append(liste[x][1] + numbers[x-1]) + + #take one them from the list ( randomly ) + mot = randint(0, numbers[len(numbers) -1]) + for x in range(0, len(numbers)): + if mot <= numbers[x]: + mot = liste[x][0] + break + + x = -1 + sortie = 0 + while mot in sentence: + x += 1 + if x >= len(liste) -1: + mot = '' + break + mot = liste[x][0] + + + mot = string.split(mot, " ") + if mot == ['']: + done = 1 + else: + map( (lambda x: sentence.append(x) ), mot ) + + sentence = pre_words[:-2] + sentence + + for x in range(0, len(sentence)): + if sentence[x][0] == "~": + #It is an aliase, we replace it by a word + if self.settings.aliases.has_key(sentence[x]): + sentence[x] = self.settings.aliases[sentence[x]][randint(0, len(self.settings.aliases[sentence[x]])-1)] + + # Sentence is finished. build into a string + return string.join(sentence, " ") + + def learn(self, body, num_context=1): + """ + Lines should be cleaned (filter_message()) before passing + to this. + """ + + def learn_line(self, body, num_context): + """ + Learn from a sentence. + """ + + words = string.split(body) + # Ignore sentences of < 1 words XXX was <3 + if len(words) < 1: + return + + voyelles = "aeiouyéàè" + for x in range(0, len(words)): + + nb_voy = 0 + digit = 0 + char = 0 + for c in words[x]: + if c in voyelles: + nb_voy += 1 + if c.isalpha(): + char += 1 + if c.isdigit(): + digit += 1 + + if len(words[x]) > 13 \ + or ( ((nb_voy*100) / len(words[x]) < 26) and len(words[x]) > 5 ) \ + or ( words[x] in self.settings.censored ) \ + or ( char and digit ) \ + or self.words.has_key(words[x]) == 0 and self.settings.learning == 0: + #if one word as more than 13 characters, don't learn the sentence + # ( in french, this represent 12% of the words ) + #and d'ont learn words where there are less than 25% of voyells + #don't learn the sentence if one word is censored + #don't learn too if there are digits and char in the word + #same thing if one word is unknow when learning is off + return + elif ( "-" in words[x] or "_" in words[x] ) : + words[x]="#nick" + + + num_w = self.settings.num_words + if num_w != 0: + num_cpw = self.settings.num_contexts/float(num_w) # contexts per word + else: + num_cpw = 0 + + cleanbody = string.join(words, " ") + + hashval = hash(cleanbody) + + # Check context isn't already known + # Hash collisions we don't care about. 2^32 is big :-) + if not(num_cpw > 50 and self.settings.learning == 0): + if not self.lines.has_key(hashval): + + self.lines[hashval] = [cleanbody, num_context] + # Add link for each word + for x in range(0, len(words)): + if self.words.has_key(words[x]): + # Add entry. (line number, word number) + self.words[words[x]].append(struct.pack("iH", hashval, x)) + else: + self.words[words[x]] = [ struct.pack("iH", hashval, x) ] + self.settings.num_words += 1 + self.settings.num_contexts += 1 + else : + self.lines[hashval][1] += 1 + + #is max_words reached ? + if self.settings.num_words >= self.settings.max_words: + #yes, don't learn more + self.settings.learning = 0 + + # Split body text into sentences and parse them + # one by one. + list = string.split(body, ". ") + for x in list: + learn_line(self, x, num_context)