From 9ca0509bb0ec3753d50c86ea94ba5747eecc1f73 Mon Sep 17 00:00:00 2001
From: Fanir <projects@mail.fanir.de>
Date: Fri, 28 Feb 2014 17:04:08 +0100
Subject: [PATCH] implemented basics

---
 main.py | 301 +++++++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 235 insertions(+), 66 deletions(-)

diff --git a/main.py b/main.py
index 0d3bfb8..1ad0b96 100755
--- a/main.py
+++ b/main.py
@@ -23,9 +23,25 @@
 #
 
 
-import sys
-import socket
-import string
+# SETTINGS CAN BE FOUND AT THE BOTTOM OF THE FILE
+
+
+import sys, time, string, socket, re, signal
+from select import poll, POLLIN, POLLPRI, POLLOUT,\
+                        POLLERR, POLLHUP, POLLNVAL
+from threading import Thread#, Event
+
+bot = None
+
+class log():
+    DEBUG, INFO, WARNING, ERROR, FATAL, SILENT =\
+        0,    1,       2,     3,     4,      5
+    
+    def show(level, msg):
+        if level in range(log.DEBUG, log.SILENT):
+            if LOGLEVEL <= level: print(msg)
+        else:
+            raise ValueError("That's not a loglevel!")
 
 
 class pircbot():
@@ -40,103 +56,256 @@ class pircbot():
             try: return textstring.decode(codec)
             except UnicodeDecodeError: continue
         return textstring.decode(self.encodings[0], 'ignore')
-
-    connected = False
-
-    def __init__(self, nicknames, server, port=6667, ident="pircbot", realname="pircbot", encodings=("utf-8", "latin-1")):
+    
+    def __init__(self, nicknames, server, port=6667, ident="pircbot",
+                realname="pircbot", encodings=("utf-8", "latin-1"),
+                command_prefix=".", admins=""):
         self.nicknames = nicknames
         self.server = server
         self.port = port
         self.ident = ident
         self.realname = realname
         self.encodings = encodings
-
+        self.admins = admins
+        self.cmdprefix = command_prefix
+        self.socket = None
+        self.recvloop = None
+        self.recvbuffer = bytearray(1024)
+        self.run = False
+        self.ready = False
+        self.is_alive = False
+        self.need_nick = True
+    
+    def recv(self):
+        """
+        Loop for reciving data
+        """
+        parser = Thread(target=self.parse, name="parser")
+        p = poll()
+        p.register(self.socket.fileno(), POLLIN)
+        while self.run:
+            ap = p.poll(1000)
+            if (self.socket.fileno(), POLLIN) in ap:
+                self.recvbuffer.extend(self.socket.recv(1024))
+                if not parser.is_alive():
+                    parser = Thread(target=self.parse, name="parser")
+                    parser.start()
+        if parser.is_alive(): parser.join()
+    
     def connect(self):
-        s=socket.socket( )
+        # connect
+        self.socket = socket.socket()
         try:
-            s.connect((self.server, self.port))
+            self.socket.connect((self.server, self.port))
         except socket.error as e:
-            print("Fehler: %s" % e)
+            log.show(log.FATAL, "Fehler: %s" % e)
             return
