1 /*
2  * Copyright (C) 2012 Dan Vrátil <dvratil@redhat.com>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17  */
18 
19 #include "logs-importer-private.h"
20 #include "logs-importer.h"
21 
22 #include <KLocalizedString>
23 #include "ktp-debug.h"
24 #include <QStandardPaths>
25 
26 using namespace KTp;
27 
Private(KTp::LogsImporter * parent)28 LogsImporter::Private::Private(KTp::LogsImporter* parent)
29   : QThread(parent)
30   , m_day(0)
31   , m_month(0)
32   , m_year(0)
33   , m_isMUCLog(false)
34 {
35 
36 }
37 
~Private()38 LogsImporter::Private::~Private()
39 {
40 
41 }
42 
setAccountId(const QString & accountId)43 void LogsImporter::Private::setAccountId(const QString& accountId)
44 {
45     m_accountId = accountId;
46 }
47 
run()48 void LogsImporter::Private::run()
49 {
50     QStringList files = findKopeteLogs(m_accountId);
51     if (files.isEmpty()) {
52         Q_EMIT error(i18n("No Kopete logs found"));
53         return;
54     }
55 
56     Q_FOREACH (const QString &file, files) {
57         convertKopeteLog(file);
58     }
59 }
60 
accountIdToAccountName(const QString & accountId) const61 QString LogsImporter::Private::accountIdToAccountName(const QString &accountId) const
62 {
63     int plugin = accountId.indexOf(QLatin1Char('/'));
64     int protocol = accountId.indexOf(QLatin1Char('/'), plugin + 1);
65 
66     QString username = accountId.mid(protocol + 1);
67 
68     /* ICQ accounts are prefixed with '_X' (X being a number) */
69     if (username.startsWith(QLatin1Char('_'))) {
70         username = username.remove(0, 2);
71     }
72 
73     /* Remove trailing "0" */
74     username.chop(1);
75 
76     /* Kopete escapes ".", "/", "~", "?" and "*" as "-" */
77     username.replace(QLatin1String("_2e"), QLatin1String("-")); /* . */
78     username.replace(QLatin1String("_2f"), QLatin1String("-")); /* / */
79     username.replace(QLatin1String("_7e"), QLatin1String("-")); /* ~ */
80     username.replace(QLatin1String("_3f"), QLatin1String("-")); /* ? */
81     username.replace(QLatin1String("_2a"), QLatin1String("-")); /* * */
82 
83     /* But Kopete has apparently no problem with "@", so unescape it */
84     username.replace(QLatin1String("_40"), QLatin1String("@"));
85 
86     return username;
87 }
88 
accountIdToProtocol(const QString & accountId) const89 QString LogsImporter::Private::accountIdToProtocol(const QString &accountId) const
90 {
91     if (accountId.startsWith(QLatin1String("haze/aim/"))) {
92         return QLatin1String("AIMProtocol");
93     } else if (accountId.startsWith(QLatin1String("haze/msn/"))) {
94         return QLatin1String("WlmProtocol");
95     } else if (accountId.startsWith(QLatin1String("haze/icq/"))) {
96         return QLatin1String("ICQProtocol");
97     } else if (accountId.startsWith(QLatin1String("haze/yahoo/"))) {
98         return QLatin1String("YahooProtocol");
99     } else if (accountId.startsWith(QLatin1String("gabble/jabber/"))) {
100         return QLatin1String("JabberProtocol");
101     } else if (accountId.startsWith(QLatin1String("sunshine/gadugadu/")) ||
102                accountId.startsWith(QLatin1String("haze/gadugadu/"))) {
103         return QLatin1String("GaduProtocol");
104     } else if (accountId.startsWith(QLatin1String("haze/groupwise"))) {
105         return QLatin1String("GroupWiseProtocol");
106     } else {
107         /* We don't support these Kopete protocols:
108          *         Bonjour - unable to reliably map Telepathy account to Kopete
109          *         Meanwhile - no support in Telepathy
110          *         QQ - no support in Telepathy
111          *         SMS - no support in Telepathy
112          *         Skype - not supported by KTp
113          *         WinPopup - no support in Telepathy
114          */
115         qCWarning(KTP_COMMONINTERNALS) << accountId << "is an unsupported protocol";
116         return QString();
117     }
118 }
119 
findKopeteLogs(const QString & accountId) const120 QStringList LogsImporter::Private::findKopeteLogs(const QString &accountId) const
121 {
122     QStringList files;
123 
124     QString protocol = accountIdToProtocol(accountId);
125     if (protocol.isEmpty()) {
126         qCWarning(KTP_COMMONINTERNALS) << "Unsupported protocol";
127         return files;
128     }
129 
130     QString kopeteAccountId = accountIdToAccountName(accountId);
131     if (kopeteAccountId.isEmpty()) {
132         qCWarning(KTP_COMMONINTERNALS) << "Unable to parse account ID";
133         return files;
134     }
135 
136     QDir dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kopete/logs/") +
137              protocol + QLatin1Char('/') + kopeteAccountId);
138 
139     if (dir.exists()) {
140         QFileInfoList entries = dir.entryInfoList(QStringList() << QLatin1String("*.xml"), QDir::Files | QDir::NoDotAndDotDot | QDir::Readable);
141         Q_FOREACH (const QFileInfo &finfo, entries) {
142             files << finfo.filePath();
143         }
144     }
145 
146     return files;
147 }
148 
initKTpDocument()149 void LogsImporter::Private::initKTpDocument()
150 {
151     m_ktpDocument.clear();
152     m_ktpLogElement.clear();
153 
154     QDomNode xmlNode = m_ktpDocument.createProcessingInstruction(
155         QLatin1String("xml"), QLatin1String("version='1.0' encoding='utf-8'"));
156     m_ktpDocument.appendChild(xmlNode);
157 
158     xmlNode = m_ktpDocument.createProcessingInstruction(
159         QLatin1String("xml-stylesheet"), QLatin1String("type=\"text/xsl\" href=\"log-store-xml.xsl\""));
160     m_ktpDocument.appendChild(xmlNode);
161 
162     m_ktpLogElement = m_ktpDocument.createElement(QLatin1String("log"));
163     m_ktpDocument.appendChild(m_ktpLogElement);
164 }
165 
saveKTpDocument()166 void LogsImporter::Private::saveKTpDocument()
167 {
168     QString filename = QString(QLatin1String("%1%2%3.log"))
169         .arg(m_year)
170         .arg(m_month, 2, 10, QLatin1Char('0'))
171         .arg(m_day, 2, 10, QLatin1Char('0'));
172 
173     QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/TpLogger/logs");
174 
175     if (m_isMUCLog) {
176         path += QDir::separator() + QLatin1String("chatrooms");
177     } else {
178         QString accountId = m_accountId;
179         /* Escape '/' in accountId as '_' */
180         if (m_accountId.contains(QLatin1Char('/'))) {
181           accountId.replace(QLatin1Char('/'), QLatin1String("_"));
182         }
183         path += QDir::separator() + accountId;
184     }
185 
186     path += QDir::separator() + m_contactId;
187 
188     /* Make sure the path exists */
189     QDir dir(path);
190     if (!dir.exists()) {
191         QDir::home().mkpath(QDir::home().relativeFilePath(dir.path()));
192     }
193 
194     path += QDir::separator() + filename;
195 
196     QFile outFile(path);
197     outFile.open(QIODevice::WriteOnly);
198     QTextStream stream(&outFile);
199     m_ktpDocument.save(stream, 0);
200 
201     qCDebug(KTP_COMMONINTERNALS) << "Stored as" << path;
202 }
203 
parseKopeteTime(const QDomElement & kopeteMessage) const204 QDateTime LogsImporter::Private::parseKopeteTime(const QDomElement& kopeteMessage) const
205 {
206     QString strtime = kopeteMessage.attribute(QLatin1String("time"));
207     if (strtime.isEmpty()) {
208         return QDateTime();
209     }
210 
211     /* Kopete time attribute is in format "D H:M:S" - year and month are stored in
212      * log header, Hour, minute and seconds don't have zero padding */
213     QStringList dateTime = strtime.split(QLatin1Char(' '), QString::SkipEmptyParts);
214     if (dateTime.length() != 2) {
215         return QDateTime();
216     }
217 
218     QStringList time = dateTime.at(1).split(QLatin1Char(':'));
219 
220     QString str = QString(QLatin1String("%1-%2-%3T%4:%5:%6Z"))
221         .arg(m_year)
222         .arg(m_month, 2, 10, QLatin1Char('0'))
223         .arg(dateTime.at(0).toInt(), 2, 10, QLatin1Char('0'))
224         .arg(time.at(0).toInt(), 2, 10, QLatin1Char('0'))
225         .arg(time.at(1).toInt(), 2, 10, QLatin1Char('0'))
226         .arg(time.at(2).toInt(), 2, 10, QLatin1Char('0'));
227 
228     /* Kopete stores date in local timezone but Telepathy in UTC. Note that we
229      * must use time offset at the specific date rather then current offset
230      * (could be different due to for example DST) */
231     QDateTime localTz = QDateTime::fromString(str, Qt::ISODate);
232     QDateTime utc = localTz.addSecs(-QDateTime::currentDateTime().timeZone().offsetData(localTz).offsetFromUtc);
233 
234     return utc;
235 }
236 
convertKopeteMessage(const QDomElement & kopeteMessage)237 QDomElement LogsImporter::Private::convertKopeteMessage(const QDomElement& kopeteMessage)
238 {
239     QDateTime time = parseKopeteTime(kopeteMessage);
240     if (!time.isValid()) {
241         qCWarning(KTP_COMMONINTERNALS) << "Failed to parse message time, skipping message";
242         return QDomElement();
243     }
244 
245     /* If this is the very first message we are processing, then initialize
246     * the day counter */
247     if (m_day == 0) {
248         m_day = time.date().day();
249     }
250 
251     /* Kopete stores logs by months, while Telepathy by days. When day changes,
252     * save to current KTp log and prepare a new document */
253     if (time.date().day() != m_day) {
254         saveKTpDocument();
255         m_day = time.date().day();
256 
257         initKTpDocument();
258     }
259 
260     QDomElement ktpMessage = m_ktpDocument.createElement(QLatin1String("message"));
261     ktpMessage.setAttribute(QStringLiteral("time"), time.toUTC().toString(QStringLiteral("%Y%m%dT%H:%M:%S")));
262 
263     QString sender = kopeteMessage.attribute(QLatin1String("from"));
264     if (!m_isMUCLog && sender.startsWith(m_contactId) && sender.length() > m_contactId.length()) {
265         m_isMUCLog = true;
266     }
267 
268     /* In MUC, the "from" attribute is in format "room@conf.server/senderId", so strip
269      * the room name */
270     if (m_isMUCLog) {
271         sender = sender.remove(m_contactId);
272     }
273 
274     ktpMessage.setAttribute(QLatin1String("id"), sender);
275     ktpMessage.setAttribute(QLatin1String("name"), kopeteMessage.attribute(QLatin1String("nick")));
276 
277     if (sender == m_meId) {
278         ktpMessage.setAttribute(QLatin1String("isuser"), QLatin1String("true"));
279     } else {
280         ktpMessage.setAttribute(QLatin1String("isuser"), QLatin1String("false"));
281     }
282 
283     /* These are not present in Kopete logs, but that should not matter */
284     ktpMessage.setAttribute(QLatin1String("token"), QString());
285     ktpMessage.setAttribute(QLatin1String("message-token"), QString());
286     ktpMessage.setAttribute(QLatin1String("type"), QLatin1String("normal"));
287 
288     /* Copy the message content */
289     QDomText message = m_ktpDocument.createTextNode(kopeteMessage.text());
290     ktpMessage.appendChild(message);
291 
292     return ktpMessage;
293 }
294 
convertKopeteLog(const QString & filepath)295 void LogsImporter::Private::convertKopeteLog(const QString& filepath)
296 {
297     qCDebug(KTP_COMMONINTERNALS) << "Converting" << filepath;
298 
299     /* Init */
300     m_day = 0;
301     m_month = 0;
302     m_year = 0;
303     m_isMUCLog = false;
304     m_meId.clear();
305     m_contactId.clear();
306 
307     initKTpDocument();
308 
309     QFile f(filepath);
310     f.open(QIODevice::ReadOnly);
311 
312     const QByteArray ba = f.readAll();
313     QString content = QString::fromUtf8(ba.constData(), ba.size());
314 
315     /* Strip Kopete HTML wrapping, which is always
316      * &lt;sometag>....&lt;/sometag> - only "<" is escaped
317      * See https://bugs.kde.org/show_bug.cgi?id=318751
318      */
319     QRegExp rx(QLatin1String("\\&lt;[^>]*>"));
320     rx.setMinimal(true);
321     content = content.replace(rx, QString());
322 
323     m_kopeteDocument.setContent(content);
324     /* Get <history> node */
325     QDomElement history = m_kopeteDocument.documentElement();
326     /* Get all <msg> nodes in <history> node */
327     QDomNodeList kopeteMessages = history.elementsByTagName(QLatin1String("msg"));
328 
329     /* Get <head> node and parse it */
330     QDomNodeList heads = history.elementsByTagName(QLatin1String("head"));
331     if (heads.isEmpty()) {
332         Q_EMIT error(i18n("Invalid Kopete log format"));
333         return;
334     }
335 
336     QDomNode head = heads.item(0);
337     QDomNodeList headData = head.childNodes();
338     if (headData.length() < 3) {
339         Q_EMIT error(i18n("Invalid Kopete log format"));
340         return;
341     }
342 
343     for (int i = 0; i < headData.count(); i++) {
344         QDomElement el = headData.item(i).toElement();
345 
346         if (el.tagName() == QLatin1String("date")) {
347             m_year = el.attribute(QLatin1String("year"), QString()).toInt();
348             m_month = el.attribute(QLatin1String("month"), QString()).toInt();
349         } else if (el.tagName() == QLatin1String("contact")) {
350             if (el.attribute(QLatin1String("type")) == QLatin1String("myself")) {
351                 m_meId = el.attribute(QLatin1String("contactId"));
352             } else {
353                 m_contactId = el.attribute(QLatin1String("contactId"));
354             }
355         }
356     }
357 
358     if ((m_year == 0) || (m_month == 0) || m_meId.isEmpty() || m_contactId.isEmpty()) {
359         qCWarning(KTP_COMMONINTERNALS) << "Failed to correctly parse header. Possibly invalid log format";
360         return;
361     }
362 
363     for (int i = 0; i < kopeteMessages.count(); i++) {
364         QDomElement kopeteMessage = kopeteMessages.item(i).toElement();
365 
366         QDomElement ktpMessage = convertKopeteMessage(kopeteMessage);
367 
368         m_ktpLogElement.appendChild(ktpMessage);
369     }
370 
371     saveKTpDocument();
372 }
373