MessageHandler.cpp 11.8 KB
Newer Older
1 2 3
/*
 *  Kaidan - A user-friendly XMPP client for every device!
 *
Linus Jahn's avatar
Linus Jahn committed
4 5
 *  Copyright (C) 2017-2018 Kaidan developers and contributors
 *  (see the LICENSE file for a full list of copyright authors)
6 7 8 9 10 11
 *
 *  Kaidan is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
Linus Jahn's avatar
Linus Jahn committed
12 13 14 15 16 17 18 19 20 21
 *  In addition, as a special exception, the author of Kaidan gives
 *  permission to link the code of its release with the OpenSSL
 *  project's "OpenSSL" library (or with modified versions of it that
 *  use the same license as the "OpenSSL" library), and distribute the
 *  linked executables. You must obey the GNU General Public License in
 *  all respects for all of the code used other than "OpenSSL". If you
 *  modify this file, you may extend this exception to your version of
 *  the file, but you are not obligated to do so.  If you do not wish to
 *  do so, delete this exception statement from your version.
 *
22 23 24 25 26 27
 *  Kaidan is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
Linus Jahn's avatar
Linus Jahn committed
28
 *  along with Kaidan.  If not, see <http://www.gnu.org/licenses/>.
29 30 31 32 33 34 35 36 37
 */

#include "MessageHandler.h"
// Std
#include <iostream>
// Qt
#include <QDateTime>
#include <QDebug>
#include <QString>
Linus Jahn's avatar
Linus Jahn committed
38 39
#include <QMimeDatabase>
#include <QMimeType>
40
// gloox
41 42 43
#include <gloox/client.h>
#include <gloox/message.h>
#include <gloox/messagesession.h>
44
#include <gloox/receipt.h>
Linus Jahn's avatar
Linus Jahn committed
45
#include <gloox/carbons.h>
Linus Jahn's avatar
Linus Jahn committed
46 47 48 49
#include "gloox-extensions/gloox-extensions.h"
#include "gloox-extensions/reference.h"
#include "gloox-extensions/sims.h"
#include "gloox-extensions/jinglefile.h"
50
#include "gloox-extensions/bitsofbinarydata.h"
51 52 53 54
// Kaidan
#include "MessageModel.h"
#include "Notifications.h"

55
QDateTime stringToQDateTime(std::string stamp)
56
{
57
	QString qStamp = QString::fromStdString(stamp);
58 59
	QDateTime dateTime;

60 61 62 63
	if (qStamp.contains('.'))
		dateTime = QDateTime::fromString(qStamp, Qt::ISODateWithMs);
	else
		dateTime = QDateTime::fromString(qStamp, Qt::ISODate);
64 65

	if (!dateTime.isValid())
66
		return QDateTime::currentDateTime().toUTC();
67

68 69
	// XMPP timestamps are always in UTC
	// also read it as such if 'Z' is missing in ISO timestamp
70 71 72 73
	dateTime.setTimeSpec(Qt::UTC);
	return dateTime;
}

74
MessageHandler::MessageHandler(gloox::Client *client, MessageModel *messageModel,
75 76
                               RosterModel *rosterModel, QObject *parent):
                               QObject(parent), client(client),
Linus Jahn's avatar
Linus Jahn committed
77
                               messageModel(messageModel), rosterModel(rosterModel)
78 79 80 81 82 83 84
{
}

MessageHandler::~MessageHandler()
{
}

Linus Jahn's avatar
Linus Jahn committed
85
void MessageHandler::setChatPartner(QString chatPartner)
86
{
Linus Jahn's avatar
Linus Jahn committed
87
	this->chatPartner = chatPartner;
88

89
	resetUnreadMessagesForJid(this->chatPartner);
90 91
}

