MessageHandler.cpp 9.36 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
#include <QMimeDatabase>
35
#include <QString>
36
#include <QUrl>
Linus Jahn's avatar
Linus Jahn committed
37 38 39 40
// QXmpp
#include <QXmppClient.h>
#include <QXmppUtils.h>
#include <QXmppRosterManager.h>
41
#include <QXmppDiscoveryManager.h>
42
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
43 44
#include <QXmppCarbonManager.h>
#endif
45
// Kaidan
Linus Jahn's avatar
Linus Jahn committed
46
#include "Kaidan.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 60 61 62

	client->addExtension(&receiptManager);
	connect(&receiptManager, &QXmppMessageReceiptManager::messageDelivered,
		[=] (const QString&, const QString &id) {
		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())
95
		return;
96

Linus Jahn's avatar
Linus Jahn committed
97 98 99 100 101 102
	MessageModel::Message entry;
	entry.author = QXmppUtils::jidToBareJid(msg.from());
	entry.recipient = QXmppUtils::jidToBareJid(msg.to());
	entry.id = msg.id();
	entry.sentByMe = (entry.author == client->configuration().jidBare());
	entry.message = msg.body();
Xavier's avatar
Xavier committed
103 104 105 106 107 108 109 110
	for (const QXmppElement &extension : msg.extensions()) {
		if (extension.tagName() == "spoiler" &&
		    extension.attribute("xmlns") == NS_SPOILERS) {
			entry.isSpoiler = true;
			entry.spoilerHint = extension.value();
			break;
		}
	}
111 112
	entry.type = MessageType::MessageText; // default to text message without media

113
	// check if message contains a link and also check out of band url
114
	QList<QString> bodyWords = msg.body().split(" ");
115 116 117
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
	bodyWords.prepend(msg.outOfBandUrl());
#endif
118
	for (const QString &word : bodyWords) {
119
		if (!word.startsWith("https://") && !word.startsWith("http://"))
120 121 122 123 124 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);
		QList<QMimeType> mediaTypes = QMimeDatabase().mimeTypesForFileName(url.fileName());
		for (const QMimeType &type : mediaTypes) {
			MessageType mType = MessageModel::messageTypeFromMimeType(type);
			if (mType == MessageType::MessageImage ||
			    mType == MessageType::MessageAudio ||
131 132 133
			    mType == MessageType::MessageVideo ||
			    mType == MessageType::MessageDocument ||
			    mType == MessageType::MessageFile) {
134 135 136 137 138 139 140 141
				entry.type = mType;
				entry.mediaContentType = type.name();
				entry.mediaUrl = url.toEncoded();
				break;
			}
		}
		break; // we can only handle one link
	}
142

Linus Jahn's avatar
Linus Jahn committed
143
	// get possible delay (timestamp)
144 145 146
	entry.timestamp = (msg.stamp().isNull() || !msg.stamp().isValid())
	                  ? QDateTime::currentDateTimeUtc().toString(Qt::ISODate)
	                  : msg.stamp().toUTC().toString(Qt::ISODate);
147

Linus Jahn's avatar
Linus Jahn committed
148
	// save the message to the database
149 150 151 152 153 154
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
	// in case of message correction, replace old message
	if (msg.replaceId().isEmpty()) {
		emit model->addMessageRequested(entry);
	} else {
		entry.edited = true;
155
		entry.id = "";
156 157 158 159
		emit model->updateMessageRequested(msg.replaceId(), entry);
	}
#else
	// no message correction with old QXmpp
Linus Jahn's avatar
Linus Jahn committed
160
	emit model->addMessageRequested(entry);
161
#endif
162

Linus Jahn's avatar
Linus Jahn committed
163 164 165 166 167 168 169 170 171 172
	// Send a message notification
	//
	// The contact can differ if the message is really from a contact or just
	// a forward of another of the user's clients.
	QString contactJid = entry.sentByMe ? entry.recipient : entry.author;
	// resolve user-defined name of this JID
	QString contactName = client->rosterManager().getRosterEntry(contactJid).name();
	if (contactName.isEmpty())
		contactName = contactJid;

