Sunday, July 22, 2012

IRC Bot (logbot)

After my first post on IRC bots, I decided to make a bot for logging a chat that was a bit better designed than the one I posted. It is far from perfect, however it will write formatted logs to a file for you, won't sit around if it is kicked and if it sees a message it cannot format, it will log the raw data. Also to keep in mind, it only joins a channel ofter the motd finishes with a certain number code. If there is no motd or it does not send the right number code it won't join. The reason for this is I just wrote the thing today and don't have the time to check all possibilities. So use it as a skeleton and develop on it further. Or ignore it and move on with your day. All descriptions will be commented in the code.

#!/usr/bin/env python
# Import all the packages we will be using
import logging
import socket
import ssl
import os
import sys
import re

# Some global variables for configuration
HOST = "irc.69megabytes.com"
PORT = 6697
NICK = "PyLogBot46"
CHANNEL = "#allthefallen"
SSL = True
FLOG = os.path.join(sys.path[0], "ircbot.log")
PWD = "supersecretpassword"
# This is for use later on to know when to join the channel and do it only once
notInChan = True

# Regex parsing of a standard IRC message
def parse(data):
    temp=re.match(r"^(:(?P<prefix>\S+) )?(?P<command>\S+)( (?!:)(?P<params>.+?))?( :(?P<trail>.+))?$", data)
    if temp:
        temp=temp.groupdict()
    return temp

# Where the magic happens
if __name__ == '__main__':
    # Set up the logging format
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s', datefmt="%Y-%d-%m (%H:%M:%S %Z)", filename=FLOG)
    # Message format strings so we can easily alter them
    format_privmsg = "{1} <{0}>: {2}"
    format_notice = "(notice) <{0}>: {1}"
    format_join = "{0} has entered {1}"
    format_nick = "{0} is now known as {1}"
    format_mode = "{0} set mode {1}"
    format_modes = "Mode set {0}"
    format_kick = "{0} kicked from {1} by {2} ({3})"
    format_srvmsg = "-!- {0} {1} {2}"
    try:
        # Create socket
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Set up SSL if necessary
        if SSL:
            s = ssl.wrap_socket(s)

        # Connect to the server
        s.connect((HOST, PORT))

        # Authenticate
        s.send("NICK " + NICK + "\r\n")
        s.send("USER " + NICK + " www.anarchy46.net PY :Python LogBot\r\n")

        # Main loop
        while 1:
            # Reset information received and processed
            data = ""
            info = {}
            # Retreive data 1 byte at a time, stop at newline
            while data.find("\n") == -1:
                data += s.recv(1)
            # Remove unnecessary whitespace
            data = data.strip()
            # Parse the raw data
            info = parse(data)
            # Privmsg handler
            if info['command'] == "PRIVMSG":
                logging.info(format_privmsg.format(info['prefix'], info['params'], info['trail']))
            # Notices
            elif info['command'] == "NOTICE":
                # If to self from someone
                if info['params'] == NICK:
                    # To quit properly
                    if info['trail'] == PWD:
                        s.send("QUIT :Done logging.\r\n")
                        break
                    # So we don't log the password
                    logging.info(format_notice.format(info['prefix'], info['trail']))
                # Server notices
                else:
                    logging.info(format_srvmsg.format(info['prefix'], info['params'], info['trail']))
            # Respond to server pings (no need to log)
            elif info['command'] == "PING":
                s.send("PONG " + info['trail'] + "\r\n")
            # Someone joins
            elif info['command'] == "JOIN":
                logging.info(format_join.format(info['prefix'], info['trail']))
            # Someone changes nick
            elif info['command'] == "NICK":
                logging.info(format_join.format(info['prefix'], info['trail']))
            # Modes
            elif info['command'] == "MODE":
                # Normal mode changes
                if info['trail']:
                    logging.info(format_modes.format(info['trail']))
                # Modes for self (set by server)
                else:
                    logging.info(format_mode.format(info['prefix'], info['params']))
            # Someone gets kicked
            elif info['command'] == "KICK":
                # Quit if it's the bot
                temp = info['params'].split(' ')
                if temp[1] == NICK:
                    logging.info("Kicked from channel.")
                    s.send("QUIT :Bye bye...\r\n")
                    break
                # Log everyone else being kicked :D
                else:
                    logging.info(format_kick.format(temp[0], temp[1], info['prefix'], info['trail'])
            # Special server messages
            elif info['command'].isdigit():
                if info['trail']:
                    logging.info(format_srvmsg.format(info['prefix'], info['command'], info['trail']))
                else:
                    logging.info(format_srvmsg.format(info['prefix'], info['command'], info['params']))
                # End of MOTD
                if notInChan and info['command'] == "376":
                    s.send("JOIN " + CHANNEL + "\r\n")
                    notInChan = False
            else:
                logging.info(data)
    # Abort!
    except socket.error as err:
        logging.critical("Socket error: " + str(err.errno))
        sys.exit(err.errno)

It's not the prettiest of code, but it works. When launching from the command line, make sure to add an ampersand (&) at the end so you don't need to wait for it or leave a window open with it running. Enjoy, and remember this thing only took me a couple of hours to write and sort out some formats.

No comments:

Post a Comment