4
Copyright (c) 2012 by Volker HƤrtel
5
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 "history2import.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>
38
#include <KStandardDirs>
40
#include <kopetecontactlist.h>
41
#include <kopetemetacontact.h>
42
#include <kopeteprotocol.h>
43
#include <kopeteaccount.h>
44
#include <kopetecontact.h>
45
#include <kopetemessage.h>
47
#include "history2logger.h"
50
History2Import::History2Import(QWidget *parent)
53
// set dialog settings
54
setButtons(KDialog::Ok | KDialog::Details | KDialog::Cancel);
55
setWindowTitle(KDialog::makeStandardCaption(i18n("Import History")));
56
setButtonText(KDialog::Ok, i18n("Import Listed Logs"));
59
QWidget *w = new QWidget(this);
60
QGridLayout *l = new QGridLayout(w);
62
display = new QTextEdit(w);
63
display->setReadOnly(true);
64
treeView = new QTreeView(w);
66
QPushButton *fromPidgin = new QPushButton(i18n("Get History From &Pidgin..."), w);
68
QPushButton *fromKopete = new QPushButton(i18n("Get History From &Kopete..."), w);
70
l->addWidget(treeView, 0, 0, 1, 3);
71
l->addWidget(display, 0, 4, 1, 10);
72
l->addWidget(fromPidgin, 1, 0);
73
l->addWidget(fromKopete, 1, 1);
78
// create details widget
79
QWidget *details = new QWidget(w);
80
QVBoxLayout *dL = new QVBoxLayout(details);
82
QTextEdit *detailsEdit = new QTextEdit(details);
83
detailsEdit->setReadOnly(true);
84
selectByHand = new QCheckBox(i18n("Select log directory by hand"), details);
86
dL->addWidget(selectByHand);
87
dL->addWidget(detailsEdit);
89
setDetailsWidget(details);
90
detailsCursor = QTextCursor(detailsEdit->document());
92
// create model for treeView
93
QStandardItemModel *model = new QStandardItemModel(treeView);
94
treeView->setModel(model);
95
model->setHorizontalHeaderLabels(QStringList(i18n("Parsed History")));
98
connect(treeView, SIGNAL(clicked(QModelIndex)), this, SLOT(itemClicked(QModelIndex)));
99
connect(fromPidgin, SIGNAL(clicked()), this, SLOT(importPidgin()));
100
connect(fromKopete, SIGNAL(clicked()), this, SLOT(importKopete()));
101
connect(this, SIGNAL(okClicked()), this, SLOT(save()));
106
pidginImported = false;
109
timeFormats << "(MM/dd/yyyy hh:mm:ss)" << "(MM/dd/yyyy hh:mm:ss AP)"
110
<< "(MM/dd/yy hh:mm:ss)" << "(MM/dd/yy hh:mm:ss AP)"
111
<< "(dd.MM.yyyy hh:mm:ss)" << "(dd.MM.yyyy hh:mm:ss AP)"
112
<< "(dd.MM.yy hh:mm:ss)" << "(dd.MM.yyyy hh:mm:ss AP)"
113
<< "(dd/MM/yyyy hh:mm:ss)" << "(dd/MM/yyyy hh:mm:ss AP)"
114
<< "(dd/MM/yy hh:mm:ss)" << "(dd/MM/yy hh:mm:ss AP)"
115
<< "(yyyy-MM-dd hh:mm:ss)" << "(yyyy-MM-dd hh:mm:ss AP)";
119
History2Import::~History2Import(void) {
123
void History2Import::save(void) {
124
QProgressDialog progress(i18n("Saving logs to disk ..."), i18n("Abort Saving"), 0, amount, this);
125
progress.setWindowTitle(i18n("Saving"));
128
History2Logger::instance()->beginTransaction();
129
foreach (log, logs) {
131
foreach (message, log->messages) {
132
Kopete::Message kMessage;
133
if (message.incoming) {
134
kMessage = Kopete::Message(log->other, log->me);
135
kMessage.setDirection(Kopete::Message::Inbound);
137
kMessage = Kopete::Message(log->me, log->other);
138
kMessage.setDirection(Kopete::Message::Outbound);
140
kMessage.setPlainBody(message.text);
141
kMessage.setTimestamp(message.timestamp);
142
History2Logger::instance()->appendMessage(kMessage, log->other, true);
144
progress.setValue(progress.value()+1);
145
qApp->processEvents();
146
if (progress.wasCanceled()) {
154
History2Logger::instance()->commitTransaction();
157
void History2Import::displayLog(struct Log *log) {
160
QList<QStandardItem*> items;
163
items << static_cast<QStandardItemModel*>(treeView->model())->invisibleRootItem();
164
items << NULL << NULL << NULL;
165
strings << "" << "" << "";
167
foreach(message, log->messages) {
168
amount++; // for QProgressDialog in save()
170
strings[0] = log->other->protocol()->pluginId() + " (" + log->other->account()->accountId() + ')';
171
strings[1] = log->other->nickName();
172
strings[2] = message.timestamp.toString("yyyy-MM-dd");
176
for (i=1; i<4; i++) {
177
if (update || !items.at(i) || items.at(i)->data(Qt::DisplayRole) != strings.at(i-1)) {
178
items[i] = findItem(strings.at(i-1), items.at(i-1));
181
//kDebug(14310) << "using cached item";
184
if (!items.at(3)->data(Qt::UserRole).isValid())
185
items[3]->setData((int)logs.indexOf(log), Qt::UserRole);
190
QStandardItem * History2Import::findItem(const QString &text, QStandardItem *parent) {
193
QStandardItem *child = 0L;
195
for (i=0; i < parent->rowCount(); i++) {
196
child = parent->child(i, 0);
197
if (child->data(Qt::DisplayRole) == text) {
203
child = new QStandardItem(text);
204
parent->appendRow(child);
210
void History2Import::itemClicked(const QModelIndex &index) {
211
QVariant id = index.data(Qt::UserRole);
213
if (id.canConvert<int>()) {
214
Log *log = logs.at(id.toInt());
215
display->document()->clear();
216
QTextCursor cursor(display->document());
219
QDate date = QDate::fromString(index.data(Qt::DisplayRole).toString(), "yyyy-MM-dd");
220
foreach (message, log->messages) {
221
if (date != message.timestamp.date())
223
cursor.insertHtml(message.timestamp.toString("hh:mm:ss "));
224
if (message.incoming)
225
cursor.insertHtml("<font color=\"blue\">" + log->other->nickName().append(": </font>"));
227
cursor.insertHtml("<font color=\"green\">" + log->me->nickName().append(": </font>"));
228
cursor.insertHtml(message.text);
229
cursor.insertBlock();
234
int History2Import::countLogs(QDir dir, int depth) {
243
files = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
245
if (pos.size() == depth) {
246
res += dir.entryList(QDir::Files).size();
248
if (files.isEmpty() || files.size() <= pos.top() || pos.size() == depth) {
254
} else if (pos.size() != depth) {
255
dir.cd(files.at(pos.top()));
263
void History2Import::importKopete() {
266
QString logDir = KStandardDirs::locateLocal("data",QString("kopete/logs/"));
268
if (selectByHand->isChecked() || !dd.exists()) {
269
logDir = QFileDialog::getExistingDirectory(mainWidget(), i18n("Select Log Directory"), QDir::homePath());
271
int total = countLogs(logDir, 3);
272
QProgressDialog progress(i18n("Parsing history from kopete ..."), i18n("Abort parsing"), 0, total, mainWidget());
273
progress.setWindowTitle(i18n("Parsing history"));
275
// qDebug() << "logdir=" << logDir;
277
ld.setFilter( QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot );
278
ld.setSorting( QDir::Name );
282
const QFileInfoList protocols = ld.entryInfoList();
283
foreach (const QFileInfo protocolDir, protocols) {
284
// qDebug() << "protocoldir=" << protocolDir.absoluteFilePath();
285
QDir pd(protocolDir.absoluteFilePath());
286
pd.setFilter( QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot );
287
pd.setSorting( QDir::Name );
288
const QFileInfoList accounts = pd.entryInfoList();
289
foreach (const QFileInfo accountDir, accounts) {
290
// qDebug() << "accountdir=" << accountDir.absoluteFilePath();
291
QDir d(accountDir.absoluteFilePath());
292
d.setFilter( QDir::Files | QDir::NoSymLinks );
293
d.setSorting( QDir::Name );
294
const QFileInfoList list = d.entryInfoList();
295
foreach( const QFileInfo &fi, list ) {
296
// qDebug() << "file=" << fi.absoluteFilePath();
297
Log *log = new Log();
298
protocol = protocolDir.fileName().replace("-", ".");
299
account = accountDir.fileName().replace("-", ".");
300
readKopeteMessages(protocol,account, fi.absoluteFilePath(), log);
303
progress.setValue(progress.value()+1);
304
qApp->processEvents();
305
if (cancel || progress.wasCanceled()) {
320
void History2Import::importPidgin() {
321
if (pidginImported) {
322
if (QMessageBox::question(this,
323
i18n("Are You Sure?"),
324
i18n("You already imported logs from pidgin. If you do it twice, each message is imported twice.\nAre you sure you want to continue?"),
325
QMessageBox::Yes | QMessageBox::No,
326
QMessageBox::No) != QMessageBox::Yes)
329
pidginImported = true;
331
QDir logDir = QDir::homePath();
332
if (selectByHand->isChecked() || !logDir.cd(".purple/logs"))
333
logDir = QFileDialog::getExistingDirectory(mainWidget(), i18n("Select Log Directory"), QDir::homePath());
335
int total = countLogs(logDir, 3);
336
QProgressDialog progress(i18n("Parsing history from pidgin ..."), i18n("Abort parsing"), 0, total, mainWidget());
337
progress.setWindowTitle(i18n("Parsing history"));
341
// protocolMap maps pidgin account-names to kopete protocol names (as in Kopete::Contact::protocol()->pluginId())
342
QHash<QString, QString> protocolMap;
343
protocolMap.insert("msn", "WlmProtocol");
344
protocolMap.insert("icq", "ICQProtocol");
345
protocolMap.insert("aim", "AIMProtocol");
346
protocolMap.insert("jabber", "JabberProtocol");
347
protocolMap.insert("yahoo", "YahooProtocol");
348
protocolMap.insert("qq", "QQProtocol");
349
protocolMap.insert("irc", "IRCProtocol");
350
protocolMap.insert("gadu-gadu", "GaduProtocol");
351
protocolMap.insert("bonjour", "BonjourProtocol");
352
protocolMap.insert("meanwhile", "MeanwhileProtocol");
354
QString protocolFolder;
355
foreach (protocolFolder, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
356
logDir.cd(protocolFolder);
358
// check if we can map the protocol
359
if (!protocolMap.contains(protocolFolder)) {
360
detailsCursor.insertText(i18n("WARNING: There is no equivalent for protocol %1 in kopete.\n", protocolFolder));
364
const QString & protocol = protocolMap.value(protocolFolder);
366
if (protocolFolder == "gadu-gadu")
367
protocolFolder = "gadu";
368
else if (protocolFolder == "msn")
369
protocolFolder = "wlm";
371
QString accountFolder;
372
foreach (accountFolder, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
373
logDir.cd(accountFolder);
375
// TODO use findContact?
376
Kopete::ContactList * cList = Kopete::ContactList::self();
377
QList<Kopete::Contact *> meList = cList->myself()->contacts();
380
foreach (me, meList) {
381
if (me->protocol()->pluginId() == protocol && me->account()->accountId().contains(accountFolder, Qt::CaseInsensitive)) {
387
detailsCursor.insertText(i18n("WARNING: Cannot find matching account for %1 (%2).\n", accountFolder, protocolFolder));
393
foreach (chatPartner, logDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
394
logDir.cd(chatPartner);
396
Kopete::Contact *other = cList->findContact(me->protocol()->pluginId(), me->account()->accountId(), chatPartner);
397
Log *log = new Log();
399
detailsCursor.insertText(i18n("WARNING: Cannot find %1 (%2) in your contact list. Found logs will not be imported.\n", chatPartner, protocolFolder));
409
filter << "*.html" << "*.txt";
410
foreach(logFile, logDir.entryList(filter, QDir::Files)) {
411
QFile file(logDir.filePath(logFile));
412
if (!file.open(QIODevice::ReadOnly)) {
413
detailsCursor.insertText(i18n("WARNING: Cannot open file %1. Skipping.\n", logDir.filePath(logFile)));
417
if (logFile.endsWith(".html"))
418
parsePidginXml(file, log, QDate::fromString(logFile.left(10), "yyyy-MM-dd"));
419
else if (logFile.endsWith(".txt"))
420
parsePidginTxt(file, log, QDate::fromString(logFile.left(10), "yyyy-MM-dd"));
424
progress.setValue(progress.value()+1);
425
qApp->processEvents();
426
if (cancel || progress.wasCanceled()) {
450
QDateTime History2Import::extractTime(const QString &string, QDate ref) {
454
// try some formats used by pidgin
455
if ((time = QTime::fromString(string, "(hh:mm:ss)")) .isValid());
456
else if ((time = QTime::fromString(string, "(hh:mm:ss AP)")) .isValid());
459
foreach (format, timeFormats) {
460
if ((dateTime = QDateTime::fromString(string, format)).isValid())
465
// check if the century in dateTime is equal to that of our date reference
466
if (dateTime.isValid()) {
467
int diff = ref.year() - dateTime.date().year();
468
dateTime = dateTime.addYears(diff - (diff % 100));
471
// if string contains only a time we use ref as date
473
dateTime = QDateTime(ref, time);
475
// inform the user about the date problems
476
// TODO ask the user for date format to enter
477
if (!dateTime.isValid())
478
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")));
484
void History2Import::parsePidginTxt(QFile &file, struct Log *log, QDate date) {
487
struct Message message;
489
// this is to collect unknown nicknames (the list stores the index in log->messages of the messages that used the nickname)
490
// the bool says if that nickname is incoming (only used when the list is empty)
491
QHash<QString, QPair<bool, QList<int> > > nicknames;
493
QTextStream str(&file);
494
// utf-8 seems to be default for pidgins-txt logs
495
str.setCodec("UTF-8");
497
while (!str.atEnd()) {
498
line = str.readLine();
500
if (line[0] == '(') {
501
if (!message.text.isEmpty()) {
502
log->messages.append(message);
503
message.text.clear();
506
int endTime = line.indexOf(')')+1;
507
message.timestamp = extractTime(line.left(endTime), date);
509
int nickEnd = QRegExp("\\s").indexIn(line, endTime + 1);
510
// TODO what if a nickname consists of two words? is this possible?
511
// the following while can't be used because in status logs there is no : after the nickname :(
512
//while (line[nickEnd-1] != ':')
513
// nickEnd = QRegExp("\\").indexIn(line, nickEnd);
514
if (line[nickEnd -1] != ':') // this line is a status message
517
nick = line.mid(endTime+1, nickEnd - endTime - 2); // -2 to delete the colon
519
// detect if the message is in- or outbound
520
if (nick == log->me->nickName())
521
message.incoming = false;
522
else if (nick == log->other->nickName())
523
message.incoming = true;
524
else if (knownNicks.contains(nick))
525
message.incoming = knownNicks.value(nick);
527
// store this nick for later decision
528
nicknames[nick].second.append(log->messages.size());
530
nicknames[nick].first = message.incoming;
535
message.text = line.mid(nickEnd + 1);
536
} else if (line[0] == ' ') {
537
// an already started message is continued in this line
538
int start = QRegExp("\\S").indexIn(line);
539
message.text.append('\n' + line.mid(start));
542
if (!message.text.isEmpty()) {
543
log->messages.append(message);
545
// check if we can guess which nickname belongs to us
546
QHash<QString, QPair<bool, QList<int> > >::iterator itr;
547
QHash<QString, QPair<bool, QList<int> > >::const_iterator itr2;
548
for (itr = nicknames.begin(); itr != nicknames.end(); ++itr) {
549
if (itr->second.isEmpty()) // no work for this one
551
bool haveAnother = false, lastIncoming = false;
552
// check against all other nicknames
553
for (itr2 = nicknames.constBegin(); itr2 != nicknames.constEnd(); ++itr2) {
554
if (itr2 == itr) // skip ourselve
556
// if there is another unknown nickname, we have no chance to guess which is our
557
if (!itr2->second.isEmpty())
560
lastIncoming = itr2->first;
563
// when there are more than one known nicknames, but with different incoming-values, we also can't guess which is ours
564
if (lastIncoming != itr2->first)
568
// we now can guess the incoming value of itr, namely !lastIncoming
569
if (haveAnother && itr2 == nicknames.constEnd()) {
572
detailsCursor.insertText(i18n("INFORMATION: Guessed %1 to be one of your nicks.\n", itr.key()));
574
detailsCursor.insertText(i18n("INFORMATION: Guessed %1 to be one of your buddys nicks.\n", itr.key()));
575
knownNicks.insert(itr.key(), !lastIncoming);
577
for (i = 0; i < itr->second.size(); i++)
578
log->messages[itr->second.at(i)].incoming = !lastIncoming;
579
itr->second.clear(); // we are finished with theese indexes
582
// iterate once again over the nicknames to detect which nicks are still not known. simply ask the user!
583
for (itr = nicknames.begin(); itr != nicknames.end(); ++itr) {
584
if (itr->second.isEmpty()) // no word for this one
587
int r = QMessageBox::question(NULL,
588
i18n("Cannot map Nickname to Account"),
589
i18n("Did you ever use \"%1\" as nickname in your history?", itr.key()),
590
QMessageBox::Yes | QMessageBox::No | QMessageBox::Abort);
591
if (r == QMessageBox::Yes) {
592
knownNicks.insert(itr.key(), false);
594
} else if (r == QMessageBox::No) {
595
knownNicks.insert(itr.key(), true);
601
// set the queried incoming value to our already stored Messages
603
for (i = 0; i < itr->second.size(); i++)
604
log->messages[itr->second.at(i)].incoming = incoming;
609
void History2Import::parsePidginXml(QFile &file, struct Log * log, QDate date) {
613
// unfortunately pidgin doesn't write <... /> for the <meta> tag
614
QByteArray data = file.readAll();
615
if (data.contains("<meta")) {
616
int metaEnd = data.indexOf(">", data.indexOf("<meta"));
617
if (data.at(metaEnd-1) != '/')
618
data.insert(metaEnd, '/');
621
QXmlStreamReader reader(data);
623
while (!reader.atEnd()) {
626
// when there is only the color attribute for the font-tag, this must be the beginning of a new message
627
if (state == 0 && reader.isStartElement() && reader.name() == "font" && reader.attributes().size() == 1 && reader.attributes().first().name() == "color") {
628
if (reader.attributes().value("color") == "#FF0000") // system message, e.g. warning
631
msg.incoming = (reader.attributes().value("color") == "#A82F2F");
634
if (state == 1 && reader.isStartElement() && reader.name() == "font") {
637
if (state == 2 && reader.isCharacters()) {
638
msg.timestamp = extractTime(reader.text().toString(), date);
639
if (msg.timestamp.isValid()) {
643
if (state == 3 && reader.isStartElement() && reader.name() == "b") {
646
if (state == 4 && reader.isEndElement() && reader.name() == "font") {
649
if (state == 5 && reader.isCharacters()) {
650
msg.text += reader.text().toString(); // append text
652
if (reader.isStartElement() && reader.name() == "br") {
654
msg.text = msg.text.trimmed();
655
if (msg.text != "") {
656
log->messages.append(msg); // save message for later import via History2Logger (see History2Import::save())
663
if (reader.hasError()) {
664
// we ignore error 4: premature end of document
665
if (reader.error() != 4) {
667
for (i=1; i<reader.lineNumber(); i++)
668
pos = data.indexOf('\n', pos) + 1;
669
detailsCursor.insertText(i18n("WARNING: XML parser error in %1 at line %2, character %3: %4",
670
file.fileName(), reader.lineNumber(), reader.columnNumber(), reader.errorString()));
671
detailsCursor.insertBlock();
672
detailsCursor.insertText(i18n("\t%1", QString(data.mid(pos, data.indexOf('\n', pos) - pos))));
673
detailsCursor.insertBlock();
676
if (state == 5) { // an unsaved message is still pending (this doesn't happen at least for my pidgin-logs - handle it anyway)
677
msg.text = msg.text.trimmed(); // trimm especially unwished newlines and spaces
678
if (msg.text != "") {
679
log->messages.append(msg); // save message for later import via History2Logger (see History2Import::save())
684
void History2Import::readKopeteMessages(QString protocol, QString account, QString f, Log *log) {
685
// qDebug() << "read kopete message from file " << f << " for protocol = " << protocol << ", account= " << account;
686
QDomDocument doc( "Kopete-History" );
688
if ( !file.open( QIODevice::ReadOnly ) ) {
689
detailsCursor.insertText("Could not open file: "+f);
690
detailsCursor.insertBlock();
693
if ( !doc.setContent( &file ) ) {
695
detailsCursor.insertText("Could not parse file: "+f);
696
detailsCursor.insertBlock();
701
QRegExp rxTime("(\\d+) (\\d+):(\\d+)($|:)(\\d*)"); //(with a 0.7.x compatibility)
702
QDomElement docElem = doc.documentElement();
703
QDomNode n = docElem.firstChild();
711
QDomElement msgElem2 = n.toElement();
712
if( !msgElem2.isNull() && msgElem2.tagName()=="head") {
713
// qDebug() << "found head";
714
QDomNodeList list = msgElem2.childNodes();
715
for (int i=0; i<list.size(); i++) {
716
QDomNode node = list.at(i);
717
if (node.isElement()) {
718
QDomElement h = node.toElement();
719
// qDebug() << "found tag " << h.tagName();
720
if (h.tagName() == "date") {
721
mon = h.attribute("month").toInt();
722
year = h.attribute("year").toInt();
723
} else if (h.tagName() == "contact") {
724
if (h.hasAttribute("type")) {
725
meString = h.attribute("contactId");
727
otherString = h.attribute("contactId");
728
log->other = Kopete::ContactList::self()->findContact(protocol, account, otherString);
730
log->me = log->other->account()->myself();
736
if (!log->me || !log->other || year <0 || mon < 0) {
737
detailsCursor.insertText("Missing information for: me="+meString+", other="+otherString+", year="+QString("%1").arg(year)+", mon="+QString("%1").arg(mon));
738
detailsCursor.insertBlock();
742
if( !msgElem2.isNull() && msgElem2.tagName()=="msg") {
743
rxTime.indexIn(msgElem2.attribute("time"));
744
QDateTime dt( QDate(year , mon , rxTime.cap(1).toUInt()), QTime( rxTime.cap(2).toUInt() , rxTime.cap(3).toUInt(), rxTime.cap(5).toUInt() ) );
746
m.incoming = (msgElem2.attribute("in") == "1");
748
m.text = msgElem2.text();
750
log->messages.append(m);
754
} // end while on messages
759
#include "history2import.moc"