/*************************************************************************** chathistorywriter.cpp - chat logs writer ------------------- begin : Tue Jul 13 2010 copyright : (C) 2010 by Valerio Pilo email : valerio@kmess.org ***************************************************************************/ /*************************************************************************** * * * 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 "chathistorywriter.h" #include "../network/chatmessage.h" #include "../utils/kmessconfig.h" #include "../utils/kmessshared.h" #include "../kmessdebug.h" #include "chathistorymanager.h" #include "chatmessagestyle.h" #include <QDir> #include <QFileInfo> #include <QtGlobal> // Amount of bytes to read from the end of the file when searching for the closing tags #define END_SEEKING_BUFFER_SIZE 64 // Closing conversation tag #define CLOSING_TAG "</conversation>" // Closing conversation tag size (added statically since it never changes) #define CLOSING_TAG_SIZE 15 // Constructor ChatHistoryWriter::ChatHistoryWriter( const QString& account, const QString& handle ) : handle_( handle ) , isFirstMessage_( true ) , isNewLogFile_( true ) , lastMessage_( 0 ) , pointer_( 0 ) { conversation_ = new Conversation(); messageStyle_ = new ChatMessageStyle(); QString dir( KMessConfig::instance()->getAccountDirectory( account ) + "/chatlogs" ); // Ensure that the logs directory exists QDir logsDir( dir ); if( ! logsDir.exists() ) { logsDir.mkdir( dir ); // It couldn't be created if( ! logsDir.exists() ) { kmWarning() << "Unable to create logs directory" << dir << "- cannot log any message!"; return; } } QString lastFile; QString nextFile( KMessShared::nextSequentialFile( dir, handle, "xml", lastFile ) ); #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Setting up log writer"; kmDebug() << "Last logfile:" << lastFile; kmDebug() << "Next logfile:" << nextFile; #endif QFileInfo fileInfo( lastFile ); // Choose whether to continue writing on the last logfile, or to create a new one if( ! fileInfo.exists() || ! fileInfo.isWritable() || fileInfo.size() > MAX_LOG_FILE_SIZE ) { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Using new file" << nextFile << "to flush content"; #endif isNewLogFile_ = true; logFile_.setFileName( nextFile ); } else { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Using existing file" << lastFile << "to flush content"; #endif isNewLogFile_ = false; logFile_.setFileName( lastFile ); } // Open the log file if( ! logFile_.open( QIODevice::ReadWrite ) ) { kmWarning() << "Unable to open log file" << logFile_.fileName() << "- cannot log any message!"; return; } qint64 currentFileSize = logFile_.size(); // Add the headers to the new log file, or move to the end of the old file if( currentFileSize < 1 ) { const QByteArray header( "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" "<!-- KMess chat log file -->\n" "<messageRoot xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n" ); const QByteArray footer( "</messageRoot>\n" ); logFile_.write( header + footer ); pointer_ = header.length(); logFile_.seek( pointer_ ); } else { // Find the last conversation closing tag of the file, we'll append content from there qint64 searchStartPoint = qMax( currentFileSize - END_SEEKING_BUFFER_SIZE, (qint64)0L ); logFile_.seek( searchStartPoint ); qint64 tagPosition = logFile_.read( END_SEEKING_BUFFER_SIZE ).lastIndexOf( CLOSING_TAG ); if( tagPosition < 0 ) { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Unable to open existing log file:" << logFile_.fileName() << "- unrecognized file format!"; #endif logFile_.close(); return; } pointer_ = ( searchStartPoint + tagPosition + CLOSING_TAG_SIZE ); logFile_.seek( pointer_ ); } // Ready to log now! } // Destructor ChatHistoryWriter::~ChatHistoryWriter() { delete conversation_; delete messageStyle_; // Delete the log file if it was created just for this chat, and // no messages were appended to it if( logFile_.isOpen() && isNewLogFile_ && isFirstMessage_ ) { logFile_.remove(); } logFile_.close(); #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Log file closed."; #endif } // Return whether the writer is capable of writing new messages bool ChatHistoryWriter::isReady() const { return logFile_.isOpen(); } // Write a message to file void ChatHistoryWriter::logMessage( const ChatMessage& message ) { if( ! isReady() ) { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Unable to write the log message:" << message.getBody(); #endif return; } // Start the conversation if needed if( isFirstMessage_ ) { // Don't log initial presence in messenger if( message.getType() == ChatMessage::TYPE_PRESENCE ) { return; } // Add the new chat to the logs // ChatHistoryManager:: //no non qui, ogni msg ricevuto bisogna chiamare il manager x l'update della conv in corso #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Adding conversation tag.."; #endif quint64 timestamp = message.getDateTime().toTime_t(); isFirstMessage_ = false; const QByteArray conversationHeader( "\n\n<conversation timestamp=\"" + QString::number( timestamp ).toLatin1() + "\">\n" ); logFile_.write( conversationHeader ); pointer_ += conversationHeader.length(); logFile_.flush(); conversation_->filePath = logFile_.fileName(); conversation_->timestamp = timestamp; conversation_->startPosition = logFile_.pos(); conversation_->endPosition = logFile_.size(); } #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Adding a message to chat:" << message.getBody(); #endif if( ! messageCache_.isEmpty() ) { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Cache not empty, checking message grouping"; #endif const ChatMessage& lastCached = messageCache_.last(); // Verify if the new message can be grouped with the older ones in cache const QDateTime newMessageDate ( message.getDateTime() ); const QDateTime lastMessageDate( lastCached.getDateTime() ); // It's not groupable, write the new one alone (the cache is already on disk) if( ! message.isNormalMessage() || message.getContactHandle() != lastCached.getContactHandle() || message.getType() != lastCached.getType() || ! lastMessageDate.isValid() || lastMessageDate.secsTo( newMessageDate ) > 600 ) { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Message was not groupable, clearing cache first"; #endif pointer_ = logFile_.pos(); messageCache_.clear(); } } if( lastMessage_ && lastMessage_->getType() == ChatMessage::TYPE_PRESENCE && lastMessage_->getType() == message.getType() && lastMessage_->getContentsClass() == message.getContentsClass() && lastMessage_->getContactHandle() == message.getContactHandle() ) { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Not adding presence message for the same contact again."; #endif return; } // Add the message and write the entire cache on file again delete lastMessage_; lastMessage_ = new ChatMessage( message ); messageCache_.append( message ); writeCache(); // Signal the chat history manager the new chat message ChatHistoryManager::updateChatLog( handle_, *conversation_ ); } // Write the cache contents to the file void ChatHistoryWriter::writeCache() { if( ! isReady() ) { #ifdef KMESSDEBUG_CHATHISTORYWRITER kmDebug() << "Unable to write the message cache"; #endif return; } // Nothing to do if( messageCache_.isEmpty() ) { return; } logFile_.seek( pointer_ ); // Write one message if( messageCache_.count() == 1 ) { logFile_.write( messageStyle_->convertMessageToXml( messageCache_.first() ).toUtf8() ); } // Write a message group else { QByteArray xml( "<messagegroup>\n" ); foreach( const ChatMessage& message, messageCache_ ) { xml += messageStyle_->convertMessageToXml( message ).toUtf8(); } xml += "</messagegroup>\n"; logFile_.write( xml ); logFile_.flush(); } // Save the point in the log file where the conversation ends conversation_->endPosition = logFile_.pos(); const QByteArray conversationFooter( "</conversation>\n" "</messageRoot>\n" ); logFile_.write( conversationFooter ); // Following writes of new messageGroups will begin just after the closing messageGroup tag logFile_.seek( logFile_.pos() - conversationFooter.length() ); logFile_.flush(); }