Linus Jahn's avatar
Linus Jahn committed
92
void MessageHandler::handleMessage(const gloox::Message &stanza, gloox::MessageSession *session)
93
{
94 95
	bool isCarbonMessage = false;

Linus Jahn's avatar
Linus Jahn committed
96 97 98 99 100 101 102 103
	// this should contain the real message (e.g. containing the body)
	gloox::Message *message = const_cast<gloox::Message*>(&stanza);
	// if the real message is in a message carbon extract it from there
	if (stanza.hasEmbeddedStanza()) {
		// get the possible carbon extension
		const gloox::Carbons *carbon = stanza.findExtension<const gloox::Carbons>(gloox::ExtCarbons);

		// if the extension exists and contains a message, use it as the real message
104 105
		if (carbon && carbon->embeddedStanza()) {
			isCarbonMessage = true;
Linus Jahn's avatar
Linus Jahn committed
106
			message = static_cast<gloox::Message*>(carbon->embeddedStanza());
107
		}
Linus Jahn's avatar
Linus Jahn committed
108 109 110
	}

	QString body = QString::fromStdString(message->body());
111

112
	if (!body.isEmpty()) {
113
		//
Linus Jahn's avatar
Linus Jahn committed
114
		// Extract information of the message
115 116 117
		//

		// author is only the 'bare' JID: e.g. 'albert@einstein.ch'
118 119 120 121 122 123 124
		MessageModel::Message msg;
		msg.author = QString::fromStdString(message->from().bare());
		msg.authorResource = QString::fromStdString(message->from().resource());
		msg.recipient = QString::fromStdString(message->to().bare());
		msg.recipientResource = QString::fromStdString(message->to().resource());
		msg.id = QString::fromStdString(message->id());
		msg.sentByMe = (msg.author == QString::fromStdString(client->jid().bare()));
Linus Jahn's avatar
Linus Jahn committed
125 126 127
		msg.message = body;
		msg.type = MessageType::MessageText; // only text, no media

128 129
		// handle media sharing (SIMS) content
		handleMediaSharing(const_cast<gloox::Message*>(message), &msg);
130

Linus Jahn's avatar
Linus Jahn committed
131
		//
132
		// If it is a delayed delivery (containing a timestamp), use its timestamp
Linus Jahn's avatar
Linus Jahn committed
133 134 135 136
		//

		const gloox::DelayedDelivery *delayedDelivery = message->when();
		if (delayedDelivery)
137
			msg.timestamp = stringToQDateTime(delayedDelivery->stamp())
138
			            .toString(Qt::ISODate);
139

Linus Jahn's avatar
Linus Jahn committed
140
		// fallback: use current time from local clock
141 142
		if (msg.timestamp.isEmpty())
			msg.timestamp = QDateTime::currentDateTime().toUTC()
143
			            .toString(Qt::ISODate);
144

145
		// add the message to the database
146
		emit messageModel->addMessageRequested(msg);
147

Linus Jahn's avatar
Linus Jahn committed
148 149 150 151
		//
		// Send a new notification | TODO: Resolve nickname from JID
		//

152
		if (!msg.sentByMe && !isCarbonMessage)
Linus Jahn's avatar
Linus Jahn committed
153 154 155 156 157 158 159 160
			Notifications::sendMessageNotification(message->from().full(), body.toStdString());

		//
		// Update contact sort (lastExchanged), last message and unread message count
		//

		// the contact can differ if the message is really from a contact or just
		// a forward of another of the user's clients
161
		const QString contactJid = msg.sentByMe ? msg.recipient : msg.author;
162 163

		// update the last message for this contact
164
		emit rosterModel->setLastMessageRequested(contactJid, body);
165 166

		// update the last exchanged for this contact
167
		updateLastExchangedOfJid(contactJid);
168

Linus Jahn's avatar
Linus Jahn committed
169 170
		// Increase unread message counter
		// don't add new unread message if chat is opened or we wrote the message
171
		if (!isCarbonMessage && chatPartner != contactJid && !msg.sentByMe)
172
			newUnreadMessageForJid(contactJid);
173 174
	}

175
	// XEP-0184: Message Delivery Receipts
176 177 178 179
	// try to handle a possible delivery receipt
	handleReceiptMessage(message, isCarbonMessage);
}

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
void MessageHandler::handleMediaSharing(const gloox::Message *message,
                                        MessageModel::Message *msg)
{
	//
	// Get media sharing (SIMS) information
	//

	const gloox::Reference *ref = message->findExtension
						<gloox::Reference>(gloox::EXT_REFERENCES);
	if (ref && ref->getEmbeddedSIMS()) {
		gloox::SIMS *sims = ref->getEmbeddedSIMS();
		gloox::StringList sources = sims->sources();
		for (auto &source : sources) {
			if (source.rfind("https://", 0) == 0 ||
				source.rfind("http://", 0) == 0) {
				msg->mediaUrl = QString::fromStdString(source);
				break;
			}
		}

		gloox::Jingle::File *file = sims->file();
		if (file && file->valid()) {
			msg->message = QString::fromStdString(file->desc());
			msg->mediaSize = file->size();
			msg->mediaContentType = QString::fromStdString(file->mediaType());
			msg->mediaLastModified = stringToQDateTime(file->date()).toTime_t();
			QMimeType mimeType = QMimeDatabase().mimeTypeForName(msg->mediaContentType);
			msg->type = getMessageType(mimeType);
			for (gloox::Hash &hash : file->hashes())
				msg->mediaHashes.append(QString::fromStdString(hash.tag()->xml()));

			// extract thumbnail
			const gloox::Jingle::Thumb *thumb = file->thumb();
			if (thumb && thumb->valid()) {
				// check if uri is valid (it is a BoB content id [cid])
				std::string uri = thumb->uri();
				if (uri.rfind("cid:", 0) == 0) {
					const gloox::BitsOfBinaryData *thumbData = message->
						findExtension<gloox::BitsOfBinaryData>(gloox::EXT_BITSOFBINARY);

					// check if thumbnail uri matches the attached data uri
					if (thumbData && thumb->uri() == uri.substr(4, uri.length() - 4)) {
						// save media thumbnail
						msg->mediaThumb = QByteArray::fromBase64(
							QByteArray::fromStdString(thumbData->data())
						);
					}
				}
			}
		}
	}
}

