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