Skip to content
Snippets Groups Projects
irc.py 15.6 KiB
Newer Older
import socket, os, threading, re
FurryHead's avatar
FurryHead committed
import plugins

class PluginManager(object):
    def __init__(self, server):
        self.server = server
FurryHead's avatar
FurryHead committed
        self.plugins = {}
        self.prnt = server.prnt
FurryHead's avatar
FurryHead committed
        self.handlers = { 
            "join" : [ ],
            "part" : [ ],
            "quit" : [ ],
            "message" : [ ],
            "connect" : [ ],
            "disconnect": [ ],
            "action" : [ ],
            "nick" : [ ],
FurryHead's avatar
FurryHead committed
            "mode" : [ ],
            "command" : { },
        }
    
    def handle(self, event, handler_func, handler_commands=None):
        event = event.lower()
        if event == "command":
            if handler_commands is not None:
                for command in handler_commands:
                    self.handlers[event][command.lower()] = handler_func
        else:
            self.handlers[event].append(handler_func)
    
    def unhandle(self, event, handler_func, handler_commands=None):
        event = event.lower()
        if event == "command":
            handlers = {}
            for command, handler in self.handlers[event]:
                if handler != handler_func and not command.lower() in handler_commands:
                    handlers.insert(0, handler)
                    
            self.handlers[event] = handlers
        else:
            handlers = []
            for handler in self.handlers[event]:
                if handler != handler_func:
                    handlers.insert(0, handler)
                    
            self.handlers[event] = handlers
        
    def event(self, eventName, *args):
        eventName = eventName.lower()
        if eventName == "command":
            handler = None
            try:
                handler = self.handlers[eventName][args[2].lower()]
            except KeyError,ke: 
                return #no such command
                
            try:
                handler(*args)
            except Exception, e:
                s = handler.__self__
                self.prnt("Plugin "+s.__class__.__name__+" "+e.__class__.__name__+": "+e.__str__())
FurryHead's avatar
FurryHead committed
        else:
            for handler in self.handlers[eventName]:
                try:
                    handler(*args)
                except Exception, e:
                    s = handler.__self__
                    self.prnt("Plugin "+s.__class__.__name__+" "+e.__class__.__name__+": "+e.__str__())
FurryHead's avatar
FurryHead committed
    
    def loadedPlugin(self, pluginName):
        return pluginName.lower() in self.plugins
    
    def pluginExists(self, pluginName):
        return plugins.getPlugin(pluginName.lower()) is not None
        
    def loadPlugin(self, pluginName):
        plugins.refresh()
        if not self.loadedPlugin(pluginName.lower()):
            if not self.pluginExists(pluginName.lower()):
                err = "No such plugin."
                return err
            
            pClass = None
            try:
                pClass = plugins.getPlugin(pluginName.lower())
            except Exception, e:
                err = "Cannot load plugin "+pluginName+" ("+e.__class__.__name__+": "+e.__str__()+")"
FurryHead's avatar
FurryHead committed
                return err
            
            try:
                self.plugins[pluginName.lower()] = pClass(self.server)
            except Exception, e:
                err = "Could not initialize "+pluginName+" ("+e.__class__.__name__+": "+e.__str__()+")"
FurryHead's avatar
FurryHead committed
                return err
                
        else:
            err = "Module has already been loaded."
            return err
    
    def getPlugin(self, pluginName): 
        try:
            return self.plugins[pluginName.lower()]
        except KeyError:
            return None
    
    def reloadPlugin(self, pluginName):
        errs = {}
        
        err = self.unloadPlugin(pluginName.lower())
        if err is not None: 
            errs["unload"] = err
            
        err = self.loadPlugin(pluginName.lower())
        if err is not None: 
            errs["load"] = err
        
        return errs
    
    def unloadPlugin(self, pluginName):
        if self.loadedPlugin(pluginName.lower()):
            inst = self.plugins[pluginName.lower()]
            for event, eList in self.handlers.items():
                if event == "command":
FurryHead's avatar
FurryHead committed
                    for cmd,func in eList.items():
                        if func.__self__ == inst:
                            destructor = getattr(inst, "__del__", None)
                            if destructor is not None:
                                destructor()
                        else:
                            newList[cmd] = func
                    self.handlers[event] = newList
FurryHead's avatar
FurryHead committed
                else:
                    newList = []
                    for func in eList:
FurryHead's avatar
FurryHead committed
                        if func.__self__ == inst:
                            destructor = getattr(inst, "__del__", None)
                            if destructor is not None:
                                destructor()
                        else:
                            newList.append(func)
                    self.handlers[event] = newList
FurryHead's avatar
FurryHead committed
                            
            del self.plugins[pluginName.lower()]
        else:
            err = "Plugin "+pluginName+" is not loaded."
            return err
    
    def loadAllPlugins(self):
        errs = {}
        plugins = [k for k in self.server.config["plugins"].split(",") if k != '']
