Skip to content
Snippets Groups Projects
irc.py 16.9 KiB
Newer Older
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed
#    guppy Copyright (C) 2010-2011 guppy team members.
#
#    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
#    This is free software, and you are welcome to redistribute it
#    under certain conditions; type `show c' for details.
#  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., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed

import socket, os, threading, re, asynchat, traceback, ssl
FurryHead's avatar
FurryHead committed
import plugins
    def __init__(self, user):
        super(User, self).__init__(self, user)
        if getattr(user, "nick", None) is not None:
            self.nick = user.nick
            self.ident = user.ident
            self.host = user.host
        else:
            self.nick = user
            self.ident = ""
            self.host = ""
FurryHead's avatar
FurryHead committed
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:
            if self.handlers.get(event, None) is None:
                self.handlers[event] = []
                
FurryHead's avatar
FurryHead committed
            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 self.handlers.get(eventName, None) is None: return
        
FurryHead's avatar
FurryHead committed
        if eventName == "command":
            handler = self.handlers[eventName].get(args[2].lower(), None)
            if handler is None:
FurryHead's avatar
FurryHead committed
                return #no such command
                
FurryHead's avatar
FurryHead committed
        else:
            for handler in self.handlers[eventName]:
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()):
                raise ValueError("No such plugin.")
            pClass = plugins.getPlugin(pluginName.lower())
            self.plugins[pluginName.lower()] = pClass(self.server)
FurryHead's avatar
FurryHead committed
    
    def getPlugin(self, pluginName): 
        try:
            return self.plugins[pluginName.lower()]
        except KeyError:
            return None
    
    def reloadPlugin(self, pluginName):
        self.unloadPlugin(pluginName.lower())
        self.loadPlugin(pluginName.lower())
FurryHead's avatar
FurryHead committed
    
    def unloadPlugin(self, pluginName):
        if self.loadedPlugin(pluginName.lower()): 
            # the plugin is loaded
            inst = self.plugins[pluginName.lower()] # the plugin object
            del self.plugins[pluginName.lower()] # delete plugins object from self.plugins
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed
            for event, eList in list(self.handlers.items()):
FurryHead's avatar
FurryHead committed
                if event == "command":
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed
                    for cmd,func in list(eList.items()):
FurryHead's avatar
FurryHead committed
                        if func.__self__ == inst:
                            destructor = getattr(inst, "destroy", None)
                            if destructor is not None and destructor not in destructors:
                        else:
                            newList[cmd] = func
                    self.handlers[event] = newList
                    for f in destructors:
                        f()
FurryHead's avatar
FurryHead committed
                else:
FurryHead's avatar
FurryHead committed
                        if func.__self__ == inst:
                            destructor = getattr(inst, "destroy", None)
                            if destructor is not None and destructor not in destructors:
                        else:
                            newList.append(func)
                    self.handlers[event] = newList
                    for f in destructors:
                        f()
            if getattr(inst, "destroy", None) != None: inst.destroy()
FurryHead's avatar
FurryHead committed
    def loadAllPlugins(self):
        plugins = [k for k in self.server.config["plugins"].split(",") if k != '']
FurryHead's avatar
FurryHead committed
        for plugin in plugins:
            self.server.prnt("Loading "+plugin)
            self.loadPlugin(plugin.lower())
FurryHead's avatar
FurryHead committed
    
    def unloadAllPlugins(self):
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed
        for plugin in list(self.plugins.copy().keys()):
            self.unloadPlugin(plugin.lower())
class IRC(asynchat.async_chat):
    def handle_error(self):
        '''Print a traceback when an error happens'''
        traceback.print_exc()
FurryHead's avatar
FurryHead committed
    def __init__(self, config):
        asynchat.async_chat.__init__(self)