233 234 235 236
void MessageHandler::handleReceiptMessage(const gloox::Message *message,
                                          bool isCarbonMessage)
{
	// check if message contains receipt
237
	gloox::Receipt *receipt = (gloox::Receipt*) message->findExtension<gloox::Receipt>(gloox::ExtReceipt);
238 239
	if (!receipt)
		return;
240

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
	// get the type of the receipt
	gloox::Receipt::ReceiptType receiptType = receipt->rcpt();

	if (receiptType == gloox::Receipt::Request && !isCarbonMessage) {
		// carbon messages won't be accepted to not send a receipt to own msgs
		// carbon msgs from other contacts should be processed by the first client

		// send the asked confirmation, that the message has been arrived
		// new message to the author of the request
		gloox::Message receiptMessage(gloox::Message::Chat, message->from());

		// add the receipt extension containing the request's message id
		gloox::Receipt *receiptPayload = new gloox::Receipt(gloox::Receipt::Received, message->id());
		receiptMessage.addExtension(receiptPayload);

		// send the receipt message
		client->send(receiptMessage);

	} else if (receiptType == gloox::Receipt::Received) {
		// Delivery Receipt Received -> mark message as read in db
		emit messageModel->setMessageAsDeliveredRequested(
			QString::fromStdString(receipt->id()));
263 264 265
	}
}

266
void MessageHandler::sendMessage(QString toJid, QString body)
267 268 269 270 271 272 273 274
{
	const std::string id = client->getID();

	addMessageToDb(toJid, body, QString::fromStdString(id), MessageType::MessageText);
	sendOnlyMessage(toJid, body, id);
}

void MessageHandler::sendOnlyMessage(QString &toJid, QString &body, const std::string &id)
275 276
{
	// create a new message
277 278
	gloox::Message message(gloox::Message::Chat, gloox::JID(toJid.toStdString()),
	                       body.toStdString());
279
	message.setID(id);
280 281 282 283 284 285 286 287

	// XEP-0184: Message Delivery Receipts
	// request a delivery receipt from the other client
	gloox::Receipt *receiptPayload = new gloox::Receipt(gloox::Receipt::Request);
	message.addExtension(receiptPayload);

	// send the message
	client->send(message);
288 289 290
}

void MessageHandler::addMessageToDb(QString &toJid, QString &body, QString id,
291
                                     MessageType type, QString mediaLocation)
292 293
{
	// add the message to the database
294 295 296 297 298 299 300 301 302 303 304
	MessageModel::Message msg;
	msg.timestamp = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate);
	msg.author = QString::fromStdString(client->jid().bare());
	msg.recipient = toJid;
	msg.message = body;
	msg.id = id;
	msg.sentByMe = true;
	msg.type = type;
	msg.mediaLocation = mediaLocation;

	emit messageModel->addMessageRequested(msg);
305 306

	// update the last message for this contact
307
	emit rosterModel->setLastMessageRequested(toJid, body);
308 309 310 311
	// update the last exchanged date
	updateLastExchangedOfJid(toJid);
}

312
void MessageHandler::updateLastExchangedOfJid(const QString &jid)
313 314
{
	QString dateTime = QDateTime::currentDateTime().toString(Qt::ISODate);
315
	emit rosterModel->setLastExchangedRequested(jid, dateTime);
316 317
}

318
void MessageHandler::newUnreadMessageForJid(const QString &jid)
319
{
320 321
	// add a new unread message to the contact
	emit rosterModel->newUnreadMessageRequested(jid);
322 323
}

324
void MessageHandler::resetUnreadMessagesForJid(const QString &jid)
325
{
326 327
	// reset the unread message count to 0
	emit rosterModel->setUnreadMessageCountRequested(jid, 0);
328
}
Linus Jahn's avatar
Linus Jahn committed
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346

MessageType MessageHandler::getMessageType(QMimeType& type)
{
	if (type.inherits("image/jpeg") || type.inherits("image/png") ||
	    type.inherits("image/gif"))
		return MessageType::MessageImage;
	else if (type.inherits("audio/flac") || type.inherits("audio/mp4") ||
	         type.inherits("audio/ogg") || type.inherits("audio/wav") ||
	         type.inherits("audio/mpeg") || type.inherits("audio/webm"))
		return MessageType::MessageAudio;
	else if (type.inherits("video/mpeg") || type.inherits("video/x-msvideo") ||
	         type.inherits("video/quicktime") || type.inherits("video/mp4") ||
	         type.inherits("video/x-matroska"))
		return MessageType::MessageVideo;
	else if (type.inherits("text/plain"))
		return MessageType::MessageDocument;
	return MessageType::MessageFile;
}