FurryHead's avatar
FurryHead committed
        for plugin in plugins:
            err = self.loadPlugin(plugin.lower())
            if err is not None:
                errs[plugin] = err
                
        return errs
    
    def unloadAllPlugins(self):
        errs = {}
        for plugin in self.plugins.copy().keys():
FurryHead's avatar
FurryHead committed
            err = self.unloadPlugin(plugin.lower())
            if err is not None:
                errs[plugin] = err
            
        return errs

class IRC(threading.Thread):
    def __init__(self, config):
        super(IRC, self).__init__()
        self.pluginManager = PluginManager(self)
        self.config = config
        self.handle = self.pluginManager.handle
        self.unhandle = self.pluginManager.unhandle
        self.handlers = self.pluginManager.handlers
        self.plugins = self.pluginManager.plugins
        self.getPlugin = self.pluginManager.getPlugin
        errs = self.pluginManager.loadAllPlugins()
        if errs:
            for k,v in errs.items():
                self.prnt("Plugin "+k+" error loading: "+v)
                
FurryHead's avatar
FurryHead committed
        self.running = False
FurryHead's avatar
FurryHead committed
        
    def _readline(self):
        ret = []
        while True:
            c = self.conn.recv(1)
            if c == '\n':
                break
            else:
                if c != '\r':
                    ret.append(c)
        return "".join(ret)
           
    def run(self):
        self.conn = socket.socket()
        self.conn.settimeout(60)
        try:
            self.conn.connect((self.config["host"], int(self.config["port"])))
            self.goodstart = True
        except socket.error:
            self.prnt("Cannot connect to server.")
            self.goodstart = False
        
        if self.goodstart:
            ident = self.config["ident"] if self.config["ident"] != "" else self.config["nickname"]
            self.sendLine("USER "+ident+" * * *")
            self.sendLine("NICK "+self.config["nickname"])
        self.running = self.goodstart
FurryHead's avatar
FurryHead committed
        while self.running:
            try:
                data = self._readline()
            except socket.timeout:
                self.sendLine("PING BACK")
                try:
                    data = self._readline()
                    continue
                except socket.timeout:
                    raise IOError("Connection was broken.")
            
            if not self.running: continue
            
            if data == "":
                self.prnt("Connection lost to server.")
                self.running = False
                continue
            
FurryHead's avatar
FurryHead committed
            words = data.split(" ")
            
            if words[0] == "PING":
                # The server pinged us, we need to pong or we'll be disconnected
                # (make sure to send whatever follows the PING, in case they send a random hash)
                self.sendLine("PONG " + data[5:])
                continue
            elif words[0] == "ERROR":
                #there was an error, we probably had a KeyboardInterrupt
                self.prnt("Connection lost to server.")
                self.running = False
                continue
            # Takes the ':Nick!Ident@Host' chunk and assigns (nick,ident,host) to user
            chunk = words[0].split(":")[1]
            user = (chunk.split("!")[0], chunk.split("!")[1].split("@")[0], chunk.split("!")[0].split("@")[1])
            if words[1] == "433":
                #There was a nick collision
                self.config["nickname"] = self.config["nickname"] + "_"
                self.sendLine("NICK "+self.config["nickname"])
            elif words[1] == "422" or words[1] == "376":
FurryHead's avatar
FurryHead committed
                # We successfully logged in, do post-login procedures
                if self.config["ns_name"] != "" and self.config["ns_pass"] != "":
                    self.doMessage("NickServ", "IDENTIFY "+self.config["ns_name"]+" "+self.config["ns_pass"])
                
                self.pluginManager.event("connect", self.config["host"])
                
                for chan in [k for k in self.config["channels"].split(",") if k != '']:
FurryHead's avatar
FurryHead committed
                    self.doJoin(chan)
            
            elif words[1] == "353":
                #user list, if it's large, we only got part of it.
                words = [k for k in data.replace(" =", "").split(" ") if k != '']
                if self.userlist.get(words[3], None) is None:
                    self.userlist[words[3]] = []
                
                userlist = [u.strip(":") for u in words[3:]]
                self.userlist[words[3]] += userlist
            
FurryHead's avatar
FurryHead committed
            elif words[1] == "PRIVMSG":
                # We got a message
                channel = (words[2] if words[2] != self.config["nickname"] else user[0])
FurryHead's avatar
FurryHead committed
                message = data[data.find(":", data.find(channel[(channel.find("-") == -1 and 1 or channel.find("-")):]))+1:]
                if message.find("\x01ACTION") == 0:
                    # String was found, it's an action
                    self.pluginManager.event("action", channel, user, message[8:-1])
                elif message.find(self.config["comchar"]) == 0:
                    # String was found, it's a command!
                    args = message.split(" ")
                    cmd = args[0][len(self.config["comchar"]):]
                    args.pop(0)
                    self.pluginManager.event("message", channel, user, message)
                    self.pluginManager.event("command", channel, user, cmd.lower(), args)
