#!/usr/bin/env python # Time-stamp: <2003-04-02 22:35:03 crabbkw> # Code and design by Casey Crabb (crabbkw@nafai.dyndns.org) # This code is licensed under the BSD license. # See the LICENSE file for details # # Copyright Casey Crabb (crabbkw@nafai.dyndns.org) July 2001 # # from __future__ import nested_scopes # from threading import * # from IMCom import IMCom, errorCodes from Preferences import Preferences try: from xml.parsers import expat except: from xml.parsers import pyexpat expat = pyexpat import xml import string import re #import socket import os import time import sys import getpass import operator import types import signal import traceback import codecs import locale # import SocketWrapper #try: # import readline # READLINE = 1 #except: # READLINE = 0 black = chr(27)+"[30;1m" red = chr(27)+"[31;1m" green = chr(27)+"[32;1m" yellow = chr(27)+"[33;1m" blue = chr(27)+"[34;1m" purple = chr(27)+"[35;1m" cyan = chr(27)+"[36;1m" white = chr(27)+"[37;1m" # # this thing is one big hack.... # 1) xml files are contained within one large tag, so we # simulate that by adding a tag to the beginning # and end of the file # # 2) we had a bug a while ago that made badly-formed XML files. # Fortunately we can fix those before the parser sees them # # some people use buffering for performance - we use it to lie :-) # class FileWrapper: def __init__(self,file,encoding): self.file = file self.topSent = 0 self.eof = 0 self.leftover = "\n" self.context = "" self.encoding = encoding def read(self,bytes): if self.eof: return "" #bytes = 15 # # if we have enough in the leftover buffer the use that... # if( len( self.leftover ) >= bytes ): out = self.leftover[:bytes] self.leftover = self.leftover[bytes:] out = out.encode(self.encoding) return out # # read the max. # of bytes we could need from the file.... # out = self.leftover + self.file.read( bytes - len(self.leftover) ) self.leftover = "" # # here we fix that log format problem.... # we use substring instead of regex because 1) we can # and 2) it's faster # # we search for a 2 char. substring "m'" # (the ' we escape before writing to the message, so that # guarantees that our match will be inside a tag, and nowhere # inside a tag should we have ' not preceded by an =) # # it will fail if the match falls on a buffer boundary. To fix # this we just have to keep one character of context from the # last buffer to temporarily prepend to our new buffer, which # is what we use self.context for.... # out = string.replace( self.context + out, "m'", "m='" ) # remove the context from the final output... out = out[len(self.context):] # # a sad and sorry excuse for a unicode hack # the xmlparser code apparently can't deal w/unicode # embedded in UTF-8 text, so we get rid of anything # that might confuse it. Hopefully we can take this out # someday.... # #for x in range( len( out ) ): # if( ord( out[x] ) > 127 ): # out = out[:x] + " " + out[(x+1):] # #print ord( out[x] ) # # is out too big now? # if( len( out ) > bytes ): self.leftover = out[bytes:] out = out[:bytes] # # did we hit the end of the file? close the "top-level" tag # # *** NOTE *** # THIS IS A CRASH WAITING TO HAPPEN # it's a bad idea to assume the last call to read() will ask for # at least 8 bytes...... # if (out == ""): self.eof = 1 out = "\n\n" # # save our context if there is any.... # if ( out != "" ): self.context = out[-1:] out = out.encode(self.encoding) return out def getTerminalWidth(): terminalWidth = 80 #linelength class LogHandler: def __init__(self,cli,profile): if (cli == None): self.standalone = 1 else: self.cli = cli self.standalone = 0 self.messages = [] self.JIDhash = {} self.NONE = 0 self.IGNORE = 1 self.STORE = 2 self.expatstatus = self.NONE # # precompile the isInt re() # self.int_re = re.compile( r"^(\d+)$" ) # # search parameters # self.mintime = -1 self.maxtime = -1 self.content = "" self.nick = "" self.maxmessages = 0 self.ignorecase = 0 # # our user-friendly memory # self.last_mintime = -1 self.last_maxtime = -1 self.last_content = "" self.last_nick = "*" self.last_maxmessages = 0 # # the message in its pre-parsed form # self.message_time = 0 self.message_from = "" self.message_message = "" self.current_log = "" def initLogHandler(self, profile): # saves me a bit of coding complexity # we'll see if this has to be fixed later.... self.profile = profile self.colors = self.profile.sessioncolors def readLogs( self, nick, mintime, maxtime, content, maxmessages, interactive ): if( interactive and ( mintime == None ) and ( maxtime == None ) and \ ( content == None ) and ( maxmessages == None ) ): nick, mintime, maxtime, content, maxmessages = self.getArgs( \ nick, None, None, None, None ) if( nick == -1 ): # # data validation error. go home. # return [] if( mintime == None ): mintime = -1 if( maxtime == None ): maxtime = -1 if( content == None ): content = "" if( maxmessages == None ): maxmessages = 0 if( not self.isInt( maxmessages ) ): self.output( self.colors["error"] + "ERROR: " +\ self.colors["status"] + maxmessages +\ self.colors["desc"] + ": Not a number" + \ self.colors["default"] ) return [] # # here we parse tha arguments # self.content = content self.nick = nick self.mintime = self.parseDate( mintime ) self.maxtime = self.parseDate( maxtime ) self.maxmessages = int( maxmessages ) if( ( self.mintime == -2 ) or ( self.maxtime == -2 ) ): return None self.last_mintime = self.mintime self.last_maxtime = self.maxtime self.last_content = self.content self.last_nick = self.nick self.last_maxmessages = self.maxmessages return self.doParsing() def getArgs( self, nick, mintime, maxtime, content, maxmessages ): if( nick == None ): nick = self.last_nick if( nick == "*" ): nick = self.askuser( "Log to search", "ALL/*", 0 ) else: nick = self.askuser( "Log to search", nick, 0 ) if( nick == "ALL/*" ): nick = "*" if( nick == "" ): self.output( self.colors["error"] + "I need to have a nick!" \ + self.colors["default"] ) return -1, None, None, None, None if( mintime == None ): mintime = self.last_mintime if( mintime == -1 ): mintime = self.askuser( "Start time for search", "NONE/-1", 0 ) else: mintime = self.askuser( "Start time for search", \ self.getDateTime( mintime ), 0 ) if( ( mintime != "NONE/-1" ) ): tmp = self.parseDate( mintime ) if( tmp == -2 ): return -1, None, None, None, None if( tmp == -1 ): self.output( "Earliest date" ) else: self.output( self.getDateTime( tmp ) ) else: self.output( "Earliest date" ) mintime = -1 if( maxtime == None ): maxtime = self.last_maxtime if( maxtime == -1 ): maxtime = self.askuser( "End time for search", "NOW/-1", 0 ) else: maxtime = self.askuser( "End time for search", \ self.getDateTime( maxtime ), 0 ) if( maxtime != "NOW/-1" ): tmp = self.parseDate( maxtime ) if( tmp == -2 ): return -1, None, None, None, None self.output( self.getDateTime( tmp ) ) else: self.output( "Current time" ) maxtime = -1 if( content == None ): content = self.last_content if( content == "" ): content = self.askuser( "Text to search for", "", 1 ) else: content = self.askuser( "Last search text was " + \ self.colors["status"] +\ content + self.colors["default"] + "\n" +\ "Text to search for", "", 1 ) if( maxmessages == None ): maxmessages = self.last_maxmessages if( maxmessages == 0 ): maxmessages = self.askuser( "Number of messages to show", \ "NO LIMIT/0", 0 ) else: maxmessages = self.askuser( "Number of messages to show", \ maxmessages, 0 ) if( maxmessages == "NO LIMIT/0" ): maxmessages = 0 return nick, mintime, maxtime, content, maxmessages def getDateTime(self, timedate): if( timedate == -1 ): timedate = time.time() return time.strftime('%m/%d/%Y',time.localtime(timedate)) +\ " " + time.strftime('%H:%M:%S',time.localtime(timedate)) def askuser( self, prompt, default, emptyIsValid ): if( not self.standalone and hasattr( self.cli, "askuser" ) ): self.cli.askuser( prompt, default, emptyIsValid ) else: if( emptyIsValid ): return raw_input( prompt + " > " ) else: out = raw_input( prompt + " (" + str(default) + ") > " ) if( out == "" ): return default else: return out def doParsing( self ): if( self.content != "" ): if( string.lower( self.content ) == self.content ): self.ignorecase = 1 else: self.ignorecase = 0 hour,min,sec = string.split( attrs[x], ":" ) # # get a directory listing of the log dir... # try: os.makedirs(self.profile.logdir,0700) except: pass dir = os.listdir( self.profile.logdir ) nickstmp = string.split( self.nick, "," ) nicks = [] for n in nickstmp: if( string.find( n, "*" ) != -1 ): # # if it's a jid we only want to match jids # vice-versa for nicks # if( string.find( n, "@" ) == -1 ): nicksonly = 1 else: nicksonly = 0 n = string.replace( n, "*", ".*?" ) wildcard_re = re.compile( "^" + n + "$" ) for f in dir: if( not nicksonly and string.find( f, "@" ) == -1 ): continue if( nicksonly and string.find( f, "@" ) != -1 ): continue if( wildcard_re.search( f ) != None ): nicks.append( f ) else: nicks.append( n ) messages = [] for n in nicks: try: self.messages = [] #f = codecs.open( self.profile.logdir + "/" + self.getJID( n ), "r", self.profile.encoding, 'replace') fileToOpen = self.profile.logdir + "/" + self.getJID( n ) #print "fileToOpen isinstance(unicode): ", isinstance(fileToOpen, unicode) fileToOpen = fileToOpen.encode(self.profile.encoding) f = codecs.open( fileToOpen, "r", self.profile.encoding) self.current_log = self.getJID( n ) filewrapper = FileWrapper(f, self.profile.encoding) # # have to init a different parser for each doc. # parser = expat.ParserCreate( self.profile.encoding ) parser.StartElementHandler = self.startElement parser.EndElementHandler = self.endElement parser.CharacterDataHandler = self.characters parser.ParseFile(filewrapper) messages.extend( self.messages ) except IOError: output = self.colors["error"] + "ERROR: " +\ self.colors["default"] + "Could not open logfile " +\ self.colors["desc"] + self.profile.logdir + \ "/" + self.getJID(self.nick) + self.colors["default"] self.output( output ) except xml.parsers.expat.error: self.output( self.colors["error"] + "Parse error: " +\ self.colors["desc"] + self.current_log +\ ", line " + str( parser.ErrorLineNumber - 1 ) + \ " column ~" + str( parser.ErrorColumnNumber ) +\ ": " + expat.ErrorString( parser.ErrorCode ) +\ self.colors["default"] ) messages.sort() return messages def startElement(self,name,attrs): if( name != "message" ): self.expatstatus = self.IGNORE return self.message_message = "" hour = min = sec = month = day = year = 0 for x in attrs.keys(): if( x == "time" ): hour,min,sec = string.split( attrs[x], ":" ) if( x == "date" ): month,day,year = string.split( attrs[x], "/" ) if( x == "from" ): self.message_from = attrs[x] year = int( year ) month = int( month ) day = int( day ) hour = int( hour ) min = int( min ) sec = int( sec ) self.message_time = time.mktime( [year, month, day, hour, \ min, sec, 0, 0, -1] ) if( ( self.mintime != -1 ) and ( self.message_time <= self.mintime ) ): self.expatstatus = self.IGNORE return if( ( self.maxtime != -1 ) and ( self.message_time >= self.maxtime ) ): self.expatstatus = self.IGNORE return def parseDate( self, text ): # # ok, so this'll get ugly :-) # if( type( text ) == types.IntType ): return int( text ) if( type( text ) == types.FloatType ): return int( text ) #int_re = re.compile( r"^[+-]{0,1}(\d+)$" ) int_re = re.compile( r"^(\d+)$" ) if( int_re.search( str( text ) ) != None ): return int( text ) if( text == "-1" ): return int( text ) # first, fields *must* be seperated by whitespace... vals = string.split( text, " " ) if( len( vals ) > 2 ): self.output( self.colors["status"] + text + \ self.colors["default"] + ": " + self.colors["desc"] + \ "I can't understand more than two parts!" +\ self.colors["default"] ) return -2 ttup = time.localtime( time.time() ) mytime = time.mktime( [ ttup[0], ttup[1], ttup[2], 0, 0, 0, -1, -1, -1 ] ) seperator_re = re.compile( r"([;:/.-])" ) #int_re = re.compile( r"^(\d+)$" ) for v in vals: # if there's 3 fields... # both dates and times can have 3 fields. tsk.... # however, if the first or third field has 4 chars. it's a date # those formats are: yyyy-mm-dd and mm-dd-yyyy (american format) # if the 2nd field has 4 chars. we can't understand the date # seperator = seperator_re.search( v ) #if( seperator == None ): # self.output( self.colors["status"] + v + \ # self.colors["default"] + ": " + self.colors["desc"] + \ # "I can't find a seperator!" +\ # self.colors["default"] ) # return -2 if( seperator == None or v[0] == "-" ): fields = [ v ] fields_s = [ v ] else: split_re = re.compile( re.escape( seperator.group(1) ) + "+" ) fields = re.split( split_re, v ); if( len( fields ) != 1 ): for f in fields: if( int_re.search( f ) == None ): self.output( self.colors["status"] + f + \ self.colors["default"] + ": " + self.colors["desc"] + \ "Not numeric!" +\ self.colors["default"] ) return -2 else: f = int( f ) fields_s = [] for x in range( len(fields) ): # I *HATE* python sometimes! fields_s.append( fields[x] ) fields[x] = int( fields[x] ) parsed = 0 if( len( fields ) == 3 ): if( len( fields_s[0] ) == 4 and len( fields_s[1] ) <= 2 and len( fields_s[2] ) <= 2 and ( not parsed ) ): parsed = 1 # it's an ISO date! ttup = time.localtime( mytime ) mytime = time.mktime( [ fields[0], fields[1], fields[2], ttup[3], ttup[4], ttup[5], -1, -1, -1 ] ) if( not parsed and len( fields_s[0] ) <= 2 and len( fields_s[1] ) <= 2 and len( fields_s[2] ) == 4 ): parsed = 1 # it's an american-format date! ttup = time.localtime( mytime ) mytime = time.mktime( [ fields[2], fields[0], fields[1], ttup[3], ttup[4], ttup[5], -1, -1, -1 ] ) if( not parsed and len( fields_s[0] ) <= 2 and len( fields_s[1] ) <= 2 and len( fields_s[2] ) <= 2 ): parsed = 1 if( seperator.group(1) == "." or seperator.group(1) == ":" or seperator.group(1) == ";" ): # it's a time! ttup = time.localtime( mytime ) mytime = time.mktime( [ ttup[0], ttup[1], ttup[2], fields[0], fields[1], fields[2], -1, -1, -1 ] ) else: # it's an american-format date! ttup = time.localtime( mytime ) mytime = time.mktime( [ fields[2], fields[0], fields[1], ttup[3], ttup[4], ttup[5], -1, -1, -1 ] ) # # if there's 2 fields... # it could be a date assuming "this year", we assume month first # or it could be hours and minutes... # we assume time seperators are ":" and "." # date seperators are /, ., and - # if( len( fields ) == 2 ): if( len( fields_s[0] ) <= 2 and len( fields_s[1] ) <= 2 and ( not parsed ) ): parsed = 1 if( seperator.group(1) == "." or seperator.group(1) == ";" or seperator.group(1) == ":" ): # it's a time! ttup = time.localtime( mytime ) mytime = time.mktime( [ ttup[0], ttup[1], ttup[2], fields[0], fields[1], 0, -1, -1, -1 ] ) else: # it's an american-format date! ttup = time.localtime( mytime ) mytime = time.mktime( [ ttup[0], fields[0], fields[1], ttup[3], ttup[4], ttup[5], -1, -1, -1 ] ) # # if there's only 1 field... # it *must* be one of our "shortcut" specifications # this means that at the toplevel there can only be one field, too # # shortcut specs: yesterday, today, -5m (last 5 mins), # -5h (last 5 hours), -59 (last 59 minutes), # -59m (last 59 minutes), -5d (last 5 days), # if( len( fields ) == 1 ): fields[0] = string.lower( fields[0] ) if( string.lower( fields[0] ) == "yesterday" ): parsed = 1 mytime = time.time() - 86400; ttup = time.localtime( mytime ) mytime = time.mktime( [ ttup[0], ttup[1], ttup[2], 0, 0, 0, -1, -1, -1 ] ) if( string.lower( fields[0] ) == "today" ): parsed = 1 mytime = time.time(); ttup = time.localtime( mytime ) mytime = time.mktime( [ ttup[0], ttup[1], ttup[2], 23, 59, 59, -1, -1, -1 ] ) if( string.lower( fields[0] ) == "now" ): parsed = 1 # breaking the rules here..... return -1 matches = re.compile( r"^-(\d+\.{0,1}\d*)([dhm])$" ).\ search( fields[0] ) if( matches != None ): parsed = 1 multdict = { "d" : 86400, "h" : 3600, "m" : 60 }; mytime = time.time() - ( float( matches.group( 1 ) ) * int( multdict[matches.group( 2 )] ) ) if( not parsed ): self.output( self.colors["status"] + text + \ self.colors["default"] + ": " + self.colors["desc"] + \ "I don't understand!" +\ self.colors["default"] ) return -2 return mytime def endElement(self,name): if( self.expatstatus == self.IGNORE ): self.expatstatus = self.NONE return if( name != "message" ): return if( self.ignorecase ): if( ( self.content != "" ) and ( string.find( \ string.lower( self.message_message ), string.lower( self.content ) ) == -1 ) ): return else: if( ( self.content != "" ) and ( string.find( \ self.message_message, self.content ) == -1 ) ): return if( self.message_from == self.profile.user + "@" + \ self.profile.server ): self.message_to = self.current_log else: self.JIDhash[self.message_from] = self.current_log self.message_to = self.profile.user + "@" + \ self.profile.server self.messages.append( [self.message_time, \ self.message_from, \ self.message_to, \ self.message_message] ) if( ( self.maxmessages != 0 ) and \ ( len( self.messages ) > self.maxmessages ) ): self.messages.pop(0) def characters(self,data): if( self.expatstatus == self.IGNORE ): return self.message_message = self.message_message + data def setProfile( self, profile ): self.profile = profile self.colors = self.profile.sessioncolors def getNick(self, jid): if self.standalone: if( self.JIDhash.has_key( jid ) ): return self.JIDhash[jid] return jid else: return self.cli.getNick(jid) def getJID(self, nick): if self.standalone: return nick else: return self.cli.getJID(nick)[0] def setParent(self, cli): self.cli = cli def output(self,message): if self.standalone: print message.encode('ISO-8859-1','replace') else: self.cli.output( message ) def isInt( self, v ): if( type( v ) == types.IntType ): return 1 if( self.int_re.search( v ) == None ): return 0 else: return 1 def displayMessage( self, message ): time, fromjid, tojid, text = message tojid = self.getNick( tojid ) fromjid = self.getNick( fromjid ) time = self.getDateTime( time ) self.output( self.colors["user"] + fromjid + \ self.colors["default"] + " -> " + \ self.colors["user"] + tojid + \ self.colors["default"] + " - " + \ self.colors["time"] + "LOGGED" + \ self.colors["default"] + " Message - " + \ self.colors["time"] + time + \ self.colors["messagebody"] + "\n" + text + \ self.colors["default"] ) if(__name__ == "__main__"): global loghandler # locale.setlocale(locale.LC_ALL, "de") #("US","ISO-8859-1")) getTerminalWidth() prefs = Preferences() profile = prefs.getDefaultProfile() loghandler = LogHandler(None, profile) profile_n = loghandler.askuser( "Profile", profile.name, 0 ) profile = prefs.getProfile( profile_n ) loghandler.setProfile( profile ) messages = loghandler.readLogs( None, None, None, None, None, 1 ) for x in messages: loghandler.displayMessage( x ) sys.exit(0)