#!/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 # # 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 os import re import stat import sys import glob import 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 ' # # CLASSES # # UserChannelActivity: Encapsulate activity for one user in one channel class 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 user class 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 channel class 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 category class 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 topics class 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 log class 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 existinguser def 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 None def 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 :D channels = find_channels() for line in channels['#C&T'].log.lines: print line.dttm, line.text