#!/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)