-        s.send(self.encode("NICK %s\r\n" % self.nicknames[0]))
-        s.send(self.encode("USER %s %s bla :%s\r\n" % (self.ident, self.server, self.realname)))
-        self.connected = True
-        readbuffer=""
-        while 1:
-            readbuffer=readbuffer+self.decode(s.recv(1024))
-            temp=readbuffer.split("\n")
-            readbuffer=temp.pop( )
-
-            for line in temp:
-                line=line.rstrip()
-                line=line.split(" ")
-
-                if(line[0]=="PING"):
-                    s.send(self.encode("PONG %s\r\n" % line[1]))
+        
+        self.run = True
+        
+        # start getting data
+        self.recvloop = Thread(target=self.recv, name="recvloop")
+        self.recvloop.start()
+        
+        # get a nick
+        self.socket.send(self.encode("NICK %s\r\n" % self.nicknames.pop(0)))
+        
+        self.socket.send(self.encode("USER %s %s bla :%s\r\n" % (self.ident, self.server, self.realname)))
+        
+        self.is_alive = True
+    
+    def parse(self):
+        """
+        Loop for parsing incoming data
+        """
+        #line = ""
+        origin = {}
+        while self.run and self.recvbuffer != b"":
+                # get and decode line from buffer
+                rawline, _, self.recvbuffer = self.recvbuffer.partition(b"\r\n")
+                rawline = self.decode(rawline)
+                # prepare line
+                line = rawline.split(" :", 1)
+                head = line[0].lstrip("\x00").split(" ")
+                larg = line[1] if len(line)>1 else ""
+                # parse prefix
+                if head[0].startswith(":"):
+                    prefix = head.pop(0)[1:]
+                    origin.clear()
+                    if "@" in prefix:
+                        origin["nick"], origin["ident"], origin["host"] = re.match(r"(\S+)!(\S+)@(\S+)", prefix).groups()
+                    else:
+                        origin["server"] = prefix
                 else:
-                    print(" ".join(line))
-
+                    prefix = ""
+                # parse command
+                command = head.pop(0)
+                # parse params
+                params = head
+                params.append(larg)
+                
+                log.show(log.DEBUG, ">> %s" % rawline)
+                
+                if command == "PING":
+                    self.socket.send(self.encode("PONG %s\r\n" % params[0]))
+                elif command == "PRIVMSG":
+                    if params[1].startswith(self.cmdprefix):
+                        args = params[1].lstrip(self.cmdprefix).split(" ")
+                        sndline = self.on_command(prefix, origin, params[0], args[0], args[1:])
+                        if sndline != None:
+                            log.show(log.DEBUG, "<< %s" % sndline)
+                            self.socket.send(self.encode(sndline))
+                elif command == "001":
+                    self.ready = True
+                elif command == "433":
+                    self.socket.send(self.encode("NICK %s\r\n" % self.nicknames.pop(0)))
+                elif command == "KILL":
+                    print("Got killed: %s", rawline)
+                    self.die()
+    
+    def is_admin(self, user):
+        for admin in self.admins:
+            if re.search(admin, user) != None:
+                return True
+        return False
+    
+    def die(self):
+        self.run = False
+        self.recvloop.join()
+        self.is_alive = False
+        sys.exit()
+    
     def quit(self, reason=""):
-        pass
-
+        self.socket.send(self.encode("QUIT :%s\n" % reason))
+        self.run = False
+        self.recvloop.join()
+        self.is_alive = False
+    
     def join(self, channel):
-        pass
-
+        print(channel)
+        self.socket.send(self.encode("JOIN %s\n" % channel))
+    
     def part(self, channel):
-        pass
-
-
-
-
-def startbot():
-    bot = pircbot(nicknames=(NICKNAME, ALT_NICKNAME), server=SERVER, port=PORT,
-        ident=IDENT, realname=REALNAME, encodings=ENCODINGS)
-    bot.connect()
-    bot.quit()
+        self.socket.send(self.encode("PART %s\n" % channel))
+    
+    def on_command(self, prefix, origin, source, command, params):
+        """
+        Executed when getting a PRIVMSG starting with self.cmdprefix
+        Prefix contains the optional prefix of the raw line.
+        Origin is a map holding the parsed prefix, containing
+            "nick", "ident" and "host" for users and "server" for servers.
+        Source is the first parameter after the irc-command, specifying
+            the channel or user, from where the line comes.
+        Command is the command for the bot.
+        Params contains a list of originally space separated parameters.
+        """
+        print(params)
+        if command.startswith("hello"):
+            greeting = "".join(("Hi " + origin["nick"] +"!"))
+            if source[0] in {"#", "+", "!", "&"}:
+                return "PRIVMSG %s :%s\r\n" % (source, greeting)
+            else:
+                return "PRIVMSG %s :%s\r\n" % (origin["nick"], greeting)
+        elif command.startswith("join") and len(params)>0:
+            if self.is_admin(origin["nick"]): self.join(params[0])
+            else: return "PRIVMSG %s :You can't do that!\r\n" % origin["nick"]
+        elif command.startswith("part") and len(params)>0:
+            if self.is_admin(origin["nick"]): self.part(params[0])
+            else: return "PRIVMSG %s :You can't do that!\r\n" % origin["nick"]
 
 
 def parseargs():
