MessageHandler.cpp 9.46 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
{
94 95
	bool isCarbonMessage = false;

Linus Jahn's avatar
Linus Jahn committed
96
	if (msg.body().isEmpty())
97
		return;
98

Linus Jahn's avatar
Linus Jahn committed
99 100 101 102 103 104
	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
105 106 107 108 109 110 111 112
	for (const QXmppElement &extension : msg.extensions()) {
		if (extension.tagName() == "spoiler" &&
		    extension.attribute("xmlns") == NS_SPOILERS) {
			entry.isSpoiler = true;
			entry.spoilerHint = extension.value();
			break;
		}
	}
113 114
	entry.type = MessageType::MessageText; // default to text message without media

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

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

Linus Jahn's avatar
Linus Jahn committed
150
	// save the message to the database
151 152 153 154 155 156 157 158 159 160
#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;
		emit model->updateMessageRequested(msg.replaceId(), entry);
	}
#else
	// no message correction with old QXmpp
Linus Jahn's avatar
Linus Jahn committed
161
	emit model->addMessageRequested(entry);
162
#endif
163

Linus Jahn's avatar
Linus Jahn committed
164 165 166 167 168 169 170 171 172 173 174 175 176
	// 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;

	if (!entry.sentByMe && !isCarbonMessage)
		Notifications::sendMessageNotification(contactName.toStdString(),
		                                       msg.body().toStdString());
Xavier's avatar
Xavier committed
177 178 179 180 181 182 183

	// 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()
	);
184 185
}

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

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

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

Linus Jahn's avatar
Linus Jahn committed
211 212 213
	QXmppMessage m(msg.author, msg.recipient, body);
	m.setId(msg.id);
	m.setReceiptRequested(true);
Xavier's avatar
Xavier committed
214 215 216 217 218 219 220 221 222
	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);
	}
223

224
	if (client->sendPacket(m))
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 259 260 261
		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.id = QXmppUtils::generateStanzaHash(48);
	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.setId(msg.id);
	m.setReceiptRequested(true);
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
	m.setReplaceId(msgId);
#endif

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

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