4
Copyright (c) 2010 by Timo Schluessler
6
Kopete (c) 2010 by the Kopete developers <kopete-devel@kde.org>
8
*************************************************************************
10
* This program is free software; you can redistribute it and/or modify *
11
* it under the terms of the GNU General Public License as published by *
12
* the Free Software Foundation; either version 2 of the License, or *
13
* (at your option) any later version. *
15
*************************************************************************
18
#include "historyimport.h"
20
#include <QtCore/QStack>
21
#include <QtCore/QDir>
22
#include <QtGui/QTextEdit>
23
#include <QtGui/QTreeView>
24
#include <QtGui/QPushButton>
25
#include <QtGui/QCheckBox>
26
#include <QtGui/QGridLayout>
27
#include <QtGui/QStandardItemModel>
28
#include <QtGui/QProgressDialog>
29
#include <QtGui/QMessageBox>
30
#include <QtGui/QFileDialog>
31
#include <QtGui/QApplication>
32
#include <QtXml/QXmlStreamReader>
37
#include <kopetecontactlist.h>
38
#include <kopetemetacontact.h>
39
#include <kopeteprotocol.h>
40
#include <kopeteaccount.h>
41
#include <kopetecontact.h>
42
#include <kopetemessage.h>
44
#include "historylogger.h"
46
HistoryImport::HistoryImport(QWidget *parent)
50
// set dialog settings
51
setButtons(KDialog::Ok | KDialog::Details | KDialog::Cancel);
52
setWindowTitle(KDialog::makeStandardCaption(i18n("Import History")));
53
setButtonText(KDialog::Ok, i18n("Import Listed Logs"));
56
QWidget *w = new QWidget(this);
57
QGridLayout *l = new QGridLayout(w);
59
display = new QTextEdit(w);
60
display->setReadOnly(true);
61
treeView = new QTreeView(w);
63
QPushButton *fromPidgin = new QPushButton(i18n("Get History From &Pidgin..."), w);
65
l->addWidget(treeView, 0, 0, 1, 3);
66
l->addWidget(display, 0, 4, 1, 10);
67
l->addWidget(fromPidgin, 1, 0);
72
// create details widget
73
QWidget *details = new QWidget(w);
74
QVBoxLayout *dL = new QVBoxLayout(details);
76
QTextEdit *detailsEdit = new QTextEdit(details);
77
detailsEdit->setReadOnly(true);
78
selectByHand = new QCheckBox(i18n("Select log directory by hand"), details);
80
dL->addWidget(selectByHand);
81
dL->addWidget(detailsEdit);
83
setDetailsWidget(details);
84
detailsCursor = QTextCursor(detailsEdit->document());
86
// create model for treeView
87
QStandardItemModel *model = new QStandardItemModel(treeView);
88
treeView->setModel(model);
89
model->setHorizontalHeaderLabels(QStringList(i18n("Parsed History")));
92
connect(treeView, SIGNAL(clicked(QModelIndex)), this, SLOT(itemClicked(QModelIndex)));
93
connect(fromPidgin, SIGNAL(clicked()), this, SLOT(importPidgin()));
94
connect(this, SIGNAL(okClicked()), this, SLOT(save()));
99
pidginImported = false;
101
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)";
106
HistoryImport::~HistoryImport(void)
110
void HistoryImport::save(void)
112
QProgressDialog progress(i18n("Saving logs to disk ..."), i18n("Abort Saving"), 0, amount, this);
113
progress.setWindowTitle(i18n("Saving"));
117
foreach (log, logs) {
118
HistoryLogger logger(log.other, this);
120
foreach (message, log.messages) {
121
Kopete::Message kMessage;
122
if (message.incoming) {
123
kMessage = Kopete::Message(log.other, log.me);
124
kMessage.setDirection(Kopete::Message::Inbound);
126
kMessage = Kopete::Message(log.me, log.other);
127
kMessage.setDirection(Kopete::Message::Outbound);
129
kMessage.setPlainBody(message.text);
130
kMessage.setTimestamp(message.timestamp);
131
logger.appendMessage(kMessage, log.other);
133
progress.setValue(progress.value()+1);
134
qApp->processEvents();
135
if (progress.wasCanceled()) {
145
void HistoryImport::displayLog(struct Log *log)
149
QList<QStandardItem*> items;
152
items << static_cast<QStandardItemModel*>(treeView->model())->invisibleRootItem();
153
items << NULL << NULL << NULL;
154
strings << "" << "" << "";
156
foreach(message, log->messages) {
157
amount++; // for QProgressDialog in save()
159
strings[0] = log->other->protocol()->pluginId() + " (" + log->other->account()->accountId() + ')';
160
strings[1] = log->other->nickName();
161
strings[2] = message.timestamp.toString("yyyy-MM-dd");
165
for (i=1; i<4; i++) {
166
if (update || !items.at(i) || items.at(i)->data(Qt::DisplayRole) != strings.at(i-1)) {
167
items[i] = findItem(strings.at(i-1), items.at(i-1));
170
//kDebug(14310) << "using cached item";
173
if (!items.at(3)->data(Qt::UserRole).isValid())
174
items[3]->setData((int)logs.indexOf(*log), Qt::UserRole);
179
QStandardItem * HistoryImport::findItem(const QString &text, QStandardItem *parent)
183
QStandardItem *child = 0L;
185
for (i=0; i < parent->rowCount(); i++) {
186
child = parent->child(i, 0);
187
if (child->data(Qt::DisplayRole) == text) {
193
child = new QStandardItem(text);
194
parent->appendRow(child);
200
void HistoryImport::itemClicked(const QModelIndex &index)
202
QVariant id = index.data(Qt::UserRole);
204
if (id.canConvert<int>()) {
205
Log log = logs.at(id.toInt());
206
display->document()->clear();
207
QTextCursor cursor(display->document());
210
QDate date = QDate::fromString(index.data(Qt::DisplayRole).toString(), "yyyy-MM-dd");
211
foreach (message, log.messages) {
212
if (date != message.timestamp.date())
214
cursor.insertText(message.timestamp.toString("hh:mm:ss "));
215
if (message.incoming)
216
cursor.insertText(log.other->nickName().append(": "));
218
cursor.insertText(log.me->nickName().append(": "));
219
cursor.insertText(message.text);
220
cursor.insertBlock();
225
int HistoryImport::countLogs(QDir dir, int depth)
235
files = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
237
if (pos.size() == depth) {
238
res += dir.entryList(QDir::Files).size();
240
if (files.isEmpty() || files.size() <= pos.top() || pos.size() == depth) {
247
else if (pos.size() != depth) {
248
dir.cd(files.at(pos.top()));
256
void HistoryImport::importPidgin()
258
if (pidginImported) {
259
if (QMessageBox::question(this,
260
i18n("Are You Sure?"),
261
i18n("You already imported logs from pidgin. If you do it twice, each message is imported twice.\nAre you sure you want to continue?"),
262
QMessageBox::Yes | QMessageBox::No,
263
QMessageBox::No) != QMessageBox::Yes)
266
pidginImported = true;
268
QDir logDir = QDir::homePath();
269
if (selectByHand->isChecked() || !logDir.cd(".purple/logs"))
270
logDir = QFileDialog::getExistingDirectory(mainWidget(), i18n("Select Log Directory"), QDir::homePath());
272
int total = countLogs(logDir, 3);
273
QProgressDialog progress(i18n("Parsing history from pidgin ..."), i18n("Abort parsing"), 0, total, mainWidget());
274
progress.setWindowTitle(i18n("Parsing history"));
278
QString protocolFolder;
279
foreach (protocolFolder, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
280
logDir.cd(protocolFolder);
282
QString accountFolder;
283
foreach (accountFolder, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
284
logDir.cd(accountFolder);
286
// TODO use findContact?
287
Kopete::ContactList * cList = Kopete::ContactList::self();
288
QList<Kopete::Contact *> meList = cList->myself()->contacts();
291
foreach (me, meList) {
292
if (me->protocol()->pluginId().contains(protocolFolder, Qt::CaseInsensitive) &&
293
me->account()->accountId().contains(accountFolder, Qt::CaseInsensitive)) {
299
detailsCursor.insertText(i18n("WARNING: Cannot find matching account for %1 (%2).\n", accountFolder, protocolFolder));
305
foreach (chatPartner, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
306
logDir.cd(chatPartner);
308
Kopete::Contact *other = cList->findContact(me->protocol()->pluginId(), me->account()->accountId(), chatPartner);
311
detailsCursor.insertText(i18n("WARNING: Cannot find %1 (%2) in your contact list. Found logs will not be imported.\n", chatPartner, protocolFolder));
322
filter << "*.html" << "*.txt";
323
foreach(logFile, logDir.entryList(filter, QDir::Files)) {
324
QFile file(logDir.filePath(logFile));
325
if (!file.open(QIODevice::ReadOnly)) {
326
detailsCursor.insertText(i18n("WARNING: Cannot open file %1. Skipping.\n", logDir.filePath(logFile)));
330
if (logFile.endsWith(".html"))
331
parsePidginXml(file, &log, QDate::fromString(logFile.left(10), "yyyy-MM-dd"));
332
else if (logFile.endsWith(".txt"))
333
parsePidginTxt(file, &log, QDate::fromString(logFile.left(10), "yyyy-MM-dd"));
337
progress.setValue(progress.value()+1);
338
qApp->processEvents();
339
if (cancel || progress.wasCanceled()) {
363
bool HistoryImport::isNickIncoming(const QString &nick, struct Log *log)
367
if (nick == log->me->nickName())
369
else if (nick == log->other->nickName())
371
else if (knownNicks.contains(nick))
372
incoming = knownNicks.value(nick);
374
int r = QMessageBox::question(NULL,
375
i18n("Cannot Map Nickname to Account"),
376
i18n("Did you use \"%1\" as nickname in history?", nick),
377
QMessageBox::Yes | QMessageBox::No | QMessageBox::Abort);
379
if (r == QMessageBox::Yes) {
380
knownNicks.insert(nick, true);
383
else if (r == QMessageBox::No) {
384
knownNicks.insert(nick, false);
395
QDateTime HistoryImport::extractTime(const QString &string, QDate ref)
400
// try some formats used by pidgin
401
if ((time = QTime::fromString(string, "(hh:mm:ss)")) .isValid());
402
else if ((time = QTime::fromString(string, "(hh:mm:ss AP)")) .isValid());
405
foreach (format, timeFormats) {
406
if ((dateTime = QDateTime::fromString(string, format)).isValid())
411
// check if the century in dateTime is equal to that of our date reference
412
if (dateTime.isValid()) {
413
int diff = ref.year() - dateTime.date().year();
414
dateTime = dateTime.addYears(diff - (diff % 100));
417
// if string contains only a time we use ref as date
419
dateTime = QDateTime(ref, time);
421
// inform the user about the date problems
422
if (!dateTime.isValid())
423
detailsCursor.insertText(i18n("WARNING: Cannot parse date \"%1\". You may want to edit the file containing this date manually. (Example recognized date strings: \"05/31/2008 15:24:30\".)\n", string, dateTime.toString("yyyy-MM-dd hh:mm:ss")));
429
void HistoryImport::parsePidginTxt(QFile &file, struct Log *log, QDate date)
434
QString messageText, nick;
435
bool incoming = false; // =false to make the compiler not complain
437
while (!file.atEnd()) {
438
line = file.readLine();
440
if (line[0] == '(') {
441
if (!messageText.isEmpty()) {
442
// messageText contains an unwished newline at the end
443
if (messageText.endsWith('\n'))
444
messageText.remove(-1, 1);
445
struct Message message;
446
message.incoming = incoming;
447
message.text = messageText;
448
message.timestamp = dateTime;
449
log->messages.append(message);
453
int endTime = line.indexOf(')')+1;
454
dateTime = extractTime(line.left(endTime), date);
456
int nickEnd = QRegExp("\\s").indexIn(line, endTime + 1);
457
// TODO what if a nickname consists of two words? is this possible?
458
// the following while can't be used because in status logs there is no : after the nickname :(
459
//while (line[nickEnd-1] != ':')
460
// nickEnd = QRegExp("\\").indexIn(line, nickEnd);
461
if (line[nickEnd -1] != ':') // this line is a status message
464
nick = line.mid(endTime+1, nickEnd - endTime - 2); // -2 to delete the colon
466
incoming = isNickIncoming(nick, log);
470
messageText = line.mid(nickEnd + 1);
472
else if (line[0] == ' ') {
473
// an already started message is continued in this line
474
int start = QRegExp("\\S").indexIn(line);
475
messageText.append('\n' + line.mid(start));
478
if (!messageText.isEmpty()) {
479
struct Message message;
480
message.incoming = incoming;
481
message.text = messageText;
482
message.timestamp = dateTime;
483
log->messages.append(message);
489
void HistoryImport::parsePidginXml(QFile &file, struct Log * log, QDate date)
491
bool inMessage = false, textComes = false;
495
// unfortunately pidgin doesn't write <... /> for the <meta> tag
496
QByteArray data = file.readAll();
497
if (data.contains("<meta")) {
498
int metaEnd = data.indexOf(">", data.indexOf("<meta"));
499
if (data.at(metaEnd-1) != '/')
500
data.insert(metaEnd, '/');
503
QXmlStreamReader reader(data);
505
while (!reader.atEnd()) {
508
// pidgin writes one chat-message per line. so if we come to the next line, we can finish and save the current message
509
if (inMessage && reader.lineNumber() != lineNumber) {
510
if (!msg.text.isEmpty()) {
511
msg.text = msg.text.trimmed(); // trimm especially unwished newlines and spaces
512
log->messages.append(msg); // save messge for later import via HistoryLogger (see HistoryImport::save())
517
// when there is only the color attribute for the font-tag, this must be the beginning of a new message
518
if (!inMessage && reader.isStartElement() && reader.name() == "font" && reader.attributes().size() == 1 && reader.attributes().first().name() == "color") {
519
if (reader.attributes().value("color") == "#A82F2F")
522
msg.incoming = false;
524
while (reader.readNext() != QXmlStreamReader::Characters) { }; // skip tags
525
msg.timestamp = extractTime(reader.text().toString(), date);
527
lineNumber = reader.lineNumber();
530
else if (inMessage && !textComes && reader.isStartElement() && reader.name() == "b") {
531
reader.readNext(); // this is the nickname, which is followed by the messageText
534
else if (textComes && reader.isCharacters())
535
msg.text += reader.text().toString(); // append text
536
else if (textComes && reader.isStartElement() && reader.name() == "br")
537
msg.text += '\n'; // append newline
540
if (reader.hasError()) {
541
// we ignore error 4: premature end of document
542
if (reader.error() != 4) {
544
for (i=1;i<reader.lineNumber();i++)
545
pos = data.indexOf('\n', pos) + 1;
546
detailsCursor.insertText(i18n("WARNING: XML parser error in %1 at line %2, character %3: %4",
547
file.fileName(), reader.lineNumber(), reader.columnNumber(), reader.errorString()));
548
detailsCursor.insertBlock();
549
detailsCursor.insertText(i18n("\t%1", QString(data.mid(pos, data.indexOf('\n', pos) - pos))));
550
detailsCursor.insertBlock();
552
} else if (inMessage) { // an unsaved message is still pending (this doesn't happen at least for my pidgin-logs - handle it anyway)
553
msg.text = msg.text.trimmed(); // trimm especially unwished newlines and spaces
554
log->messages.append(msg); // save messge for later import via HistoryLogger (see HistoryImport::save())
558
#include "historyimport.moc"