FurryHead's avatar
FurryHead committed
                else:
                    if re.match("^"+self.config["nickname"]+"[^A-Za-z0-9] .+$", message):
                        #Someone addressed us, it's a command (probably)
                        args = message.split(" ")
                        cmd = args[1]
                        args = args[2:]
                        self.pluginManager.event("message", channel, user, message)
                        self.pluginManager.event("command", channel, user, cmd.lower(), args)
                    else:
                        #Nothing was found, it has to be a message
                        self.pluginManager.event("message", channel, user, message)
FurryHead's avatar
FurryHead committed
            
            elif words[1] == "JOIN":
                # Someone joined a channel that we're in
                if user[0] != self.config["nickname"]:
FurryHead's avatar
FurryHead committed
                    self.pluginManager.event("join", words[2].strip(":"), user)
                    self.userlist[words[2].strip(":")].append(user)
FurryHead's avatar
FurryHead committed
            
            elif words[1] == "PART":
                if user != self.config["nickname"]:
                    # Someone parted a channel we're in
                    if user in self.userlist[words[2].strip(":")]:
                        self.userlist[words[2]].remove(user)
                    self.pluginManager.event("part", words[2].strip(":"), user, " ".join(words[3:]))
FurryHead's avatar
FurryHead committed
            
            elif words[1] == "QUIT":
                # Someone quit the server
                for k in self.userlist.keys():
                    if user in self.userlist[k]:
                        self.userlist[k].remove(user)
FurryHead's avatar
FurryHead committed
                self.pluginManager.event("quit", user, " ".join(words[2:])[1:]) 
            
            elif words[1] == "NICK":
                # Someone changed their nickname 
                for k in self.userlist.keys():
                    if user in self.userlist[k]:
                        self.userlist[k].remove(user)
                        self.userlist[k].append(words[2].strip(":"))
FurryHead's avatar
FurryHead committed
                self.pluginManager.event("nick", user, words[2].strip(":"))
            
            elif words[1] == "MODE":
                # Someone set a mode
                try:
                    self.pluginManager.event("mode", words[2], user, words[3], words[4])
                except IndexError: # words[4] is not valid, it wasn't set on a user
                    self.pluginManager.event("mode", words[2], user, words[3], "")
            
            elif words[1] == "KICK":
                #someone kicked someone
                if self.userlist.get(words[2].lower(), None) is not None:
                    self.userlist[words[2].lower()].remove(words[3])
                    
                self.pluginManager.event("kick", words[2], user, words[3], words[4][1:])
FurryHead's avatar
FurryHead committed
        
        #end of loop
        if self.goodstart:
            self.pluginManager.event("disconnect", self.config["host"])
FurryHead's avatar
FurryHead committed
        
    def prnt(self, line):
        print("["+self.config["host"]+"] "+line)
    
    def sendLine(self, line):
        try:
            self.conn.send(line+"\r\n")
        except socket.error:
            self.prnt("Connection lost to server.")
            exit()
FurryHead's avatar
FurryHead committed
    
    def doMessage(self, channel, message):
        self.sendLine("PRIVMSG "+channel+" :"+message)
        self.pluginManager.event("message", channel, self.config["nickname"], message)
        
    def doAction(self, channel, action):
        self.sendLine("PRIVMSG "+channel+" :\x01ACTION "+action+" \x01")
        self.pluginManager.event("action", channel, self.config["nickname"], action)
    
    def doQuit(self, message=""):
        self.sendLine("QUIT :" + message)
        self.pluginManager.event("quit", self.config["nickname"], message)
FurryHead's avatar
FurryHead committed
        self.running = False
    
    def doNotice(self, user, message):
        self.sendLine("NOTICE "+user+" :"+message)
    
    def doNick(self, newnick):
        self.sendLine("NICK " + newnick)
        self.pluginManager.event("nick", self.config["nickname"], newnick)
        self.config["nickname"] = newnick
    
    def doJoin(self, channel):
        self.sendLine("JOIN "+channel)
        self.pluginManager.event("join", channel, self.config["nickname"])

    def doKick(self, channel, user, message=""):
        self.sendLine("KICK %s %s :%s" % (channel, user, message))
        self.pluginManager.event("kick", channel, self.config["nickname"], user, message)
    def doPart(self, channel, message=""):
FurryHead's avatar
FurryHead committed
        self.sendLine("PART "+channel)
        if channel in self.userlist.keys():
            del self.userlist[channel]
        self.pluginManager.event("part", channel, self.config["nickname"], message)
    def doMode(self, channel, mode, user=""):
        self.sendLine("MODE "+channel+" "+mode+user)
        self.pluginManager.event("mode", channel, self.config["nickname"], mode, user)