This is a multi-part message in MIME format. --------------070502030608080004000704 Content-Type: text/plain; charset=ISO-8859-1; format=flowed Content-Transfer-Encoding: 7bit Hello, attached is a patch and the two files which add the ability to import logs from pidgin to kopete. The source-code files belong to kdenetwork/kopete/plugins/history. I changed HistoryLogger slightly so that appendMessage() uses the timestamp from the message that is to be imported but not QDate::currentDate(). I don't know what happens if you want to import multiuser chatlogs because i haven't got any ... i would be glad if you can check that. For me it works just fine, but the logs from pidgin are very unsteady. anyway i hope it works for all pidgin logs. thanks for testing, timo --------------070502030608080004000704 Content-Type: text/plain; name="historyimport.cpp" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="historyimport.cpp" /* historyimport.cpp Copyright (c) 2008 by Timo Schluessler Kopete (c) 2008 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 "historyimport.h" #include "historylogger.h" #include #include #include #include #include HistoryImport::HistoryImport(QWidget *parent) : KDialog(parent) { // set dialog settings setButtons(KDialog::Ok | KDialog::Details | KDialog::Cancel); setWindowTitle(KDialog::makeStandardCaption("Import History")); setButtonText(KDialog::Ok, "Import listed logs"); // create widgets QWidget *w = new QWidget(this); QGridLayout *l = new QGridLayout(w); display = new QTextEdit(w); display->setReadOnly(true); treeView = new QTreeView(w); QPushButton *fromPidgin = new QPushButton("Get history from &Pidgin", w); l->addWidget(treeView, 0, 0, 1, 3); l->addWidget(display, 0, 4, 1, 10); l->addWidget(fromPidgin, 1, 0); setMainWidget(w); // create details widget QWidget *details = new QWidget(w); QVBoxLayout *dL = new QVBoxLayout(w); QTextEdit *detailsEdit = new QTextEdit(details); detailsEdit->setReadOnly(true); selectByHand = new QCheckBox(tr("Select log directory by hand"), details); dL->addWidget(selectByHand); dL->addWidget(detailsEdit); details->setLayout(dL); setDetailsWidget(details); detailsCursor = QTextCursor(detailsEdit->document()); // create model for treeView QStandardItemModel *model = new QStandardItemModel(treeView); treeView->setModel(model); model->setHorizontalHeaderLabels(QStringList("Parsed History")); // connect everything connect(treeView, SIGNAL(clicked(const QModelIndex &)), this, SLOT(itemClicked(const QModelIndex &))); connect(fromPidgin, SIGNAL(clicked()), this, SLOT(importPidgin())); connect(this, SIGNAL(okClicked()), this, SLOT(save())); // define variables amount = 0; cancel = false; timeFormats << "(MM/dd/yyyy hh:mm:ss)" << "(MM/dd/yyyy hh:mm:ss AP)" << "(MM/dd/yy hh:mm:ss)" << "(MM/dd/yy hh:mm:ss AP)" << "(dd.MM.yyyy hh:mm:ss)" << "(dd.MM.yyyy hh:mm:ss AP)" << "(dd.MM.yy hh:mm:ss)" << "(dd.MM.yyyy hh:mm:ss AP)" << "(dd/MM/yyyy hh:mm:ss)" << "(dd/MM/yyyy hh:mm:ss AP)" << "(dd/MM/yy hh:mm:ss)" << "(dd/MM/yy hh:mm:ss AP)"; show(); } HistoryImport::~HistoryImport(void) { } void HistoryImport::save(void) { QProgressDialog progress("Saving logs to disk ...", "Abort Saving", 0, amount, this); progress.setWindowTitle("Saving"); Log log; foreach (log, logs) { HistoryLogger logger(log.other, this); Message message; foreach (message, log.messages) { Kopete::Message kMessage; if (message.incoming) { kMessage = Kopete::Message(log.other, log.me); kMessage.setDirection(Kopete::Message::Inbound); } else { kMessage = Kopete::Message(log.me, log.other); kMessage.setDirection(Kopete::Message::Outbound); } kMessage.setPlainBody(message.text); kMessage.setTimestamp(message.timestamp); logger.appendMessage(kMessage, log.other); progress.setValue(progress.value()+1); qApp->processEvents(); if (progress.wasCanceled()) { cancel = true; break; } } if (cancel) break; } } void HistoryImport::displayLog(struct Log *log) { Message message; QStandardItem *root = dynamic_cast(treeView->model())->invisibleRootItem(), *item; foreach(message, log->messages) { amount++; // for QProgressDialog in save() item = findItem(log->other->protocol()->pluginId().append(" (").append(log->other->account()->accountId()).append(")"), root); item = findItem(log->other->nickName(), item); item = findItem(message.timestamp.toString("yyyy-MM-dd"), item); if (!item->data(Qt::UserRole).isValid()) item->setData((int)logs.indexOf(*log), Qt::UserRole); } } QStandardItem * HistoryImport::findItem(QString text, QStandardItem *parent) { int i; bool found = false; QStandardItem *child = 0L; for (i=0; i < parent->rowCount(); i++) { child = parent->child(i, 0); if (child->data(Qt::DisplayRole) == text) { found = true; break; } } if (!found) { child = new QStandardItem(text); parent->appendRow(child); } return child; } void HistoryImport::itemClicked(const QModelIndex & index) { QVariant id = index.data(Qt::UserRole); if (id.canConvert()) { Log log = logs.at(id.toInt()); display->document()->clear(); QTextCursor cursor(display->document()); Message message; QDate date = QDate::fromString(index.data(Qt::DisplayRole).toString(), "yyyy-MM-dd"); foreach (message, log.messages) { if (date != message.timestamp.date()) continue; cursor.insertText(message.timestamp.toString("hh:mm:ss ")); if (message.incoming) cursor.insertText(log.other->nickName().append(": ")); else cursor.insertText(log.me->nickName().append(": ")); cursor.insertText(message.text); cursor.insertBlock(); } } } int HistoryImport::countLogs(QDir dir, int depth) { int res = 0; QStack pos; QStringList files; pos.push(0); depth++; forever { files = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); if (pos.size() == depth) { res += dir.entryList(QDir::Files).size(); } if (files.isEmpty() || files.size() <= pos.top() || pos.size() == depth) { dir.cdUp(); pos.pop(); if (pos.isEmpty()) break; pos.top()++; } else if (pos.size() != depth) { dir.cd(files.at(pos.top())); pos.push(0); } } return res; } void HistoryImport::importPidgin() { QDir logDir = QDir::homePath(); if (selectByHand->isChecked() || !logDir.cd(".purple/logs")) logDir = QFileDialog::getExistingDirectory(mainWidget(), tr("Select log directory"), QDir::homePath()); int total = countLogs(logDir, 3); QProgressDialog progress("Parsing history from pidgin ...", "Abort parsing", 0, total, mainWidget()); progress.setWindowTitle("Parsing history"); progress.show(); cancel = false; QString protocolFolder; foreach (protocolFolder, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { logDir.cd(protocolFolder); QString accountFolder; foreach (accountFolder, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { logDir.cd(accountFolder); Kopete::ContactList * cList = Kopete::ContactList::self(); QList meList = cList->myself()->contacts(); Kopete::Contact *me; bool found = false; foreach (me, meList) { qDebug() << me->protocol()->pluginId() << protocolFolder << me->account()->accountId() << accountFolder; if (me->protocol()->pluginId().contains(protocolFolder, Qt::CaseInsensitive) && me->account()->accountId().contains(accountFolder, Qt::CaseInsensitive)) { found = true; break; } } if (!found) { detailsCursor.insertText(QString("WARNING: Can't find matching account for %1 (%2)!\n").arg(accountFolder).arg(protocolFolder)); qDebug() << logDir.path(); logDir.cdUp(); qDebug() << logDir.path(); continue; } QString chatPartner; foreach (chatPartner, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { logDir.cd(chatPartner); Kopete::Contact *other = cList->findContact(me->protocol()->pluginId(), me->account()->accountId(), chatPartner); struct Log log; if (!other) { detailsCursor.insertText(QString("WARNING: Can't find %1 (%2) in your contact list. Found logs will not be imported!\n").arg(chatPartner).arg(protocolFolder)); logDir.cdUp(); continue; } else { log.me = me; log.other = other; } QString logFile; QStringList filter; filter << "*.html" << "*.txt"; foreach(logFile, logDir.entryList(filter, QDir::Files)) { QFile file(logDir.filePath(logFile)); if (!file.open(QIODevice::ReadOnly)) { detailsCursor.insertText(QString("WARNING: Can't open file %1. Skipping.\n").arg(logDir.filePath(logFile))); continue; } if (logFile.endsWith(".html")) parsePidginXml(file, &log, QDate::fromString(logFile.left(10), "yyyy-MM-dd")); else if (logFile.endsWith(".txt")) parsePidginTxt(file, &log, QDate::fromString(logFile.left(10), "yyyy-MM-dd")); file.close(); progress.setValue(progress.value()+1); qApp->processEvents(); if (cancel || progress.wasCanceled()) { cancel = true; break; } } logs.append(log); displayLog(&log); if (cancel) break; logDir.cdUp(); } if (cancel) break; logDir.cdUp(); } if (cancel) break; logDir.cdUp(); } } bool HistoryImport::isNickIncoming(QString nick, struct Log *log) { bool incoming; if (nick == log->me->nickName()) incoming = false; else if (nick == log->other->nickName()) incoming = true; else if (knownNicks.contains(nick)) incoming = knownNicks.value(nick); else { int r = QMessageBox::question(NULL, tr("Can't map nickname to account"), tr("Did you use \"%1\" as nickname in history?").arg(nick), QMessageBox::Yes | QMessageBox::No | QMessageBox::Abort); if (r == QMessageBox::Yes) { knownNicks.insert(nick, true); incoming = true; } else if (r == QMessageBox::No) { knownNicks.insert(nick, false); incoming = false; } else { cancel = true; return false; } } return incoming; } QDateTime HistoryImport::extractTime(QString string, QDate ref) { QDateTime dateTime; QTime time; // try some formats used by pidgin if ((time = QTime::fromString(string, "(hh:mm:ss)")) .isValid()); else if ((time = QTime::fromString(string, "(hh:mm:ss AP)")) .isValid()); else { QString format; foreach (format, timeFormats) { if ((dateTime = QDateTime::fromString(string, format)).isValid()) break; } } // check if the century in dateTime is equal to that of our date reference if (dateTime.isValid()) { int diff = ref.year() - dateTime.date().year(); dateTime = dateTime.addYears(diff - (diff % 100)); } // if string contains only a time we use ref as date if (time.isValid()) dateTime = QDateTime(ref, time); // inform the user about the date problems if (!dateTime.isValid()) detailsCursor.insertText(tr("WARNING: can't parse date \"%1\". You may want to edit the file containing this date manually. (example for recognized date strings: \"05/31/2008 15:24:30\")\n").arg(string, dateTime.toString("yyyy-MM-dd hh:mm:ss"))); return dateTime; } void HistoryImport::parsePidginTxt(QFile & file, struct Log *log, QDate date) { QByteArray line; QTime time; QDateTime dateTime; QString messageText, nick; bool incoming = false; // =false to make the compiler not complain while (!file.atEnd()) { line = file.readLine(); if (line[0] == '(') { if (!messageText.isEmpty()) { // messageText contains an unwished newline at the end if (messageText.endsWith("\n")) messageText.remove(-1, 1); struct Message message; message.incoming = incoming; message.text = messageText; message.timestamp = dateTime; log->messages.append(message); messageText.clear(); } int endTime = line.indexOf(")")+1; dateTime = extractTime(line.left(endTime), date); int nickEnd = QRegExp("\\s").indexIn(line, endTime + 1); // TODO what if a nickname consists of two words? is this possible? // the following while can't be used because in status logs there is no : after the nickname :( //while (line[nickEnd-1] != ':') // nickEnd = QRegExp("\\").indexIn(line, nickEnd); if (line[nickEnd -1] != ':') // this line is a status message continue; nick = line.mid(endTime+1, nickEnd - endTime - 2); // -2 to delete the colon incoming = isNickIncoming(nick, log); if (cancel) return; messageText = line.mid(nickEnd + 1); } else if (line[0] == ' ') { // an already started message is continued in this line int start = QRegExp("\\S").indexIn(line); messageText.append("\n" + line.mid(start)); } } if (!messageText.isEmpty()) { struct Message message; message.incoming = incoming; message.text = messageText; message.timestamp = dateTime; log->messages.append(message); messageText.clear(); } } void HistoryImport::parsePidginXml(QFile & file, struct Log * log, QDate date) { bool incoming = false, inMessage = false, textComes = false; QString messageText, status; QTime time; QDateTime dateTime; // unfortunately pidgin doesn't write <... /> for the tag QByteArray data = file.readAll(); if (data.contains("", data.indexOf("messages.append(message); messageText.clear(); textComes = false; inMessage = false; } } if (reader.hasError()) { // we ignore error 4: premature end of document if (reader.error() != 4) { int i, pos = 0; for (i=1;i Kopete (c) 2008 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. * * * ************************************************************************* */ #ifndef HISTORYIMPORT_H #define HISTORYIMPORT_H #include #include #include "kopeteglobal.h" #include "kopetecontact.h" #include "kopetemessage.h" /** * @author Timo Schluessler */ class HistoryImport : public KDialog { Q_OBJECT public: HistoryImport(QWidget *parent); ~HistoryImport(); private: /** * Used for internal storage of a message. */ struct Message { bool incoming; QString text; QDateTime timestamp; }; /** * Holds the messages sent and received between two specified contacts. */ struct Log { Kopete::Contact *me; Kopete::Contact *other; QList messages; /** * Comparison between the Message lists is not neccessary because we need this operator * only in displayLog() to get the index of this log in logs. */ bool operator==(const struct Log & cmp) { if (cmp.me == me && cmp.other == other/* && other.messages == messages*/) return true; else return false; } }; /** * Parses @param file and appends the found messages to @param log. */ void parsePidginXml(QFile & file, struct Log *log, QDate date); /** * Parses @param file and appends the found messages to @param log. */ void parsePidginTxt(QFile & file, struct Log *log, QDate date); /** * Inserts @param log into treeView and prepares to display it when clicking on it. */ void displayLog(struct Log *log); /** * Looks up if an item with @param text exists already as child of @param parent. * If not, it creates one. */ QStandardItem * findItem(QString text, QStandardItem *parent); /** * @return the number of files found in @param depth th subdir of @param dir. */ int countLogs(QDir dir, int depth); /** * Checks if nickname @param nick can be mapped to either @param log ->me or @param log ->other. * If not it asks the user if it is his nickname or someone elses. */ bool isNickIncoming(QString nick, struct Log *log); /** * Trys to extract the time from @param string using formats specified in timeFormats. * @param ref is used when @param string doesn't contain a date or to adjust a found date. * @param ref is taken from the filename of the log. */ QDateTime extractTime(QString string, QDate ref); QStringList timeFormats; QTreeView *treeView; QTextEdit *display; /** * Used for adding details to the details widget. */ QTextCursor detailsCursor; /** * Enables/disables auto search for log dirs. */ QCheckBox *selectByHand; /** * Contains all already parsed logs. */ QList logs; /** * Used for mapping nickname to account. See isNickIncoming(). */ QHash knownNicks; int amount; bool cancel; private slots: /** * Starts parsing history from pidgin. * Default path is ~/.purple/logs. */ void importPidgin(void); void save(void); void itemClicked(const QModelIndex & index); }; #endif --------------070502030608080004000704 Content-Type: text/plain; name="kopete-history-import.patch" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="kopete-history-import.patch" Index: kopete/plugins/history/historylogger.cpp =================================================================== --- kopete/plugins/history/historylogger.cpp (revision 890701) +++ kopete/plugins/history/historylogger.cpp (working copy) @@ -117,16 +117,16 @@ } -QDomDocument HistoryLogger::getDocument(const Kopete::Contact *c, unsigned int month , bool canLoad , bool* contain) +QDomDocument HistoryLogger::getDocument(const Kopete::Contact *c, unsigned int month , const QDate date , bool canLoad , bool* contain) { - if(m_realMonth!=QDate::currentDate().month()) + if(m_realMonth!=date.month() /*QDate::currentDate().month()*/) { //We changed month, our index is 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=QDate::currentDate().month(); + m_realMonth=date.month();//QDate::currentDate().month(); } if(!m_metaContact) @@ -149,7 +149,7 @@ return documents[month]; - QDomDocument doc = getDocument(c, QDate::currentDate().addMonths(0-month), canLoad, contain); + QDomDocument doc = getDocument(c, date.addMonths(0-month) /*QDate::currentDate().addMonths(0-month)*/, canLoad, contain); documents.insert(month, doc); m_documents[c]=documents; @@ -255,7 +255,9 @@ return; } - QDomDocument doc=getDocument(c,0); + + + QDomDocument doc=getDocument(c, 0, msg.timestamp().date()); QDomElement docElem = doc.documentElement(); if(docElem.isNull()) @@ -293,7 +295,7 @@ // 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 QString filename=getFileName(c,QDate::currentDate()); + const QString filename=getFileName(c, msg.timestamp().date()); if(!m_toSaveFileName.isEmpty() && m_toSaveFileName != filename) { //that mean the contact or the month has changed, save it now. saveToDisk(); Index: kopete/plugins/history/historylogger.h =================================================================== --- kopete/plugins/history/historylogger.h (revision 890701) +++ kopete/plugins/history/historylogger.h (working copy) @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -148,7 +149,7 @@ * Get the document, open it is @param canload is true, contain is set to false if the document * is not already contained */ - QDomDocument getDocument(const Kopete::Contact *c, unsigned int month , bool canLoad=true , bool* contain=0L); + QDomDocument getDocument(const Kopete::Contact *c, unsigned int month , const QDate date = QDate::currentDate(), bool canLoad=true , bool* contain=0L); QDomDocument getDocument(const Kopete::Contact *c, const QDate date, bool canLoad=true, bool* contain=0L); Index: kopete/plugins/history/historypreferences.cpp =================================================================== --- kopete/plugins/history/historypreferences.cpp (revision 890701) +++ kopete/plugins/history/historypreferences.cpp (working copy) @@ -19,6 +19,7 @@ #include "historypreferences.h" #include "historyconfig.h" #include "ui_historyprefsui.h" +#include "historyimport.h" #include @@ -51,6 +52,7 @@ this, SLOT(slotModified())); connect(p->History_color, SIGNAL(changed(const QColor&)), this, SLOT(slotModified())); + connect(p->importLogs, SIGNAL(clicked(void)), this, SLOT(importLogs(void))); } HistoryPreferences::~HistoryPreferences() @@ -94,4 +96,10 @@ emit KCModule::changed(true); } +void HistoryPreferences::importLogs(void) +{ + HistoryImport importer(this); + importer.exec(); +} + #include "historypreferences.moc" Index: kopete/plugins/history/historypreferences.h =================================================================== --- kopete/plugins/history/historypreferences.h (revision 890701) +++ kopete/plugins/history/historypreferences.h (working copy) @@ -42,6 +42,7 @@ private slots: void slotModified(); void slotShowPreviousChanged(bool); + void importLogs(void); private: Ui::HistoryPrefsUI *p; Index: kopete/plugins/history/historyprefsui.ui =================================================================== --- kopete/plugins/history/historyprefsui.ui (revision 890701) +++ kopete/plugins/history/historyprefsui.ui (working copy) @@ -14,42 +14,21 @@ 6 - + 0 - - 0 - - - 0 - - - 0 - Chat History - + 9 - - 9 - - - 9 - - - 9 - - + 6 - - 6 - @@ -58,7 +37,7 @@ QSizePolicy::Fixed - + 16 20 @@ -164,6 +143,13 @@ + + + + Import History + + + @@ -175,7 +161,7 @@ QSizePolicy::Expanding - + 341 16 Index: kopete/plugins/history/CMakeLists.txt =================================================================== --- kopete/plugins/history/CMakeLists.txt (revision 890701) +++ kopete/plugins/history/CMakeLists.txt (working copy) @@ -28,7 +28,7 @@ ########### next target ############### -set(kcm_kopete_history_PART_SRCS historypreferences.cpp ) +set(kcm_kopete_history_PART_SRCS historypreferences.cpp historyimport.cpp historylogger.cpp ) kde4_add_ui_files(kcm_kopete_history_PART_SRCS historyprefsui.ui ) --------------070502030608080004000704 Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline _______________________________________________ kopete-devel mailing list kopete-devel@kde.org https://mail.kde.org/mailman/listinfo/kopete-devel --------------070502030608080004000704--