MessageHandler.cpp 10.1 KB
Newer Older
1 2 3
/*
 *  Kaidan - A user-friendly XMPP client for every device!
 *
Linus Jahn's avatar
Linus Jahn committed
4
 *  Copyright (C) 2016-2019 Kaidan developers and contributors
Linus Jahn's avatar
Linus Jahn committed
5
 *  (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
 */

#include "MessageHandler.h"
// Qt
#include <QDateTime>
34 35
#include <QMimeDatabase>
#include <QUrl>
Linus Jahn's avatar
Linus Jahn committed
36 37
// QXmpp
#include <QXmppClient.h>
38
#include <QXmppDiscoveryManager.h>
39 40
#include <QXmppRosterManager.h>
#include <QXmppUtils.h>
41
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 0, 0)
42 43
#include <QXmppCarbonManager.h>
#endif
44
// Kaidan
Linus Jahn's avatar
Linus Jahn committed
45
#include "Kaidan.h"
46
#include "Message.h"
47 48 49
#include "MessageModel.h"
#include "Notifications.h"

Linus Jahn's avatar
Linus Jahn committed
50 51 52
MessageHandler::MessageHandler(Kaidan *kaidan, QXmppClient *client, MessageModel *model,
                               QObject *parent)
	: QObject(parent), kaidan(kaidan), client(client), model(model)
53
{
Linus Jahn's avatar
Linus Jahn committed
54 55
	connect(client, &QXmppClient::messageReceived, this, &MessageHandler::handleMessage);
	connect(kaidan, &Kaidan::sendMessage, this, &MessageHandler::sendMessage);
56
	connect(kaidan, &Kaidan::correctMessage, this, &MessageHandler::correctMessage);
Linus Jahn's avatar
Linus Jahn committed
57 58 59

	client->addExtension(&receiptManager);
	connect(&receiptManager, &QXmppMessageReceiptManager::messageDelivered,
60
	        this, [=] (const QString&, const QString &id) {
Linus Jahn's avatar
Linus Jahn committed
61 62
		emit model->setMessageAsDeliveredRequested(id);
	});
63

64
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 0, 0)
65 66 67 68 69 70 71 72 73 74 75
	carbonManager = new QXmppCarbonManager();
	client->addExtension(carbonManager);

	// messages sent to our account (forwarded from another client)
	connect(carbonManager, &QXmppCarbonManager::messageReceived,
	        client, &QXmppClient::messageReceived);
	// messages sent from our account (but another client)
	connect(carbonManager, &QXmppCarbonManager::messageSent,
	        client, &QXmppClient::messageReceived);

	// carbons discovery
76
	auto *discoManager = client->findExtension<QXmppDiscoveryManager>();
77 78 79 80 81 82 83 84 85 86
	if (!discoManager)
		return;

	connect(discoManager, &QXmppDiscoveryManager::infoReceived,
	        this, &MessageHandler::handleDiscoInfo);
#endif
}

MessageHandler::~MessageHandler()
{
87
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 0, 0)
88 89
	delete carbonManager;
#endif
90 91
}

Linus Jahn's avatar
Linus Jahn committed
92
void MessageHandler::handleMessage(const QXmppMessage &msg)
93
{
Linus Jahn's avatar
Linus Jahn committed
94
    if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error)
95
		return;
96

97 98 99 100 101 102 103
	Message message;
	message.setFrom(QXmppUtils::jidToBareJid(msg.from()));
	message.setTo(QXmppUtils::jidToBareJid(msg.to()));
	message.setSentByMe(msg.from() == client->configuration().jidBare());
	message.setId(msg.id());
	message.setBody(msg.body());
	message.setMediaType(MessageType::MessageText); // default to text message without media
104
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 0, 1)
105 106 107
	message.setIsSpoiler(msg.isSpoiler());
	message.setSpoilerHint(msg.spoilerHint());
