#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
syslog2IRC
==========

Receive syslog messages via UDP and show them on IRC.

Requires the `python-irclib`_ package.

Setup your ``syslog.conf``::

    *.*     @host-to-send-log-messages-to-and-this-script-runs-on

For more information, see `RFC 3164`, "The BSD syslog Protocol".

:Copyright: 2007 Jochen Kupperschmidt
:Date: 12-Apr-2007
:License: GNU General Public License, Version 2

.. _python-irclib:  http://python-irclib.sourceforge.net/
.. _RFC 3164:       http://tools.ietf.org/html/rfc3164
"""

from Queue import Queue
from SocketServer import BaseRequestHandler, ThreadingUDPServer
from threading import Thread
from time import sleep, strftime, strptime

from ircbot import SingleServerIRCBot
from irclib import nm_to_n


# ---------------------------------------------------------------- #
# syslog stuff

class SyslogMessage(object):
    """A syslog message."""

    FACILITIES = {
        0: 'kernel messages',
        1: 'user-level messages',
        2: 'mail system',
        3: 'system daemons',
        4: 'security/authorization messages',
        5: 'messages generated internally by syslogd',
        6: 'line printer subsystem',
        7: 'network news subsystem',
        8: 'UUCP subsystem',
        9: 'clock daemon',
        10: 'security/authorization messages',
        11: 'FTP daemon',
        12: 'NTP subsystem',
        13: 'log audit',
        14: 'log alert',
        15: 'clock daemon',
        16: 'local use 0 (local0)',
        17: 'local use 1 (local1)',
        18: 'local use 2 (local2)',
        19: 'local use 3 (local3)',
        20: 'local use 4 (local4)',
        21: 'local use 5 (local5)',
        22: 'local use 6 (local6)',
        23: 'local use 7 (local7)',
        }
    SEVERITIES = {
        0: 'Emergency',
        1: 'Alert',
        2: 'Critical',
        3: 'Error',
        4: 'Warning',
        5: 'Notice',
        6: 'Informational',
        7: 'Debug',
        }

    def __init__(self, data):
        # 1024 bytes max says RFC.
        self.payload = data[:1024]
        self.parse_priority()
        self.parse_header()

    def parse_priority(self):
        """Extract and resolve priority."""
        prio, self.payload = self.payload.split('>', 1)
        self.priority_id = int(prio[1:])
        self.facility_id, self.severity_id = divmod(self.priority_id, 8)
        self.facility = self.FACILITIES[self.facility_id]
        self.severity = self.SEVERITIES[self.severity_id]

    def parse_header(self):
        """Try to extract a RFC-compliant TIMESTAMP/HOSTNAME header."""
        try:
            self.timestamp = strptime(self.payload[:15], '%b %d %H:%M:%S')
        except ValueError:
            # Header is not RFC-compliant.
            self.timestamp, self.hostname = None, None
        else:
            self.hostname, self.payload = self.payload[16:].split(' ', 1)

    def __str__(self):
        s = ''
        if self.timestamp is not None:
            s += '[%s] ' % strftime('%Y-%m-%d %H:%M:%S', self.timestamp)
        if self.hostname is not None:
            s += '(%s) ' % self.hostname
        s += '[%s]: %s' % (self.severity, self.payload)
        return s


class SyslogRequestHandler(BaseRequestHandler):
    """Handler for syslog messages."""

    def handle(self):
        try:
            msg = SyslogMessage(self.request[0].strip())
        except ValueError:
            msg = 'Invalid message.'
        else:
            self.server.queue.put((self.client_address, msg))
        print ('%s:%d' % self.client_address), str(msg)


class SyslogReceiveServer(ThreadingUDPServer):
    """UDP server that waits for syslog messages."""

    def __init__(self):
        ThreadingUDPServer.__init__(self, ('', 514), SyslogRequestHandler)
        self.queue = Queue()


# ---------------------------------------------------------------- #
# IRC bot stuff

class SyslogBot(SingleServerIRCBot):

    def __init__(self, server_list, channel_list, nickname='Syslog',
            realname='syslog'):
        SingleServerIRCBot.__init__(self, server_list, nickname, realname)
        self.channel_list = channel_list

    def on_welcome(self, conn, event):
        """Join channels after connect."""
        print 'Connected to %s:%d.' % conn.socket.getsockname()
        for channel, key in self.channel_list:
            conn.join(channel, key)

    def on_nicknameinuse(self, conn, event):
        """Choose another nickname if conflicting."""
        self._nickname += '_'
        conn.nick(self._nickname)

    def on_ctcp(self, conn, event):
        """Answer CTCP PING and VERSION queries."""
        whonick = nm_to_n(event.source())
        message = event.arguments()[0].lower()
        if message == 'version':
            conn.notice(whonick, 'Syslog2IRC')
        elif message == 'ping':
            conn.pong(whonick)

    def on_privmsg(self, conn, event):
        """React on private messages.

        Die, for example.
        """
        whonick = nm_to_n(event.source())
        message = event.arguments()[0]
        if message == 'die!':
            print 'Shutting down as requested by %s...' % whonick
            self.die('Shutting down.')

    def say(self, msg):
        """Say message to channels."""
        for channel, key in self.channel_list:
            self.connection.privmsg(channel, msg)


# ---------------------------------------------------------------- #

def process_queue(announce_callback, queue, delay=2):
    """Process received messages in queue."""
    while True:
        sleep(delay)
        try:
            addr, msg = queue.get()
        except Empty:
            continue
        announce_callback('%s:%d ' % addr + str(msg))

if __name__ == '__main__':
    # Set IRC connection parameters.
    irc_active = True
    irc_servers = [('irc.example.com', 6667)]
    irc_channels = [('#examplechannel', 'secret')]

    if irc_active:
        # Prepare and start IRC bot.
        bot = SyslogBot(irc_servers, irc_channels)
        Thread(target=bot.start).start()
        announce = bot.say
    else:
        # Just display messages locally.
        def announce(s):
            print s

    # Prepare and start syslog message receiver.
    receiver = SyslogReceiveServer()
    Thread(target=receiver.serve_forever).start()

    process_queue(announce, receiver.queue)