#!/usr/bin/python -tt## SILAN# Simple IRC Log Analyzer# http://slackforge.com/silan/## Version 0.0.1# 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.### MODULE IMPORTS#import osimport reimport statimport sysimport globimport datetime## CONSTANTS## Application information_APP_VERSION = '0.0.1'# Regular expressions_RE_TIMESTAMP = '\[(\d\d) ([A-Z][a-z][a-z]) (\d\d):(\d\d)\]'_RE_LINESPLIT = '^' + _RE_TIMESTAMP + ' ([^\n\r\f\v]*)$' # Group up log line_RE_SELFNICK = '--- You changed your nickname to (\S*)'_RE_NICKCHANGE = '--- (\S*) \(\S*\) changed nickname to (\S*)'_RE_QUIT = '<-- (\S*) \(\S*\) quit from IRC: ([^\t\n\r\f\v]*)$'_RE_JOIN = '--> (\S*) \([^\t\n\r\f\v)]*\) joined the channel$'_RE_PART = '<-- (\S*) \([^\t\n\r\f\v)]*\) left the channel$'_RE_MESSAGE = '<([^ \t\n\r\f\v!]*)![^\t\n\r\f\v>]*> ([^\n\r\f\v]*)$'_RE_DESCRIBE = '\* (\S*) ([^\n\r\f\v]*)'_RE_CHANNEL = '^(\S*).log$' # Parse name of channel from filename of log# File pattern matching (for glob)_GLOB_SERVERLOG = 'Server.log'_GLOB_CHANLOG = '#*.log'# Months to translate from dircproxy's format_MONTHS = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12 }## DISPLAY PROGRAM INFORMATION#print '\n\nSILAN', _APP_VERSION, '- Simple IRC Log Analyzer - http://slackforge.com/silan/'print 'Copyright (c) 2007 Shidayu <shidayu@gmail.com>'## CLASSES## UserChannelActivity: Encapsulate activity for one user in one channelclass UserChannelActivity: """Activity data for one user in one channel""" def __init__(self): """Default constructor""" self.user = None self.channel = None self.messages = [] self.describes = [] self.topics = [] def __init__(self, user, channel): """Constructor with optional user and channel instances provided""" self.user = user self.channel = channel self.messages = [] self.describes = [] self.topics = []# User: Track IRC activity for a particular userclass User: """IRC activity for a particular user""" def __init__(self): """Default constructor""" self.nick_primary = None self.nick_alternates = [] self.channels = {} self.quit_messages = [] self.words = {} def __init__(self, primary): """Constructor with optional primary nickname specified""" self.nick_primary = primary self.nick_alternates = [] self.channels = {} self.quit_messages = [] self.words = {} def is_known_as(self, alternate): """Return 1 if this IRCUser could be known as the provided argument""" if alternate == self.nick_primary or alternate in self.nick_alternates: return 1 else: return 0# Channel: Track IRC activity for a particular channelclass Channel: """IRC activity for a particular channel""" def __init__(self): """Default constructor""" self.name = None self.log = None self.users = {} self.topics = [] def __init__(self, args): """Constructor with optional arguments specified A Channel can be instantiated with a string argument representing the channel's name, or with a tuple argument in the format of (channelname, logfilename) """ if isinstance(args, tuple): self.name = args[0] self.log = Log(args[1]) else: self.name = args[0] self.log = None self.users = {} self.topics = []# StampedStatement: Topics, describes, and messages fall under this categoryclass StampedStatement: """A timestamped bit of information such as a topic, describe, and message""" def __init__(self): """Default constructor""" self.dttm = None self.text = None def __init__(self, dttm, text): """Constructor with optional date/time and text provided""" self.dttm = dttm self.text = text# HistoricalTopic: StampedStatment specifically for storing history of channel topicsclass HistoricalTopic(StampedStatement): """Extended StampedStatement including user instance for topic history storage""" def __init__(self): """Default constructor, calls parent constructor""" StampedStatement.__init__() self.user = None def __init__(self, dttm, user, text): """Constructor with optional date/time, user instance, and text provided""" StampedStatement.__init__(user, text) self.user = user# Log: About an IRC logclass Log: """IRC log file information and contents""" def __init__(self, filename): """Constructor, filename of log must be passed as an argument The log file will immediately be read into self.contents """ self.filename = filename self.dttm_start = self._find_start() self.lines = self._prepare_log() def _read_log(self): """Simply reads in a text file as a list and returns it""" loghandler = open(self.filename, 'r') loglist = loghandler.readlines() loghandler.close() return loglist def _first_line(self): """Read in the first line of the log and return it""" loghandler = open(self.filename, 'r') firstline = loghandler.readline() # readline is SINGULAR! loghandler.close() return firstline def _find_start(self): """Determines the time the log started by combining the date/time the file was created and the date/time of the first line in the log """ logstat = os.stat(self.filename) logctime = logstat.st_ctime logcdttm = datetime.datetime.fromtimestamp(logctime) logyear = logcdttm.year firststamp = re.compile('^' + _RE_TIMESTAMP + ' .*$').match( self._first_line()) # See CONSTANTS above for what's in _RE_TIMESTAMP firststampday = int(firststamp.group(1)) firststampmonth = int(month_convert(firststamp.group(2))) # Turns 'Oct' into '10' firststamphour = int(firststamp.group(3)) firststampminute = int(firststamp.group(4)) return datetime.datetime(logyear, firststampmonth, firststampday, firststamphour, firststampminute) def _prepare_log(self): """Loops through all lines in the log, splits the timestamp out, and returns a new list of StampedStatement()s with the line's text and a 'nice' date/time timestamp """ original = self._read_log() stampedlist = [] dttm_last = self.dttm_start splitregex = re.compile(_RE_LINESPLIT) # See CONSTANTS above for what's in _RE_LINESPLIT for line in original: splitmatch = splitregex.match(line) if not splitmatch: sys.exit('\nError: Could not find timestamp in log entry. Are timestamps properly enabled?\n') lineday = int(splitmatch.group(1)) linemonth = int(month_convert(splitmatch.group(2))) linehour = int(splitmatch.group(3)) lineminute = int(splitmatch.group(4)) linetext = splitmatch.group(5) # # Takes a fairly good guess as to whether a year has advanced in # the log. This may not always be accurate if, say, an entire year # to-the-minute passes since the last item inserted into the log. # if linemonth < dttm_last.month or (linemonth == dttm_last.month and (lineday < dttm_last.day or (lineday == dttm_last.day and (linehour < dttm_last.hour or (linehour == dttm_last.hour and lineminute < dttm_last.minute))))): # The preceding: Does it look like we've gone back in time? dttm_last = datetime.datetime(dttm_last.year + 1, linemonth, lineday, linehour, lineminute) else: dttm_last = datetime.datetime(dttm_last.year, linemonth, lineday, linehour, lineminute) stampedlist.append(StampedStatement(dttm_last, linetext)) return stampedlist## FUNCTIONS#def conjure_user(allusers, nick): """Return user instance a nickname belongs to. If none, one is created.""" existinguser = locate_nick(allusers, nick) if not existinguser: newuser = User(nick) allusers[nick] = newuser return newuser else: return existinguserdef locate_nick(allusers, nick): """Attempts to locate a nickname and return its User instance. If not there, None.""" if nick in allusers: return allusers[nick] else: for user in allusers: if allusers[user].nick_primary == nick or nick in allusers[user].nick_alternates: return allusers[user] return Nonedef month_convert(month): """Takes in three-letter month from dircproxy logs and returns two-digit number.""" return _MONTHS[month]def find_channels(): """Searches current working directory for all channel logs and returns a Dictionary of Channel()s based on this. """ chanlogs = glob.glob(_GLOB_CHANLOG) # See CONSTANTS above for what's in _GLOB_CHANLOG # Remove logs which are not regular files for filename in chanlogs: filestat = os.stat(filename) filemode = filestat.st_mode if not stat.S_ISREG(filemode): chanlogs.remove(filename) # Return None if there are... none if len(chanlogs) < 1: return None else: chandict = {} chanregex = re.compile(_RE_CHANNEL) for filename in chanlogs: channame = chanregex.match(filename).group(1) chandict[channame] = Channel((channame, filename)) return chandict## INITIALIZATION########### The below is for testing, wahahaha!########## Right now, some classes and functions aren't being tested and used########## That will come later :Dchannels = find_channels()for line in channels['#C&T'].log.lines: print line.dttm, line.text