#else
Xavier's avatar
Xavier committed
108 109 110
	for (const QXmppElement &extension : msg.extensions()) {
		if (extension.tagName() == "spoiler" &&
		    extension.attribute("xmlns") == NS_SPOILERS) {
111 112
			message.setIsSpoiler(true);
			message.setSpoilerHint(extension.value());
Xavier's avatar
Xavier committed
113 114 115
			break;
		}
	}
116
#endif
117

118
	// check if message contains a link and also check out of band url
119
	QStringList bodyWords = message.body().split(" ");
120 121 122
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
	bodyWords.prepend(msg.outOfBandUrl());
#endif
123
	for (const QString &word : bodyWords) {
124
		if (!word.startsWith("https://") && !word.startsWith("http://"))
125 126 127 128 129 130
			continue;

		// check message type by file name in link
		// This is hacky, but needed without SIMS or an additional HTTP request.
		// Also, this can be useful when a user manually posts an HTTP url.
		QUrl url(word);
131 132
		const QList<QMimeType> mediaTypes =
		                QMimeDatabase().mimeTypesForFileName(url.fileName());
133
		for (const QMimeType &type : mediaTypes) {
134
			MessageType mType = Message::mediaTypeFromMimeType(type);
135 136
			if (mType == MessageType::MessageImage ||
			    mType == MessageType::MessageAudio ||
137 138 139
			    mType == MessageType::MessageVideo ||
			    mType == MessageType::MessageDocument ||
			    mType == MessageType::MessageFile) {
140 141 142
				message.setMediaType(mType);
				message.setMediaContentType(type.name());
				message.setOutOfBandUrl(url.toEncoded());
143 144 145 146 147
				break;
			}
		}
		break; // we can only handle one link
	}
148

Linus Jahn's avatar
Linus Jahn committed
149
	// get possible delay (timestamp)
150 151 152
	message.setStamp((msg.stamp().isNull() || !msg.stamp().isValid())
	                 ? QDateTime::currentDateTimeUtc()
	                 : msg.stamp().toUTC());
153

Linus Jahn's avatar
Linus Jahn committed
154
	// save the message to the database
155
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 0, 0)
156 157
	// in case of message correction, replace old message
	if (msg.replaceId().isEmpty()) {
158
		emit model->addMessageRequested(message);
159
	} else {
160 161 162 163 164 165
		message.setIsEdited(true);
		message.setId(QString());
		emit model->updateMessageRequested(msg.replaceId(), [=] (Message &m) {
			// replace completely
			m = message;
		});
166 167 168
	}
#else
	// no message correction with old QXmpp
169
	emit model->addMessageRequested(message);
170
#endif
171

Linus Jahn's avatar
Linus Jahn committed
172
	// Send a message notification
173

Linus Jahn's avatar
Linus Jahn committed
174 175
	// The contact can differ if the message is really from a contact or just
	// a forward of another of the user's clients.
176
	QString contactJid = message.sentByMe() ? message.to() : message.from();
Linus Jahn's avatar
Linus Jahn committed
177 178 179 180 181
	// resolve user-defined name of this JID
	QString contactName = client->rosterManager().getRosterEntry(contactJid).name();
	if (contactName.isEmpty())
		contactName = contactJid;

182
	if (!message.sentByMe())
183
		Notifications::sendMessageNotification(contactName, msg.body());
Xavier's avatar
Xavier committed
184 185 186

	// TODO: Move back following call to RosterManager::handleMessage when spoiler
	// messages are implemented in QXmpp
187 188 189 190 191 192 193 194 195
	const QString lastMessage =
		message.isSpoiler() ? message.spoilerHint().isEmpty() ? tr("Spoiler")
								      : message.spoilerHint()
				    : msg.body();
	emit kaidan->getRosterModel()->updateItemRequested(
		contactJid,
		[=] (RosterItem &item) {
			item.setLastMessage(lastMessage);
		}
Xavier's avatar
Xavier committed
196
	);
197 198
}

199 200 201 202
void MessageHandler::sendMessage(const QString& toJid,
                                 const QString& body,
                                 bool isSpoiler,
                                 const QString& spoilerHint)