FurryHead's avatar
FurryHead committed
        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()
                
        self.data = b""
        self.set_terminator(b"\r\n")
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed
        # check ipv6 flag (added in a recent version, may not exist)
        if ('ipv6' in self.config) and self.config["ipv6"].lower().startswith("y"): stype = socket.AF_INET6
        else: stype = socket.AF_INET
        # create socket
        tup = socket.getaddrinfo(self.config["host"], int(self.config["port"]), stype)[0][4]
        self.create_socket(stype, socket.SOCK_STREAM)
        # check ssl
        if self.config["use_ssl"].lower().startswith("y"):
            self.ssl = ssl.wrap_socket(self.socket)
            self.set_socket(self.ssl)
        # connect
        except ssl.SSLError as error:
            self.prnt('SSL Error connecting to server. (Are you using the right port?) Error message: %s' % error)
            return
        except socket.error as error:
            self.prnt('There was an error connecting to %s. %s' % (self.config["host"], error))
    def _getData(self):
        ret = self.data
        return ret
    
    def found_terminator(self):
        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:])
            return 
        
        # Takes the ':Nick!Ident@Host' chunk and assigns (nick,ident,host) to user
        user = None
        if words[0].find("!") != -1:
            user = User(words[0][words[0].find(":") + 1:words[0].find('!')])
            user.ident = words[0][words[0].find('!') + 1:words[0].find('@')]
            user.host = words[0][words[0].find('@') + 1:]
        else:
            #it's our nick, in the format ":NickName"
            user = User(words[0][words[0].find(":") + 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":
            # We successfully logged in, do post-login procedures
            if self.config["ns_name"] != "" and self.config["ns_pwd"] != "":
                self.doMessage("NickServ", "IDENTIFY "+self.config["ns_name"]+" "+self.config["ns_pwd"])
            self.pluginManager.event("connect", self.config["network"])
            for chan in [k for k in self.config["channels"].split(",") if k != '']:
                self.doJoin(chan)
        
        elif words[1] == "353":
            #user list, if it's large, we only got part of it.
            words = [k for k in str(data).replace(" =", " ").replace(" @", " ").replace(" +", " ").replace(" %", " ").replace(" &", " ").replace(" @", " ").replace(" ~", " ").split(" ") if k != '']
            if self.userlist.get(words[3], None) is None:
                self.userlist[words[3]] = []
            self.userlist[words[3]] += [u.strip(":") for u in words[3:]]
        
        elif words[1] == "PRIVMSG":
            # We got a message
            channel = (words[2] if words[2] != self.config["nickname"] else user)
            
            # Checks to see if message is a PM or not
            isPrivate = False
            if words[2] == self.config["nickname"]:
            	isPrivate = True
            	
            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)
            elif isPrivate:
            	# If its a private message, its probably a command
            	args = message.split(" ")
            	cmd = args[0]
            	args.pop(0)
            	self.pluginManager.event("message", channel, user, message)
            	self.pluginManager.event("command", channel, user, cmd.lower(), args)
                if re.match("^"+self.config["nickname"]+"[^A-Za-z0-9] .+$", message, flags=re.IGNORECASE):
                    #Someone addressed us, it's a command (probably)
FurryHead's avatar
FurryHead committed
                    args = message.split(" ")
                    cmd = args[1]
                    args = args[2:]
FurryHead's avatar
FurryHead committed
                    self.pluginManager.event("message", channel, user, message)
                    self.pluginManager.event("command", channel, user, cmd.lower(), args)
FurryHead's avatar
FurryHead committed
                else:
                    #Nothing was found, it has to be a message
                    self.pluginManager.event("message", channel, user, message)
        
        elif words[1] == "JOIN":
            # Someone joined a channel that we're in
            if user != self.config["nickname"]:
                self.pluginManager.event("join", words[2].strip(":"), user)
                self.userlist[words[2].strip(":")].append(user)
        
        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:]))
        
        elif words[1] == "QUIT":
            # Someone quit the server
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed
            for k in list(self.userlist.keys()):
                if user in self.userlist[k]:
                    self.userlist[k].remove(user)
            self.pluginManager.event("quit", user, " ".join(words[2:])[1:]) 
        
        elif words[1] == "NICK":
            # Someone changed their nickname 
Svetlana Tkachenko's avatar
Svetlana Tkachenko committed
            for k in list(self.userlist.keys()):
                if user in self.userlist[k]:
                    self.userlist[k].remove(user)
                    self.userlist[k].append(words[2].strip(":"))
            self.pluginManager.event("nick", user, words[2].strip(":"))
        
        elif words[1] == "MODE":
            if user != self.config["nickname"]:
                # 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:])
        
        elif words[1] == "NOTICE":
            self.pluginManager.event("notice", user, words[2], " ".join(words[3:]).strip(":"))
        else:
            self.pluginManager.event(words[1], data)
    def collect_incoming_data(self, data):
        self.data += data
    def handle_connect(self):
        self.config["ident"] = ident = self.config["ident"] if self.config["ident"] != "" else self.config["nickname"]
        # pass, nick, user - http://tools.ietf.org/html/rfc1459#section-4.1
        if self.config["srpass"] != "": self.sendLine("PASS "+self.config["srpass"])
        self.sendLine("NICK "+self.config["nickname"])
        self.sendLine("USER "+ident+" * * *")
    
    def handle_disconnect(self):
        self.pluginManager.event("disconnect", self.config["network"])
    
FurryHead's avatar
FurryHead committed
    def prnt(self, line):
        print(time.strftime("%Y-%m-%d %H:%M:%S") + (" ["+self.config["network"]+"] "+line))
FurryHead's avatar
FurryHead committed
    
    def sendLine(self, line):
        if line.endswith("\r\n"):
            self.push(bytes(line + "\r\n", "utf_8"))
FurryHead's avatar
FurryHead committed
    
    def doMessage(self, channel, message):
        self.sendLine("PRIVMSG "+channel+" :"+message)
        self.pluginManager.event("message", channel, User(self.config["nickname"]), message)
FurryHead's avatar
FurryHead committed
        
    def doAction(self, channel, action):
        self.sendLine("PRIVMSG "+channel+" :\x01ACTION "+action+" \x01")
        self.pluginManager.event("action", channel, User(self.config["nickname"]), action)
    def doQuit(self, message=""):
        self.sendLine("QUIT :" + message)
        self.pluginManager.event("quit", User(self.config["nickname"]), message)
        self.close_when_done()
FurryHead's avatar
FurryHead committed
    
    def doNotice(self, user, message):
        self.sendLine("NOTICE "+user+" :"+message)
    
    def doNick(self, newnick):
        self.sendLine("NICK " + newnick)
        self.pluginManager.event("nick", User(self.config["nickname"]), User(newnick))
FurryHead's avatar
FurryHead committed
        self.config["nickname"] = newnick
    
    def doJoin(self, channel):
        self.sendLine("JOIN "+channel)
        self.pluginManager.event("join", channel, User(self.config["nickname"]))

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