173
	if (!entry.sentByMe)
Linus Jahn's avatar
Linus Jahn committed
174 175
		Notifications::sendMessageNotification(contactName.toStdString(),
		                                       msg.body().toStdString());
Xavier's avatar
Xavier committed
176 177 178 179 180 181 182

	// TODO: Move back following call to RosterManager::handleMessage when spoiler
	// messages are implemented in QXmpp
	emit kaidan->getRosterModel()->setLastMessageRequested(contactJid,
	        entry.isSpoiler ? entry.spoilerHint.isEmpty() ? tr("Spoiler") : entry.spoilerHint
	                        : msg.body()
	);
183 184
}

Xavier's avatar
Xavier committed
185
void MessageHandler::sendMessage(QString toJid, QString body, bool isSpoiler, QString spoilerHint)
186
{
Linus Jahn's avatar
Linus Jahn committed
187 188 189 190 191 192 193 194
	// 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
195
	}
196

197
	MessageModel::Message msg;
Xavier's avatar
Xavier committed
198 199
	msg.isSpoiler = isSpoiler;
	msg.spoilerHint = spoilerHint;
Linus Jahn's avatar
Linus Jahn committed
200
	msg.author = client->configuration().jidBare();
201
	msg.recipient = toJid;
Linus Jahn's avatar
Linus Jahn committed
202
	msg.id = QXmppUtils::generateStanzaHash(48);
203
	msg.sentByMe = true;
Linus Jahn's avatar
Linus Jahn committed
204 205
	msg.message = body;
	msg.type = MessageType::MessageText; // text message without media
Xavier's avatar
Xavier committed
206
	msg.timestamp = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
207

Linus Jahn's avatar
Linus Jahn committed
208
	emit model->addMessageRequested(msg);
209

Linus Jahn's avatar
Linus Jahn committed
210 211 212
	QXmppMessage m(msg.author, msg.recipient, body);
	m.setId(msg.id);
	m.setReceiptRequested(true);
Xavier's avatar
Xavier committed
213 214 215 216 217 218 219 220 221
	if (isSpoiler) {
		QXmppElementList extensions = m.extensions();
		QXmppElement spoiler = QXmppElement();
		spoiler.setTagName("spoiler");
		spoiler.setValue(msg.spoilerHint);
		spoiler.setAttribute("xmlns", NS_SPOILERS);
		extensions.append(spoiler);
		m.setExtensions(extensions);
	}
222

223
	if (client->sendPacket(m))
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
		emit model->setMessageAsSentRequested(msg.id);
	// TODO: handle error
}

void MessageHandler::correctMessage(QString toJid, QString msgId, QString body)
{
	// 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;
	}

	MessageModel::Message msg;
	msg.author = client->configuration().jidBare();
	msg.recipient = toJid;
	msg.sentByMe = true;
	msg.message = body;
	msg.type = MessageType::MessageText; // text message without media
	msg.edited = true;

	emit model->updateMessageRequested(msgId, msg);

	QXmppMessage m(msg.author, msg.recipient, body);
	m.setReceiptRequested(true);
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
	m.setReplaceId(msgId);
#endif

259
	if (client->sendPacket(m))
260 261
		emit model->setMessageAsSentRequested(msg.id);
	// TODO: handle error
262
}
Linus Jahn's avatar
Linus Jahn committed
263

264 265
void MessageHandler::handleDiscoInfo(const QXmppDiscoveryIq &info)
{
266
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
267 268 269
	if (info.from() != client->configuration().domain())
		return;
	// enable carbons, if feature found
270
	if (info.features().contains(NS_CARBONS))
271 272 273
		carbonManager->setCarbonsEnabled(true);
#endif
}