File indexing completed on 2023-12-03 05:01:58

0001 /*
0002  * Copyright (C) 2012 Dan Vrátil <dvratil@redhat.com>
0003  *
0004  * This library is free software; you can redistribute it and/or
0005  * modify it under the terms of the GNU Lesser General Public
0006  * License as published by the Free Software Foundation; either
0007  * version 2.1 of the License, or (at your option) any later version.
0008  *
0009  * This library is distributed in the hope that it will be useful,
0010  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0012  * Lesser General Public License for more details.
0013  *
0014  * You should have received a copy of the GNU Lesser General Public
0015  * License along with this library; if not, write to the Free Software
0016  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
0017  */
0018 
0019 #include "logs-importer-private.h"
0020 #include "logs-importer.h"
0021 
0022 #include <KLocalizedString>
0023 #include "ktp-debug.h"
0024 #include <QStandardPaths>
0025 
0026 using namespace KTp;
0027 
0028 LogsImporter::Private::Private(KTp::LogsImporter* parent)
0029   : QThread(parent)
0030   , m_day(0)
0031   , m_month(0)
0032   , m_year(0)
0033   , m_isMUCLog(false)
0034 {
0035 
0036 }
0037 
0038 LogsImporter::Private::~Private()
0039 {
0040 
0041 }
0042 
0043 void LogsImporter::Private::setAccountId(const QString& accountId)
0044 {
0045     m_accountId = accountId;
0046 }
0047 
0048 void LogsImporter::Private::run()
0049 {
0050     QStringList files = findKopeteLogs(m_accountId);
0051     if (files.isEmpty()) {
0052         Q_EMIT error(i18n("No Kopete logs found"));
0053         return;
0054     }
0055 
0056     Q_FOREACH (const QString &file, files) {
0057         convertKopeteLog(file);
0058     }
0059 }
0060 
0061 QString LogsImporter::Private::accountIdToAccountName(const QString &accountId) const
0062 {
0063     int plugin = accountId.indexOf(QLatin1Char('/'));
0064     int protocol = accountId.indexOf(QLatin1Char('/'), plugin + 1);
0065 
0066     QString username = accountId.mid(protocol + 1);
0067 
0068     /* ICQ accounts are prefixed with '_X' (X being a number) */
0069     if (username.startsWith(QLatin1Char('_'))) {
0070         username = username.remove(0, 2);
0071     }
0072 
0073     /* Remove trailing "0" */
0074     username.chop(1);
0075 
0076     /* Kopete escapes ".", "/", "~", "?" and "*" as "-" */
0077     username.replace(QLatin1String("_2e"), QLatin1String("-")); /* . */
0078     username.replace(QLatin1String("_2f"), QLatin1String("-")); /* / */
0079     username.replace(QLatin1String("_7e"), QLatin1String("-")); /* ~ */
0080     username.replace(QLatin1String("_3f"), QLatin1String("-")); /* ? */
0081     username.replace(QLatin1String("_2a"), QLatin1String("-")); /* * */
0082 
0083     /* But Kopete has apparently no problem with "@", so unescape it */
0084     username.replace(QLatin1String("_40"), QLatin1String("@"));
0085 
0086     return username;
0087 }
0088 
0089 QString LogsImporter::Private::accountIdToProtocol(const QString &accountId) const
0090 {
0091     if (accountId.startsWith(QLatin1String("haze/aim/"))) {
0092         return QLatin1String("AIMProtocol");
0093     } else if (accountId.startsWith(QLatin1String("haze/msn/"))) {
0094         return QLatin1String("WlmProtocol");
0095     } else if (accountId.startsWith(QLatin1String("haze/icq/"))) {
0096         return QLatin1String("ICQProtocol");
0097     } else if (accountId.startsWith(QLatin1String("haze/yahoo/"))) {
0098         return QLatin1String("YahooProtocol");
0099     } else if (accountId.startsWith(QLatin1String("gabble/jabber/"))) {
0100         return QLatin1String("JabberProtocol");
0101     } else if (accountId.startsWith(QLatin1String("sunshine/gadugadu/")) ||
0102                accountId.startsWith(QLatin1String("haze/gadugadu/"))) {
0103         return QLatin1String("GaduProtocol");
0104     } else if (accountId.startsWith(QLatin1String("haze/groupwise"))) {
0105         return QLatin1String("GroupWiseProtocol");
0106     } else {
0107         /* We don't support these Kopete protocols:
0108          *         Bonjour - unable to reliably map Telepathy account to Kopete
0109          *         Meanwhile - no support in Telepathy
0110          *         QQ - no support in Telepathy
0111          *         SMS - no support in Telepathy
0112          *         Skype - not supported by KTp
0113          *         WinPopup - no support in Telepathy
0114          */
0115         qCWarning(KTP_COMMONINTERNALS) << accountId << "is an unsupported protocol";
0116         return QString();
0117     }
0118 }
0119 
0120 QStringList LogsImporter::Private::findKopeteLogs(const QString &accountId) const
0121 {
0122     QStringList files;
0123 
0124     QString protocol = accountIdToProtocol(accountId);
0125     if (protocol.isEmpty()) {
0126         qCWarning(KTP_COMMONINTERNALS) << "Unsupported protocol";
0127         return files;
0128     }
0129 
0130     QString kopeteAccountId = accountIdToAccountName(accountId);
0131     if (kopeteAccountId.isEmpty()) {
0132         qCWarning(KTP_COMMONINTERNALS) << "Unable to parse account ID";
0133         return files;
0134     }
0135 
0136     QDir dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kopete/logs/") +
0137              protocol + QLatin1Char('/') + kopeteAccountId);
0138 
0139     if (dir.exists()) {
0140         QFileInfoList entries = dir.entryInfoList(QStringList() << QLatin1String("*.xml"), QDir::Files | QDir::NoDotAndDotDot | QDir::Readable);
0141         Q_FOREACH (const QFileInfo &finfo, entries) {
0142             files << finfo.filePath();
0143         }
0144     }
0145 
0146     return files;
0147 }
0148 
0149 void LogsImporter::Private::initKTpDocument()
0150 {
0151     m_ktpDocument.clear();
0152     m_ktpLogElement.clear();
0153 
0154     QDomNode xmlNode = m_ktpDocument.createProcessingInstruction(
0155         QLatin1String("xml"), QLatin1String("version='1.0' encoding='utf-8'"));
0156     m_ktpDocument.appendChild(xmlNode);
0157 
0158     xmlNode = m_ktpDocument.createProcessingInstruction(
0159         QLatin1String("xml-stylesheet"), QLatin1String("type=\"text/xsl\" href=\"log-store-xml.xsl\""));
0160     m_ktpDocument.appendChild(xmlNode);
0161 
0162     m_ktpLogElement = m_ktpDocument.createElement(QLatin1String("log"));
0163     m_ktpDocument.appendChild(m_ktpLogElement);
0164 }
0165 
0166 void LogsImporter::Private::saveKTpDocument()
0167 {
0168     QString filename = QString(QLatin1String("%1%2%3.log"))
0169         .arg(m_year)
0170         .arg(m_month, 2, 10, QLatin1Char('0'))
0171         .arg(m_day, 2, 10, QLatin1Char('0'));
0172 
0173     QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/TpLogger/logs");
0174 
0175     if (m_isMUCLog) {
0176         path += QDir::separator() + QLatin1String("chatrooms");
0177     } else {
0178         QString accountId = m_accountId;
0179         /* Escape '/' in accountId as '_' */
0180         if (m_accountId.contains(QLatin1Char('/'))) {
0181           accountId.replace(QLatin1Char('/'), QLatin1String("_"));
0182         }
0183         path += QDir::separator() + accountId;
0184     }
0185 
0186     path += QDir::separator() + m_contactId;
0187 
0188     /* Make sure the path exists */
0189     QDir dir(path);
0190     if (!dir.exists()) {
0191         QDir::home().mkpath(QDir::home().relativeFilePath(dir.path()));
0192     }
0193 
0194     path += QDir::separator() + filename;
0195 
0196     QFile outFile(path);
0197     outFile.open(QIODevice::WriteOnly);
0198     QTextStream stream(&outFile);
0199     m_ktpDocument.save(stream, 0);
0200 
0201     qCDebug(KTP_COMMONINTERNALS) << "Stored as" << path;
0202 }
0203 
0204 QDateTime LogsImporter::Private::parseKopeteTime(const QDomElement& kopeteMessage) const
0205 {
0206     QString strtime = kopeteMessage.attribute(QLatin1String("time"));
0207     if (strtime.isEmpty()) {
0208         return QDateTime();
0209     }
0210 
0211     /* Kopete time attribute is in format "D H:M:S" - year and month are stored in
0212      * log header, Hour, minute and seconds don't have zero padding */
0213     QStringList dateTime = strtime.split(QLatin1Char(' '), QString::SkipEmptyParts);
0214     if (dateTime.length() != 2) {
0215         return QDateTime();
0216     }
0217 
0218     QStringList time = dateTime.at(1).split(QLatin1Char(':'));
0219 
0220     QString str = QString(QLatin1String("%1-%2-%3T%4:%5:%6Z"))
0221         .arg(m_year)
0222         .arg(m_month, 2, 10, QLatin1Char('0'))
0223         .arg(dateTime.at(0).toInt(), 2, 10, QLatin1Char('0'))
0224         .arg(time.at(0).toInt(), 2, 10, QLatin1Char('0'))
0225         .arg(time.at(1).toInt(), 2, 10, QLatin1Char('0'))
0226         .arg(time.at(2).toInt(), 2, 10, QLatin1Char('0'));
0227 
0228     /* Kopete stores date in local timezone but Telepathy in UTC. Note that we
0229      * must use time offset at the specific date rather then current offset
0230      * (could be different due to for example DST) */
0231     QDateTime localTz = QDateTime::fromString(str, Qt::ISODate);
0232     QDateTime utc = localTz.addSecs(-QDateTime::currentDateTime().timeZone().offsetData(localTz).offsetFromUtc);
0233 
0234     return utc;
0235 }
0236 
0237 QDomElement LogsImporter::Private::convertKopeteMessage(const QDomElement& kopeteMessage)
0238 {
0239     QDateTime time = parseKopeteTime(kopeteMessage);
0240     if (!time.isValid()) {
0241         qCWarning(KTP_COMMONINTERNALS) << "Failed to parse message time, skipping message";
0242         return QDomElement();
0243     }
0244 
0245     /* If this is the very first message we are processing, then initialize
0246     * the day counter */
0247     if (m_day == 0) {
0248         m_day = time.date().day();
0249     }
0250 
0251     /* Kopete stores logs by months, while Telepathy by days. When day changes,
0252     * save to current KTp log and prepare a new document */
0253     if (time.date().day() != m_day) {
0254         saveKTpDocument();
0255         m_day = time.date().day();
0256 
0257         initKTpDocument();
0258     }
0259 
0260     QDomElement ktpMessage = m_ktpDocument.createElement(QLatin1String("message"));
0261     ktpMessage.setAttribute(QStringLiteral("time"), time.toUTC().toString(QStringLiteral("%Y%m%dT%H:%M:%S")));
0262 
0263     QString sender = kopeteMessage.attribute(QLatin1String("from"));
0264     if (!m_isMUCLog && sender.startsWith(m_contactId) && sender.length() > m_contactId.length()) {
0265         m_isMUCLog = true;
0266     }
0267 
0268     /* In MUC, the "from" attribute is in format "room@conf.server/senderId", so strip
0269      * the room name */
0270     if (m_isMUCLog) {
0271         sender = sender.remove(m_contactId);
0272     }
0273 
0274     ktpMessage.setAttribute(QLatin1String("id"), sender);
0275     ktpMessage.setAttribute(QLatin1String("name"), kopeteMessage.attribute(QLatin1String("nick")));
0276 
0277     if (sender == m_meId) {
0278         ktpMessage.setAttribute(QLatin1String("isuser"), QLatin1String("true"));
0279     } else {
0280         ktpMessage.setAttribute(QLatin1String("isuser"), QLatin1String("false"));
0281     }
0282 
0283     /* These are not present in Kopete logs, but that should not matter */
0284     ktpMessage.setAttribute(QLatin1String("token"), QString());
0285     ktpMessage.setAttribute(QLatin1String("message-token"), QString());
0286     ktpMessage.setAttribute(QLatin1String("type"), QLatin1String("normal"));
0287 
0288     /* Copy the message content */
0289     QDomText message = m_ktpDocument.createTextNode(kopeteMessage.text());
0290     ktpMessage.appendChild(message);
0291 
0292     return ktpMessage;
0293 }
0294 
0295 void LogsImporter::Private::convertKopeteLog(const QString& filepath)
0296 {
0297     qCDebug(KTP_COMMONINTERNALS) << "Converting" << filepath;
0298 
0299     /* Init */
0300     m_day = 0;
0301     m_month = 0;
0302     m_year = 0;
0303     m_isMUCLog = false;
0304     m_meId.clear();
0305     m_contactId.clear();
0306 
0307     initKTpDocument();
0308 
0309     QFile f(filepath);
0310     f.open(QIODevice::ReadOnly);
0311 
0312     const QByteArray ba = f.readAll();
0313     QString content = QString::fromUtf8(ba.constData(), ba.size());
0314 
0315     /* Strip Kopete HTML wrapping, which is always
0316      * &lt;sometag>....&lt;/sometag> - only "<" is escaped
0317      * See https://bugs.kde.org/show_bug.cgi?id=318751
0318      */
0319     QRegExp rx(QLatin1String("\\&lt;[^>]*>"));
0320     rx.setMinimal(true);
0321     content = content.replace(rx, QString());
0322 
0323     m_kopeteDocument.setContent(content);
0324     /* Get <history> node */
0325     QDomElement history = m_kopeteDocument.documentElement();
0326     /* Get all <msg> nodes in <history> node */
0327     QDomNodeList kopeteMessages = history.elementsByTagName(QLatin1String("msg"));
0328 
0329     /* Get <head> node and parse it */
0330     QDomNodeList heads = history.elementsByTagName(QLatin1String("head"));
0331     if (heads.isEmpty()) {
0332         Q_EMIT error(i18n("Invalid Kopete log format"));
0333         return;
0334     }
0335 
0336     QDomNode head = heads.item(0);
0337     QDomNodeList headData = head.childNodes();
0338     if (headData.length() < 3) {
0339         Q_EMIT error(i18n("Invalid Kopete log format"));
0340         return;
0341     }
0342 
0343     for (int i = 0; i < headData.count(); i++) {
0344         QDomElement el = headData.item(i).toElement();
0345 
0346         if (el.tagName() == QLatin1String("date")) {
0347             m_year = el.attribute(QLatin1String("year"), QString()).toInt();
0348             m_month = el.attribute(QLatin1String("month"), QString()).toInt();
0349         } else if (el.tagName() == QLatin1String("contact")) {
0350             if (el.attribute(QLatin1String("type")) == QLatin1String("myself")) {
0351                 m_meId = el.attribute(QLatin1String("contactId"));
0352             } else {
0353                 m_contactId = el.attribute(QLatin1String("contactId"));
0354             }
0355         }
0356     }
0357 
0358     if ((m_year == 0) || (m_month == 0) || m_meId.isEmpty() || m_contactId.isEmpty()) {
0359         qCWarning(KTP_COMMONINTERNALS) << "Failed to correctly parse header. Possibly invalid log format";
0360         return;
0361     }
0362 
0363     for (int i = 0; i < kopeteMessages.count(); i++) {
0364         QDomElement kopeteMessage = kopeteMessages.item(i).toElement();
0365 
0366         QDomElement ktpMessage = convertKopeteMessage(kopeteMessage);
0367 
0368         m_ktpLogElement.appendChild(ktpMessage);
0369     }
0370 
0371     saveKTpDocument();
0372 }