/* historylogger.cpp Copyright (c) 2003-2004 by Olivier Goffart Kopete (c) 2003-2004 by the Kopete developers ************************************************************************* * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ************************************************************************* */ #include "historylogger.h" #include "historyconfig.h" #include #include #include #include #include #include #include #include #include #include "kopeteglobal.h" #include "kopetecontact.h" #include "kopeteprotocol.h" #include "kopeteaccount.h" #include "kopetemetacontact.h" #include "kopetechatsession.h" // ----------------------------------------------------------------------------- HistoryLogger::HistoryLogger( Kopete::MetaContact *m, TQObject *parent, const char *name ) : TQObject(parent, name) { m_saveTimer=0L; m_saveTimerTime=0; m_metaContact=m; m_hideOutgoing=false; m_cachedMonth=-1; m_realMonth=TQDate::currentDate().month(); m_oldSens=Default; //the contact may be destroyed, for example, if the contact changes its metacontact connect(m_metaContact , TQ_SIGNAL(destroyed(TQObject *)) , this , TQ_SLOT(slotMCDeleted())); setPositionToLast(); } HistoryLogger::HistoryLogger( Kopete::Contact *c, TQObject *parent, const char *name ) : TQObject(parent, name) { m_saveTimer=0L; m_saveTimerTime=0; m_cachedMonth=-1; m_metaContact=c->metaContact(); m_hideOutgoing=false; m_realMonth=TQDate::currentDate().month(); m_oldSens=Default; //the contact may be destroyed, for example, if the contact changes its metacontact connect(m_metaContact , TQ_SIGNAL(destroyed(TQObject *)) , this , TQ_SLOT(slotMCDeleted())); setPositionToLast(); } HistoryLogger::~HistoryLogger() { if(m_saveTimer && m_saveTimer->isActive()) saveToDisk(); } void HistoryLogger::setPositionToLast() { setCurrentMonth(0); m_oldSens = AntiChronological; m_oldMonth=0; m_oldElements.clear(); } void HistoryLogger::setPositionToFirst() { setCurrentMonth( getFirstMonth() ); m_oldSens = Chronological; m_oldMonth=m_currentMonth; m_oldElements.clear(); } void HistoryLogger::setCurrentMonth(int month) { m_currentMonth = month; m_currentElements.clear(); } TQDomDocument HistoryLogger::getDocument(const Kopete::Contact *c, unsigned int month , bool canLoad , bool* contain) { if(m_realMonth!=TQDate::currentDate().month()) { //We changed month, our indice are not correct anymore, clean memory. // or we will see what i called "the 31 midnight bug"(TM) :-) -Olivier m_documents.clear(); m_cachedMonth=-1; m_currentMonth++; //Not usre it's ok, but should work; m_oldMonth++; // idem m_realMonth=TQDate::currentDate().month(); } if(!m_metaContact) { //this may happen if the contact has been moved, and the MC deleted if(c && c->metaContact()) m_metaContact=c->metaContact(); else return TQDomDocument(); } if(!m_metaContact->contacts().contains(c)) { if(contain) *contain=false; return TQDomDocument(); } TQMap documents = m_documents[c]; if (documents.contains(month)) return documents[month]; TQDomDocument doc = getDocument(c, TQDate::currentDate().addMonths(0-month), canLoad, contain); documents.insert(month, doc); m_documents[c]=documents; return doc; } TQDomDocument HistoryLogger::getDocument(const Kopete::Contact *c, const TQDate date , bool canLoad , bool* contain) { if(!m_metaContact) { //this may happen if the contact has been moved, and the MC deleted if(c && c->metaContact()) m_metaContact=c->metaContact(); else return TQDomDocument(); } if(!m_metaContact->contacts().contains(c)) { if(contain) *contain=false; return TQDomDocument(); } if(!canLoad) { if(contain) *contain=false; return TQDomDocument(); } TQString FileName = getFileName(c, date); TQDomDocument doc( "Kopete-History" ); TQFile file( FileName ); if ( !file.open( IO_ReadOnly ) ) { if(contain) *contain=false; return doc; } if ( !doc.setContent( &file ) ) { file.close(); if(contain) *contain=false; return doc; } file.close(); if(contain) *contain=true; return doc; } void HistoryLogger::appendMessage( const Kopete::Message &msg , const Kopete::Contact *ct ) { if(!msg.from()) return; // If no contact are given: If the manager is availiable, use the manager's // first contact (the channel on irc, or the other contact for others protocols const Kopete::Contact *c = ct; if(!c && msg.manager() ) { TQPtrList mb=msg.manager()->members() ; c = mb.first(); } if(!c) //If the contact is still not initialized, use the message author. c = msg.direction()==Kopete::Message::Outbound ? msg.to().first() : msg.from() ; if(!m_metaContact) { //this may happen if the contact has been moved, and the MC deleted if(c && c->metaContact()) m_metaContact=c->metaContact(); else return; } if(!c || !m_metaContact->contacts().contains(c) ) { /*TQPtrList contacts= m_metaContact->contacts(); TQPtrListIterator it( contacts ); for( ; it.current(); ++it ) { if( (*it)->protocol()->pluginId() == msg.from()->protocol()->pluginId() ) { c=*it; break; } }*/ //if(!c) kdWarning(14310) << k_funcinfo << "No contact found in this metacontact to" << " append in the history" << endl; return; } TQDomDocument doc=getDocument(c,0); TQDomElement docElem = doc.documentElement(); if(docElem.isNull()) { docElem= doc.createElement( "kopete-history" ); docElem.setAttribute ( "version" , "0.9" ); doc.appendChild( docElem ); TQDomElement headElem = doc.createElement( "head" ); docElem.appendChild( headElem ); TQDomElement dateElem = doc.createElement( "date" ); dateElem.setAttribute( "year", TQString::number(TQDate::currentDate().year()) ); dateElem.setAttribute( "month", TQString::number(TQDate::currentDate().month()) ); headElem.appendChild(dateElem); TQDomElement myselfElem = doc.createElement( "contact" ); myselfElem.setAttribute( "type", "myself" ); myselfElem.setAttribute( "contactId", c->account()->myself()->contactId() ); headElem.appendChild(myselfElem); TQDomElement contactElem = doc.createElement( "contact" ); contactElem.setAttribute( "contactId", c->contactId() ); headElem.appendChild(contactElem); } TQDomElement msgElem = doc.createElement( "msg" ); msgElem.setAttribute( "in", msg.direction()==Kopete::Message::Outbound ? "0" : "1" ); msgElem.setAttribute( "from", msg.from()->contactId() ); msgElem.setAttribute( "nick", msg.from()->property( Kopete::Global::Properties::self()->nickName() ).value().toString() ); //do we have to set this? msgElem.setAttribute( "time", msg.timestamp().toString("d h:m:s") ); TQDomText msgNode = doc.createTextNode( msg.plainBody() ); docElem.appendChild( msgElem ); msgElem.appendChild( msgNode ); // I'm temporizing the save. // On hight-traffic channel, saving can take lots of CPU. (because the file is big) // So i wait a time proportional to the time needed to save.. const TQString filename=getFileName(c,TQDate::currentDate()); if(!m_toSaveFileName.isEmpty() && m_toSaveFileName != filename) { //that mean the contact or the month has changed, save it now. saveToDisk(); } m_toSaveFileName=filename; m_toSaveDocument=doc; if(!m_saveTimer) { m_saveTimer=new TQTimer(this); connect( m_saveTimer, TQ_SIGNAL( timeout() ) , this, TQ_SLOT(saveToDisk()) ); } if(!m_saveTimer->isActive()) m_saveTimer->start( m_saveTimerTime, true /*singleshot*/ ); } void HistoryLogger::saveToDisk() { if(m_saveTimer) m_saveTimer->stop(); if(m_toSaveFileName.isEmpty() || m_toSaveDocument.isNull()) return; TQTime t; t.start(); //mesure the time needed to save. KSaveFile file( m_toSaveFileName ); if( file.status() == 0 ) { TQTextStream *stream = file.textStream(); //stream->setEncoding( TQTextStream::UnicodeUTF8 ); //???? oui ou non? m_toSaveDocument.save( *stream, 1 ); file.close(); m_saveTimerTime=TQMIN(t.elapsed()*1000, 300000); //a time 1000 times supperior to the time needed to save. but with a upper limit of 5 minutes //on a my machine, (2.4Ghz, but old HD) it should take about 10 ms to save the file. // So that would mean save every 10 seconds, which seems to be ok. // But it may take 500 ms if the file to save becomes too big (1Mb). kdDebug(14310) << k_funcinfo << m_toSaveFileName << " saved in " << t.elapsed() << " ms " < HistoryLogger::readMessages(TQDate date) { TQRegExp rxTime("(\\d+) (\\d+):(\\d+)($|:)(\\d*)"); //(with a 0.7.x compatibility) TQValueList messages; TQPtrList ct=m_metaContact->contacts(); TQPtrListIterator it( ct ); for( ; it.current(); ++it ) { TQDomDocument doc=getDocument(*it,date, true, 0L); TQDomElement docElem = doc.documentElement(); TQDomNode n = docElem.firstChild(); while(!n.isNull()) { TQDomElement msgElem2 = n.toElement(); if( !msgElem2.isNull() && msgElem2.tagName()=="msg") { rxTime.search(msgElem2.attribute("time")); TQDateTime dt( TQDate(date.year() , date.month() , rxTime.cap(1).toUInt()), TQTime( rxTime.cap(2).toUInt() , rxTime.cap(3).toUInt(), rxTime.cap(5).toUInt() ) ); if (dt.date() != date) { n = n.nextSibling(); continue; } Kopete::Message::MessageDirection dir = (msgElem2.attribute("in") == "1") ? Kopete::Message::Inbound : Kopete::Message::Outbound; if(!m_hideOutgoing || dir != Kopete::Message::Outbound) { //parse only if we don't hide it TQString f=msgElem2.attribute("from" ); const Kopete::Contact *from=f.isNull()? 0L : (*it)->account()->contacts()[f]; if(!from) from= dir==Kopete::Message::Inbound ? (*it) : (*it)->account()->myself(); Kopete::ContactPtrList to; to.append( dir==Kopete::Message::Inbound ? (*it)->account()->myself() : *it ); Kopete::Message msg(dt, from, to, msgElem2.text(), dir); msg.setBody( TQString::fromLatin1("%2") .arg( dt.toString(TQt::LocalDate), msg.escapedBody() ), Kopete::Message::RichText); // We insert it at the good place, given its date TQValueListIterator msgIt; for (msgIt = messages.begin(); msgIt != messages.end(); ++msgIt) { if ((*msgIt).timestamp() > msg.timestamp()) break; } messages.insert(msgIt, msg); } } n = n.nextSibling(); } // end while on messages } return messages; } TQValueList HistoryLogger::readMessages(unsigned int lines, const Kopete::Contact *c, Sens sens, bool reverseOrder, bool colorize) { //TQDate dd = TQDate::currentDate().addMonths(0-m_currentMonth); TQValueList messages; // A regexp useful for this function TQRegExp rxTime("(\\d+) (\\d+):(\\d+)($|:)(\\d*)"); //(with a 0.7.x compatibility) if(!m_metaContact) { //this may happen if the contact has been moved, and the MC deleted if(c && c->metaContact()) m_metaContact=c->metaContact(); else return messages; } if(c && !m_metaContact->contacts().contains(c) ) return messages; if(sens ==0 ) //if no sens are selected, just continue in the previous sens sens = m_oldSens ; if( m_oldSens != 0 && sens != m_oldSens ) { //we changed our sens! so retrieve the old position to fly in the other way m_currentElements= m_oldElements; m_currentMonth=m_oldMonth; } else { m_oldElements=m_currentElements; m_oldMonth=m_currentMonth; } m_oldSens=sens; //getting the color for messages: TQColor fgColor = HistoryConfig::history_color(); //Hello guest! //there are two algoritms: // - if a contact is given, or the metacontact contain only one contact, just read the history. // - else, merge the history //the merging algoritm is the following: // we see what contact we have to read first, and we look at the firt date before another contact // has a message with a bigger date. TQDateTime timeLimit; const Kopete::Contact *currentContact=c; if(!c && m_metaContact->contacts().count()==1) currentContact=m_metaContact->contacts().first(); else if(!c && m_metaContact->contacts().count()== 0) { return messages; } while(messages.count() < lines) { timeLimit=TQDateTime(); TQDomElement msgElem; //here is the message element TQDateTime timestamp; //and the timestamp of this message if(!c && m_metaContact->contacts().count()>1) { //we have to merge the differents subcontact history TQPtrList ct=m_metaContact->contacts(); TQPtrListIterator it( ct ); for( ; it.current(); ++it ) { //we loop over each contact. we are searching the contact with the next message with the smallest date, // it will becomes our current contact, and the contact with the mext message with the second smallest // date, this date will bocomes the limit. TQDomNode n; if(m_currentElements.contains(*it)) n=m_currentElements[*it]; else //there is not yet "next message" register, so we will take the first (for the current month) { TQDomDocument doc=getDocument(*it,m_currentMonth); TQDomElement docElem = doc.documentElement(); n= (sens==Chronological)?docElem.firstChild() : docElem.lastChild(); //i can't drop the root element workaround.append(docElem); } while(!n.isNull()) { TQDomElement msgElem2 = n.toElement(); if( !msgElem2.isNull() && msgElem2.tagName()=="msg") { rxTime.search(msgElem2.attribute("time")); TQDate d=TQDate::currentDate().addMonths(0-m_currentMonth); TQDateTime dt( TQDate(d.year() , d.month() , rxTime.cap(1).toUInt()), TQTime( rxTime.cap(2).toUInt() , rxTime.cap(3).toUInt(), rxTime.cap(5).toUInt() ) ); if(!timestamp.isValid() || ((sens==Chronological )? dt < timestamp : dt > timestamp) ) { timeLimit=timestamp; timestamp=dt; msgElem=msgElem2; currentContact=*it; } else if(!timeLimit.isValid() || ((sens==Chronological) ? timeLimit > dt : timeLimit < dt) ) { timeLimit=dt; } break; } n=(sens==Chronological)? n.nextSibling() : n.previousSibling(); } } } else //we don't have to merge the history. just take the next item in the contact { if(m_currentElements.contains(currentContact)) msgElem=m_currentElements[currentContact]; else { TQDomDocument doc=getDocument(currentContact,m_currentMonth); TQDomElement docElem = doc.documentElement(); TQDomNode n= (sens==Chronological)?docElem.firstChild() : docElem.lastChild(); msgElem=TQDomElement(); while(!n.isNull()) //continue until we get a msg { msgElem=n.toElement(); if( !msgElem.isNull() && msgElem.tagName()=="msg") { m_currentElements[currentContact]=msgElem; break; } n=(sens==Chronological)? n.nextSibling() : n.previousSibling(); } //i can't drop the root element workaround.append(docElem); } } if(msgElem.isNull()) //we don't find ANY messages in any contact for this month. so we change the month { if(sens==Chronological) { if(m_currentMonth <= 0) break; //there are no other messages to show. break even if we don't have nb messages setCurrentMonth(m_currentMonth-1); } else { if(m_currentMonth >= getFirstMonth(c)) break; //we don't have any other messages to show setCurrentMonth(m_currentMonth+1); } continue; //begin the loop from the bottom, and find currentContact and timeLimit again } while( (messages.count() < lines) && !msgElem.isNull() && (!timestamp.isValid() || !timeLimit.isValid() || ((sens==Chronological) ? timestamp <= timeLimit : timestamp >= timeLimit) )) { // break this loop, if we have reached the correct number of messages, // if there are no more messages for this contact, or if we reached // the timeLimit msgElem is the next message, still not parsed, so // we parse it now Kopete::Message::MessageDirection dir = (msgElem.attribute("in") == "1") ? Kopete::Message::Inbound : Kopete::Message::Outbound; if(!m_hideOutgoing || dir != Kopete::Message::Outbound) { //parse only if we don't hide it if( m_filter.isNull() || ( m_filterRegExp? msgElem.text().contains(TQRegExp(m_filter,m_filterCaseSensitive)) : msgElem.text().contains(m_filter,m_filterCaseSensitive) )) { TQString f=msgElem.attribute("from" ); const Kopete::Contact *from=(f.isNull() || !currentContact) ? 0L : currentContact->account()->contacts()[f]; if(!from) from= dir==Kopete::Message::Inbound ? currentContact : currentContact->account()->myself(); Kopete::ContactPtrList to; to.append( dir==Kopete::Message::Inbound ? currentContact->account()->myself() : currentContact ); if(!timestamp.isValid()) { //parse timestamp only if it was not already parsed rxTime.search(msgElem.attribute("time")); TQDate d=TQDate::currentDate().addMonths(0-m_currentMonth); timestamp=TQDateTime( TQDate(d.year() , d.month() , rxTime.cap(1).toUInt()), TQTime( rxTime.cap(2).toUInt() , rxTime.cap(3).toUInt() , rxTime.cap(5).toUInt() ) ); } Kopete::Message msg(timestamp, from, to, msgElem.text(), dir); if (colorize) { msg.setBody( TQString::fromLatin1("%3") .arg( fgColor.name(), timestamp.toString(TQt::LocalDate), msg.escapedBody() ), Kopete::Message::RichText ); msg.setFg( fgColor ); } else { msg.setBody( TQString::fromLatin1("%2") .arg( timestamp.toString(TQt::LocalDate), msg.escapedBody() ), Kopete::Message::RichText ); } if(reverseOrder) messages.prepend(msg); else messages.append(msg); } } //here is the point of workaround. If i drop the root element, this crashes //get the next message TQDomNode node = ( (sens==Chronological) ? msgElem.nextSibling() : msgElem.previousSibling() ); msgElem = TQDomElement(); //n.toElement(); while (!node.isNull() && msgElem.isNull()) { msgElem = node.toElement(); if (!msgElem.isNull()) { if (msgElem.tagName() == "msg") { if (!c && (m_metaContact->contacts().count() > 1)) { // In case of hideoutgoing messages, it is faster to do // this, so we don't parse the date if it is not needed TQRegExp rx("(\\d+) (\\d+):(\\d+):(\\d+)"); rx.search(msgElem.attribute("time")); TQDate d = TQDate::currentDate().addMonths(0-m_currentMonth); timestamp = TQDateTime( TQDate(d.year(), d.month(), rx.cap(1).toUInt()), TQTime( rx.cap(2).toUInt(), rx.cap(3).toUInt() ) ); } else timestamp = TQDateTime(); //invalid } else msgElem = TQDomElement(); } node = (sens == Chronological) ? node.nextSibling() : node.previousSibling(); } m_currentElements[currentContact]=msgElem; //this is the next message } } if(messages.count() < lines) m_currentElements.clear(); //current elements are null this can't be allowed return messages; } TQString HistoryLogger::getFileName(const Kopete::Contact* c, TQDate date) { TQString name = c->protocol()->pluginId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ) + TQString::fromLatin1( "/" ) + c->account()->accountId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ) + TQString::fromLatin1( "/" ) + c->contactId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ) + date.toString(".yyyyMM"); TQString filename=locateLocal( "data", TQString::fromLatin1( "kopete/logs/" ) + name+ TQString::fromLatin1( ".xml" ) ) ; //Check if there is a kopete 0.7.x file TQFileInfo fi(filename); if(!fi.exists()) { name = c->protocol()->pluginId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ) + TQString::fromLatin1( "/" ) + c->contactId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ) + date.toString(".yyyyMM"); TQString filename2=locateLocal( "data", TQString::fromLatin1( "kopete/logs/" ) + name+ TQString::fromLatin1( ".xml" ) ) ; TQFileInfo fi2(filename2); if(fi2.exists()) return filename2; } return filename; } unsigned int HistoryLogger::getFirstMonth(const Kopete::Contact *c) { if(!c) return getFirstMonth(); TQRegExp rx( "\\.(\\d\\d\\d\\d)(\\d\\d)" ); TQFileInfo *fi; // BEGIN check if there are Kopete 0.7.x TQDir d1(locateLocal("data",TQString("kopete/logs/")+ c->protocol()->pluginId().replace( TQRegExp(TQString::fromLatin1("[./~?*]")),TQString::fromLatin1("-")) )); d1.setFilter( TQDir::Files | TQDir::NoSymLinks ); d1.setSorting( TQDir::Name ); const TQFileInfoList *list1 = d1.entryInfoList(); TQFileInfoListIterator it1( *list1 ); while ( (fi = it1.current()) != 0 ) { if(fi->fileName().contains(c->contactId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ))) { rx.search(fi->fileName()); int result = 12*(TQDate::currentDate().year() - rx.cap(1).toUInt()) +TQDate::currentDate().month() - rx.cap(2).toUInt(); if(result < 0) { kdWarning(14310) << k_funcinfo << "Kopete only found log file from Kopete 0.7.x made in the future. Check your date!" << endl; break; } return result; } ++it1; } // END of kopete 0.7.x check TQDir d(locateLocal("data",TQString("kopete/logs/")+ c->protocol()->pluginId().replace( TQRegExp(TQString::fromLatin1("[./~?*]")),TQString::fromLatin1("-")) + TQString::fromLatin1( "/" ) + c->account()->accountId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ) )); d.setFilter( TQDir::Files | TQDir::NoSymLinks ); d.setSorting( TQDir::Name ); const TQFileInfoList *list = d.entryInfoList(); TQFileInfoListIterator it( *list ); while ( (fi = it.current()) != 0 ) { if(fi->fileName().contains(c->contactId().replace( TQRegExp( TQString::fromLatin1( "[./~?*]" ) ), TQString::fromLatin1( "-" ) ))) { rx.search(fi->fileName()); int result = 12*(TQDate::currentDate().year() - rx.cap(1).toUInt()) +TQDate::currentDate().month() - rx.cap(2).toUInt(); if(result < 0) { kdWarning(14310) << k_funcinfo << "Kopete only found log file made in the future. Check your date!" << endl; break; } return result; } ++it; } return 0; } unsigned int HistoryLogger::getFirstMonth() { if(m_cachedMonth!=-1) return m_cachedMonth; if(!m_metaContact) return 0; int m=0; TQPtrList contacts=m_metaContact->contacts(); TQPtrListIterator it( contacts ); for( ; it.current(); ++it ) { int m2=getFirstMonth(*it); if(m2>m) m=m2; } m_cachedMonth=m; return m; } void HistoryLogger::setHideOutgoing(bool b) { m_hideOutgoing = b; } void HistoryLogger::slotMCDeleted() { m_metaContact = 0; } void HistoryLogger::setFilter(const TQString& filter, bool caseSensitive , bool isRegExp) { m_filter=filter; m_filterCaseSensitive=caseSensitive; m_filterRegExp=isRegExp; } TQString HistoryLogger::filter() const { return m_filter; } bool HistoryLogger::filterCaseSensitive() const { return m_filterCaseSensitive; } bool HistoryLogger::filterRegExp() const { return m_filterRegExp; } TQValueList HistoryLogger::getDaysForMonth(TQDate date) { TQRegExp rxTime("time=\"(\\d+) \\d+:\\d+(:\\d+)?\""); //(with a 0.7.x compatibility) TQValueList dayList; TQPtrList contacts = m_metaContact->contacts(); TQPtrListIterator it(contacts); int lastDay=0; for(; it.current(); ++it) { // kdDebug() << getFileName(*it, date) << endl; TQFile file(getFileName(*it, date)); if(!file.open(IO_ReadOnly)) { continue; } TQTextStream stream(&file); TQString fullText = stream.read(); file.close(); int pos = 0; while( (pos = rxTime.search(fullText, pos)) != -1) { pos += rxTime.matchedLength(); int day=rxTime.capturedTexts()[1].toInt(); if ( day !=lastDay && dayList.find(day) == dayList.end()) // avoid duplicates { dayList.append(rxTime.capturedTexts()[1].toInt()); lastDay=day; } } } return dayList; } #include "historylogger.moc"