#!/usr/bin/python -tt## SILAN# Simple IRC Log Analyzer# http://slackforge.com/silan/## Version 0.0.2# An extremely simple IRC log analyzer --;# Only supports dircproxy logs, because that's what I use## Copyright (c) 2007 Shidayu <shidayu@gmail.com>## Permission is hereby granted, free of charge, to any person obtaining a copy# of this software and associated documentation files (the "Software"), to deal# in the Software without restriction, including without limitation the rights# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell# copies of the Software, and to permit persons to whom the Software is# furnished to do so, subject to the following conditions:## The above copyright notice and this permission notice shall be included in# all copies or substantial portions of the Software.## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN# THE SOFTWARE.##### Import Other Modules#import os, re, stat, sys, glob, datetime, string, time, randomfrom ConfigParser import SafeConfigParser### Globals## Regular Expressions used by SILANre_rawlog = re.compile('^\[(\d\d) ([A-Z][a-z][a-z]) (\d\d):(\d\d)\] ([^\n\r\f\v]*)$')re_ownnick = re.compile('--- You changed your nickname to (\S*)')re_nickchange = re.compile('--- (\S*) \([^\t\n\r\f\v)]*\) changed nickname to (\S*)')re_quit = re.compile('<-- (\S*) \([^\t\n\r\f\v)]*\) quit from IRC: ([^\n\r\f\v]*)$')re_join = re.compile('--> (\S*) \([^\t\n\r\f\v)]*\) joined the channel$')re_part = re.compile('<-- (\S*) \([^\t\n\r\f\v)]*\) left the channel$')re_message = re.compile('<([^ \t\n\r\f\v!]*)![^\t\n\r\f\v>]*> ([^\n\r\f\v]*)$')re_describe = re.compile('\* (\S*) ([^\n\r\f\v]*)')re_topic = re.compile('--- (\S*) \([^\t\n\r\f\v)]*\) changed topic: ([^\t\n\r\f\v]*)$')re_kick = re.compile('<-- (\S*) kicked off by (\S*) \([^\t\n\r\f\v)]*\): ([^\n\r\f\v]*)$')re_ownkick = re.compile('<-- Kicked off by (\S*) \([^\t\n\r\f\v)]*\): ([^\n\r\f\v]*)$')re_nonword = re.compile('[^a-zA-Z0-9]')re_channame = re.compile('^(\S*).log$')re_tpltag = re.compile('{{silan:([^\t\n\r\f\v}]*)}}')# Month conversion Dictionaryfmt_month = { "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4, "May": 5, "Jun": 6, "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12 }### Class Definitions#class Silan(object): """ Simple IRC Log Analyzer """ def __init__(self): self.config = SafeConfigParser() self.config.read("silan.ini") self.networks = [Network(self.config)] channellogs = glob.glob("#*.log") for filename in channellogs: self.networks[0].channels.append(Channel(re_channame.match(filename).group(1), self.networks[0], filename)) for channel in self.networks[0].channels: outname = channel.name.lower() outname = re.sub(re_nonword, "_", outname) outname += ".html" outfile = open("output/" + outname, "w") outfile.write(Report(channel).output_string()) outfile.close()class Line(object): """ Line of log data with date and time """ def __init__(self, dttm, text): self.dttm = dttm self.text = text def wordcount(self): return len(self.text.split())class UserLine(Line): """ Line of log data that includes user instance """ def __init__(self, dttm, user, text): Line.__init__(self, dttm, text) self.user = userclass Message(UserLine): """ Standard IRC message from a user """ def __init__(self, dttm, user, text): UserLine.__init__(self, dttm, user, text)class Describe(UserLine): """ Describe or action from a user """ def __init__(self, dttm, user, text): UserLine.__init__(self, dttm, user, text)class Topic(UserLine): """ Channel topic """ def __init__(self, dttm, user, text): UserLine.__init__(self, dttm, user, text)class Kick(UserLine): """ Channel kick """ def __init__(self, dttm, target, user, text): UserLine.__init__(self, dttm, user, text) self.target = targetclass Log(object): """ Log recorded by IRC client/proxy """ def __init__(self, filename): self.filename = filename self.lines = [] self._read_log() def _read_log(self): loghandler = open(self.filename, 'r') loglist = loghandler.readlines() loghandler.close() creationdttm = datetime.datetime.fromtimestamp(os.stat(self.filename).st_ctime) firstday = int(re_rawlog.match(loglist[0]).group(1)) firstmonth = fmt_month[re_rawlog.match(loglist[0]).group(2)] firsthour = int(re_rawlog.match(loglist[0]).group(3)) firstminute = int(re_rawlog.match(loglist[0]).group(4)) prevdttm = datetime.datetime(creationdttm.year, firstmonth, firstday, firsthour, firstminute) for logtext in loglist: day = int(re_rawlog.match(logtext).group(1)) month = fmt_month[re_rawlog.match(logtext).group(2)] hour = int(re_rawlog.match(logtext).group(3)) minute = int(re_rawlog.match(logtext).group(4)) text = re_rawlog.match(logtext).group(5) if month < prevdttm.month or (month == prevdttm.month and day < prevdttm.day): # The preceding: Does it look like we've gone back in time? prevdttm = datetime.datetime(prevdttm.year + 1, month, day, hour, minute) else: prevdttm = datetime.datetime(prevdttm.year, month, day, hour, minute) self.lines.append(Line(prevdttm, text))class Network(Log): """ IRC network """ def __init__(self, config, name="default", filename="Server.log"): Log.__init__(self, filename) self.name = name self.channels = [] self.users = [] self.ownuser = None self.config = config self._config() self._parse() def _config(self): for nick in self.config.options(self.name + ".alternates"): for alt in self.config.get(self.name + ".alternates", nick).split(): self.add_alternate(nick, alt) for nick in self.config.options(self.name + ".separates"): for sep in self.config.get(self.name + ".separates", nick).split(): self.add_separate(nick, sep) for nick in self.config.options(self.name + ".propercase"): self.get_user(nick).name = self.config.get(self.name + ".propercase", nick) def get_channel(self, channame): for netchannel in self.channels: if channame.lower() == netchannel.name.lower(): return netchannel return None def get_user(self, nickname): netuser = self.find_user(nickname) if not netuser: newuser = User(nickname) self.users.append(newuser) return newuser else: return netuser def find_user(self, nickname): for netuser in self.users: if nick_belongs_to_user(nickname, netuser): return netuser def add_separate(self, nick1, nick2): netuser1 = self.get_user(nick1) netuser2 = self.get_user(nick2) if netuser1 == netuser2: if netuser1.name.lower() == nick1.lower(): netuser1.alternates.remove(nick2) newuser = self.get_user(nick2) newuser.separates.append(nick1) netuser1.separates.append(nick2) if netuser2.name.lower() == nick2.lower(): netuser2.alternates.remove(nick1) newuser = self.get_user(nick1) newuser.separates.append(nick2) netuser2.separates.append(nick1) else: netuser1.separates.append(nick2) netuser2.separates.append(nick1) def add_alternate(self, nick1, nick2): netuser1 = self.find_user(nick1) netuser2 = self.find_user(nick2) if not netuser1 and not netuser2: newuser = self.get_user(nick1) newuser.alternates.append(nick2) elif netuser1 and not netuser2: if nick2.lower() not in [sep.lower() for sep in netuser1.separates]: netuser1.alternates.append(nick2) else: self.get_user(nick2) elif netuser2 and not netuser1: if nick1.lower() not in [sep.lower() for sep in netuser2.separates]: netuser2.alternates.append(nick1) else: self.get_user(nick1) elif netuser1 and netuser2 and netuser1 != netuser2 and not are_separate(netuser1, netuser2): if self.users.index(netuser1) < self.users.index(netuser2): netuser1.alternates.append(netuser2.name) netuser1.alternates.extend(netuser2.alternates) self.users.remove(netuser2) if self.ownuser == netuser2: self.ownuser = netuser1 for netchan in self.channels: netchan.reassign_events(netuser2, netuser1) del netuser2 else: netuser2.alternates.append(netuser1.name) netuser2.alternates.extend(netuser1.alternates) self.users.remove(netuser1) if self.ownuser == netuser1: self.ownuser = netuser2 for netchan in self.channels: netchan.reassign_events(netuser1, netuser2) del netuser1 def _parse(self): for line in self.lines: if re_nickchange.match(line.text): oldnick = re_nickchange.match(line.text).group(1) newnick = re_nickchange.match(line.text).group(2) self.add_alternate(oldnick, newnick) if re_ownnick.match(line.text): newnick = re_ownnick.match(line.text).group(1) if not self.ownuser: self.ownuser = self.get_user(newnick) else: self.add_alternate(self.ownuser.name, newnick)class Channel(Log): """ IRC channel """ def __init__(self, name, network, filename): Log.__init__(self, filename) self.name = name self.network = network self.topics = [] self.messages = [] self.describes = [] self.kicks = [] self._parse() def reassign_events(self, userfrom, userto): for message in get_messages_by_user(userfrom): message.user = userto for describe in get_describes_by_user(userfrom): describe.user = userto for kick in get_kicks_by_user(userfrom): kick.user = userto for kick in get_kicks_by_target(userfrom): kick.target = userto for topic in get_topics_by_user(userfrom): topic.user = userto def get_messages_by_hour(self, hour): return [msg for msg in self.messages if msg.dttm.hour == hour] def get_messages_by_weekday(self, weekday): return [msg for msg in self.messages if msg.dttm.weekday == weekday] def get_messages_by_user(self, user): return [msg for msg in self.messages if msg.user == user] def get_describes_by_user(self, user): return [dsc for dsc in self.describes if dsc.user == user] def get_kicks_by_user(self, user): return [kck for kck in self.kicks if kck.user == user] def get_kicks_by_target(self, user): return [kck for kck in self.kicks if kck.target == user] def get_topics_by_user(self, user): return [tpc for tpc in self.topics if tpc.user == user] def get_users(self): users = [] for message in self.messages: if message.user not in users: users.append(message.user) for describe in self.describes: if describe.user not in users: users.append(describe.user) for kick in self.kicks: if kick.user not in users: users.append(kick.user) if kick.target not in users: users.append(kick.target) for topic in self.topics: if message.user not in users: users.append(topic.user) return users def _parse(self): for line in self.lines: if re_message.match(line.text): user = self.network.get_user(re_message.match(line.text).group(1)) text = re_message.match(line.text).group(2) self.messages.append(Message(line.dttm, user, text)) elif re_describe.match(line.text): user = self.network.get_user(re_describe.match(line.text).group(1)) text = re_describe.match(line.text).group(2) self.describes.append(Describe(line.dttm, user, text)) elif re_kick.match(line.text): target = self.network.get_user(re_kick.match(line.text).group(1)) user = self.network.get_user(re_kick.match(line.text).group(2)) text = re_kick.match(line.text).group(3) self.kicks.append(Kick(line.dttm, target, user, text)) elif re_ownkick.match(line.text): user = self.network.get_user(re_ownkick.match(line.text).group(1)) text = re_ownkick.match(line.text).group(2) self.kicks.append(Kick(line.dttm, self.network.ownuser, user, text)) elif re_topic.match(line.text): user = self.network.get_user(re_topic.match(line.text).group(1)) text = re_topic.match(line.text).group(2) self.topics.append(Topic(line.dttm, user, text))class User(object): """ IRC user """ def __init__(self, name): self.name = name self.alternates = [] self.separates = []class ReportTemplate(object): def __init__(self, filename, theme="default"): self.filename = filename self.theme = theme self.contents = None self.registry = {} self._read() def _read(self): tplhandler = open("themes/" + self.theme + "/" + self.filename) self.contents = tplhandler.read() tplhandler.close() def output_string(self): dough = self.contents for key in self.registry: dough = dough.replace("{{silan:" + key + "}}", self.registry[key]) return dough def register_tag(self, tag, something): self.registry[tag] = something def kill_registry(self): self.registry = {}class Report(ReportTemplate): def __init__(self, channel, theme="default"): self.channel = channel ReportTemplate.__init__(self, "main.tpl", theme) self._register() def _register(self): self.register_tag("channel", self.channel.name) self.register_tag("ownusername", self.channel.network.ownuser.name) self.register_tag("daycount", str((self.channel.lines[-1].dttm - self.channel.lines[0].dttm).days)) self.register_tag("startdate", self.channel.lines[0].dttm.strftime("%A %B %d, %Y")) self.register_tag("enddate", self.channel.lines[-1].dttm.strftime("%A %B %d, %Y")) self.register_tag("usercount", str(len(self.channel.get_users()))) self.register_tag("modules", self._modules()) def _modules(self): return UserStatistics(self.channel, self.theme).output_string()class ReportModule(ReportTemplate): def __init__(self, name, theme): self.name = name self.theme = theme ReportTemplate.__init__(self, "module.tpl", theme) self._register() def _register(self): self.register_tag("title", self.name)class UserStatistics(ReportModule): def __init__(self, channel, theme): self.channel = channel self.theme = theme ReportModule.__init__(self, "User Activity Statistics", self.theme) self.register_tag("contents", self._contents()) def _contents(self): jacket = ReportTemplate("useractivity.tpl", self.theme) userformat = ReportTemplate("useractivity.user.tpl", self.theme) internal = "" bymsgct = [(len(self.channel.get_messages_by_user(user)), user) for user in self.channel.get_users()] bymsgct.sort() bymsgct.reverse() for userandct in bymsgct: userformat.register_tag("rank", str(bymsgct.index(userandct) + 1)) userformat.register_tag("name", userandct[1].name) userformat.register_tag("count", str(userandct[0])) if userandct[0] > 0: userformat.register_tag("quote", self.channel.get_messages_by_user(userandct[1])[random.randint(0, userandct[0] - 1)].text) else: userformat.register_tag("quote", "") internal += userformat.output_string() userformat.kill_registry() jacket.register_tag("users", internal) return jacket.output_string()### Functions#def are_separate(user1, user2): for sep in user1.separates: if nick_belongs_to_user(sep, user2): return 1 for sep in user2.separates: if nick_belongs_to_user(sep, user1): return 1 return 0def nick_belongs_to_user(nick, user): return nick.lower() == user.name.lower() or nick.lower() in [alt.lower() for alt in user.alternates]def main(): appversion = "0.0.2" print '\n\nSILAN', appversion, '- Simple IRC Log Analyzer - http://slackforge.com/silan/' print 'Copyright (c) 2007 Shidayu <shidayu@gmail.com>' try: appinstance = Silan() except: print "An unexpected error has occurred:", sys.exc_info()[0] raise### Begin Here!#if __name__=="__main__": main()