203
{
Linus Jahn's avatar
Linus Jahn committed
204 205 206 207 208 209 210 211
	// TODO: Add offline message cache and send when connnected again
	if (client->state() != QXmppClient::ConnectedState) {
		emit kaidan->passiveNotificationRequested(
			tr("Could not send message, as a result of not being connected.")
		);
		qWarning() << "[client] [MessageHandler] Could not send message, as a result of "
		              "not being connected.";
		return;
Linus Jahn's avatar
Linus Jahn committed
212
	}
213

214 215 216 217 218 219 220 221 222
	Message msg;
	msg.setFrom(client->configuration().jidBare());
	msg.setTo(toJid);
	msg.setBody(body);
	msg.setId(QXmppUtils::generateStanzaHash(28));
	msg.setReceiptRequested(true);
	msg.setSentByMe(true);
	msg.setMediaType(MessageType::MessageText); // text message without media
	msg.setStamp(QDateTime::currentDateTimeUtc());
Xavier's avatar
Xavier committed
223
	if (isSpoiler) {
224 225 226 227 228
		msg.setIsSpoiler(isSpoiler);
		msg.setSpoilerHint(spoilerHint);

		// parsing/serialization of spoilers isn't implemented in QXmpp
		QXmppElementList extensions = msg.extensions();
Xavier's avatar
Xavier committed
229 230
		QXmppElement spoiler = QXmppElement();
		spoiler.setTagName("spoiler");
231
		spoiler.setValue(msg.spoilerHint());
Xavier's avatar
Xavier committed
232 233
		spoiler.setAttribute("xmlns", NS_SPOILERS);
		extensions.append(spoiler);
234
		msg.setExtensions(extensions);
Xavier's avatar
Xavier committed
235
	}
236

237 238 239 240 241 242
	emit model->addMessageRequested(msg);

	if (client->sendPacket(static_cast<QXmppMessage>(msg)))
		emit model->setMessageAsSentRequested(msg.id());
	else
	        emit kaidan->passiveNotificationRequested(tr("Message could not be sent."));
243 244 245
	// TODO: handle error
}

246 247 248
void MessageHandler::correctMessage(const QString& toJid,
                                    const QString& msgId,
                                    const QString& body)
249 250 251 252 253 254 255 256 257 258 259 260 261 262
{
	// TODO: load old message from model and put everything into the new message
	//       instead of only the new body

	// TODO: Add offline message cache and send when connnected again
	if (client->state() != QXmppClient::ConnectedState) {
		emit kaidan->passiveNotificationRequested(
			tr("Could not correct message, as a result of not being connected.")
		);
		qWarning() << "[client] [MessageHandler] Could not correct message, as a result of "
		              "not being connected.";
		return;
	}

263 264 265 266 267 268 269 270 271
	Message msg;
	msg.setFrom(client->configuration().jidBare());
	msg.setTo(toJid);
	msg.setId(QXmppUtils::generateStanzaHash(28));
	msg.setBody(body);
	msg.setReceiptRequested(true);
	msg.setSentByMe(true);
	msg.setMediaType(MessageType::MessageText); // text message without media
	msg.setIsEdited(true);
272
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 0, 0)
273
	msg.setReplaceId(msgId);
274 275
#endif

276 277 278 279 280 281 282 283
	emit model->updateMessageRequested(msgId, [=] (Message &msg) {
		msg.setBody(body);
	});
	if (client->sendPacket(msg))
		emit model->setMessageAsSentRequested(msg.id());
	else
		emit kaidan->passiveNotificationRequested(
	                        tr("Message correction was not successful."));
284
}
Linus Jahn's avatar
Linus Jahn committed
285

286 287
void MessageHandler::handleDiscoInfo(const QXmppDiscoveryIq &info)
{
288
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 0, 0)
289 290 291
	if (info.from() != client->configuration().domain())
		return;
	// enable carbons, if feature found
292
	if (info.features().contains(NS_CARBONS))
293 294 295
		carbonManager->setCarbonsEnabled(true);
#endif
}