# Hmm Bot
# Copyright (C) 2009 Susam Pal
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

"""Hmm Bot.

This bot is based on an idea by John Wesley. The bot replies with a
'hmm' for every message it receives from a nick in a channel or as a
private message. The module variables contain the connection details of
the bot. They need to be modified before connecting to an IRC server.
"""

import sys
import re
import socket
import asynchat
import asyncore

host = 'irc.freenode.net'
port = 6667
nick = 'hmmbot'
password = None
username = 'hmmbot'
realname = 'Hmm Bot'
channel = '#hmmbot'

class HmmBot(asynchat.async_chat):
    """Class for Hmm Bot.

    Methods:

    run -- Run the bot
    """
    def __init__(self):
        """Set up socket and message terminator."""
        print 'Connecting to %s:%s ...' % (host, port)
        asynchat.async_chat.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 
        self.connect((host, port))
        self.set_terminator('\r\n')
        self.data = ''

    def run(self):
        """Run the bot"""
        asyncore.loop()

    def handle_connect(self):
        """Send password, user and nick details."""
        print 'Connected.'
        if password:
            self.push('PASS %s\r\n' % password)
        self.push('USER %s . . :%s\r\n' % (username, realname))
        self.push('NICK %s\r\n' % nick)

    def collect_incoming_data(self, data):
        """Accumulate received data."""
        self.data += data

    def found_terminator(self):
        """Parse received message."""
        data = self.data
        self.data = ''

        message = ParsedMessage(data)
        prefix = ParsedPrefix(message.prefix)
        params = ParsedParams(message.params)
        command = message.command.upper()

        if command == '251': # RPL_LUSERCLIENT (RFC 1459 - 6.2)
            print 'Joining %s ...' % channel
            self.push('JOIN %s\r\n' % channel)

        if command == '366': # RPL_ENDOFNAMES (RFC 1459 - 6.2)
            print 'Joined.'

        elif command == '433': # ERR_NICKNAMEINUSE (RFC 1459 - 6.1)
            print >> sys.stderr, 'ERROR: Nickname is already in use.'
            self.close_when_done()

        elif command == 'PING':
            self.push('PONG :%s\r\n' % params.trailing)

        elif command == 'PRIVMSG':
            # Send a private message to the nick if the message was received
            # as a private message, send to the channel otherwise.
            if params.middle.lower() == nick.lower():
                to = prefix.nick
            else:
                to = channel
            self.push('PRIVMSG %s :%s\r\n' % (to, 'hmm'))
                    
class ParsedMessage:
    """This class represents a received message in IRC protocol.

    An example of a received IRC message is:

    :spal!n=spal@unaffiliated/spal PRIVMSG #python :hello, world

    The above message is parsed as:

    prefix = 'spal!n=spal@unaffiliated/spal'
    command = 'PRIVMSG'
    params = '#python :hello, world'

    Some messages may not have a prefix. The prefix of such messages is
    set to None. An example of such message is:

    PING :simmons.freenode.net
    """
    def __init__(self, message):
        """Parse the received IRC message."""

        self.message = message
        self.prefix = None
        self.command = None
        self.params = None 

        # RFC 1459 - 2.3.1
        # <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
        #
        # The prefix is extracted into self.prefix and rest of the
        # message is left intact in message.
        if message.startswith(':'):
            self.prefix, message = message[1:].split(None, 1)

        # RFC 1459 - 2.3.1
        # <command> ::= <letter> { <letter> } | <number> <number> <number>
        #
        # The command and params are separated into separate variables.
        message = message.split(None, 1)
        if len(message) < 2:
            print 'Bad message found'
            return
        else:
            self.command, self.params = message

class ParsedPrefix:
    """This class represents a prefix present in a received IRC message.

    An example of a prefix is:

    spal!n=spal@unaffiliated/spal

    The above prefix is parsed as: nick = 'spal', user = 'spal' and
    host = 'unaffiliated/spal'.
    
    If any part of the prefix is missing, then that part is set to None.
    """

    def __init__(self, prefix): 
        """Parse the specified prefix."""

        if not prefix:
            self.prefix = self.nick = self.user = self.host = None
            return

        self.prefix = prefix

        # RFC 1459 - 2.3.1
        # <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
        nick_regex = re.compile(r'^([^!@]+)')
        user_regex = re.compile(r'!([^!@]+)')
        host_regex = re.compile(r'@([^!@]+)')

        match = nick_regex.search(prefix)
        if match:
            self.nick = match.group(1)
        else:
            self.nick = None

        match = user_regex.search(prefix)
        if match:
            self.user = match.group(1)
        else:
            self.user = None

        match = host_regex.search(prefix)
        if match:
            self.host = match.group(1)
        else:
            self.host = None

class ParsedParams:
    """This class represents the params of a command in IRC message.

    An example of params is:

    #python :hello, world

    It is parsed as: middle = '#python', trailing = ':hello, world'.
    """

    def __init__(self, params):
        """Parse the specified params."""
        
        if not params:
            self.params = self.middle = self.trailing = None
            return

        self.params = params
        self.middle = None

        # RFC 1459 - 2.3.1
        # <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
        if ':' in self.params:
            self.middle, self.trailing = self.params.split(':', 1)
            self.middle = self.middle.strip()
        else:
            self.middle = params
            self.trailing = None

        if not self.middle:
            self.middle = None

        if not self.trailing:
            self.trailing = None

# Instantiate the bot and run it
if __name__ == '__main__':
    bot = HmmBot()
    bot.run()

