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 * <sometag>....</sometag> - only "<" is escaped 0317 * See https://bugs.kde.org/show_bug.cgi?id=318751 0318 */ 0319 QRegExp rx(QLatin1String("\\<[^>]*>")); 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 }