-    """
-    Parses comand line arguments
-    """
     import argparse
     p = argparse.ArgumentParser(
         description = "i think my desc is missing")
     p.add_argument("action", default="help", choices = ["start", "stop"], help="What to do?")
     #p.add_argument("--daemon", "-d", type = bool, choices = [1, 0], default=1, help="Daemonize, Default: 1")
-
     return p.parse_args()
 
+def KeyboardInterruptHandler(signum, frame):
+    global bot
+    print("Got Ctrl-C, dying now...")
+    bot.quit("")
+    sys.exit()
 
 def main():
+    global bot
+    signal.signal(signal.SIGINT, KeyboardInterruptHandler)
+    
     args = parseargs()
     if args.action == "start":
-        startbot()
+        bot = pircbot(nicknames=NICKNAMES, ident=IDENT, realname=REALNAME,
+            server=SERVER, port=PORT, encodings=ENCODINGS, command_prefix=COMMAND_PREFIX, admins=ADMINS)
+        try:
+            bot.connect()
+            while bot.ready == False: time.sleep(1)
+            for channel in CHANNELS:
+                bot.join(channel)
+            while 1:
+                if bot.is_alive == False: print("X")
+                time.sleep(10)
+        except SystemExit:
+            print("exiting...")
     elif args.action == "stop": print("nope!")
     return 0
 
 
 if __name__ == '__main__':
-
+    
     ################
     ### SETTINGS ###
     ################
-
-
-    IDENT        = "chalkbot"
-    NICKNAME     = "chalkbot"
-    ALT_NICKNAME = "chalkbot_"
-    REALNAME     = "A ChalkBot Instance"
-    # Command for registering with nickserv, without leading slash
-    NICKSERVCMD  = ""
-
-    SERVER       = "fanir.de"
-    PORT         = 6667
-
-    ENCODINGS     = ('utf-8', 'latin-1', 'iso-8859-1', 'cp1252')
-
-    CHANNELS     = ["#bots"]
-
-
+    
+    # With how much information do you want to be annoyed?
+    # DEBUG spams most, FATAL least. WARNING should be a good tradeoff.
+    # One of: log.DEBUG, log.INFO, log.WARNING, log.ERROR, log.FATAL
+    LOGLEVEL = log.DEBUG
+    
+    
+    # Also known as username. Some IRC-internal.
+    # By default, the nickname will be used as ident.
+    IDENT = "chalkbot"
+    
+    # The list of nicknames to try. If the first one is not aviable, it will
+    # try the second, and so on...
+    # You should specfy at least two nicks.
+    NICKNAMES = ["chalkbot", "chalkbot_", "chalkbot__"]
+    
+    REALNAME = "A ChalkBot Instance"
+    
+    # Command for registering with nickserv, without leading slash.
+    NICKSERVCMD = ""
+    
+    MODES = "+B"
+    
+    
+    # The Server name to connect to. Duh!
+    #SERVER = "fanir.de"
+    SERVER = "localhost"
+    
+    # "Default" is 6667. An often used port for SSL would be 6697, if SSL would be
+    # supported. Maybe in a future, far far away...
+    PORT = 6667
+    
+    # A comma-seperated list of channels to join, enclosed by braces.
+    CHANNELS = ["#bots", "#main"]
+    
+    # The encodings to try when getting messages from IRC. Will be tried in the given order.
+    # The first one is used to encode data when sending stuff.
+    # The list given shoud do just fine in most networks, I assume.
+    # Also comma seperated and enclosed by braces.
+    ENCODINGS = ['utf-8', 'latin-1', 'iso-8859-1', 'cp1252']
+    
+    
+    # List of users (hostmasks, will be parsed as regex) who can do important stuff,
+    # like joining and parting channels and shutting down the bot.
+    ADMINS = ["Fanir.*"]
+    
+    COMMAND_PREFIX = "."
+    
+    #######################
+    ### END OF SETTINGS ###
+    #######################
+    
+    
     main()
-