diff --git a/src/MessageHandler.cpp b/src/MessageHandler.cpp index 75adbcc..c91fdf3 100644 --- a/src/MessageHandler.cpp +++ b/src/MessageHandler.cpp @@ -1,291 +1,291 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * 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. * * 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. * * 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 * along with Kaidan. If not, see . */ #include "MessageHandler.h" // Qt #include #include #include // QXmpp #include #include #include #include #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) #include #endif // Kaidan #include "Kaidan.h" #include "Message.h" #include "MessageModel.h" #include "Notifications.h" MessageHandler::MessageHandler(Kaidan *kaidan, QXmppClient *client, MessageModel *model, QObject *parent) : QObject(parent), kaidan(kaidan), client(client), model(model) { connect(client, &QXmppClient::messageReceived, this, &MessageHandler::handleMessage); connect(kaidan, &Kaidan::sendMessage, this, &MessageHandler::sendMessage); connect(kaidan, &Kaidan::correctMessage, this, &MessageHandler::correctMessage); client->addExtension(&receiptManager); connect(&receiptManager, &QXmppMessageReceiptManager::messageDelivered, this, [=] (const QString&, const QString &id) { emit model->setMessageAsDeliveredRequested(id); }); #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) 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 auto *discoManager = client->findExtension(); if (!discoManager) return; connect(discoManager, &QXmppDiscoveryManager::infoReceived, this, &MessageHandler::handleDiscoInfo); #endif } MessageHandler::~MessageHandler() { #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) delete carbonManager; #endif } void MessageHandler::handleMessage(const QXmppMessage &msg) { - if (msg.body().isEmpty()) + if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error) return; 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 for (const QXmppElement &extension : msg.extensions()) { if (extension.tagName() == "spoiler" && extension.attribute("xmlns") == NS_SPOILERS) { message.setIsSpoiler(true); message.setSpoilerHint(extension.value()); break; } } // check if message contains a link and also check out of band url QStringList bodyWords = message.body().split(" "); #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) bodyWords.prepend(msg.outOfBandUrl()); #endif for (const QString &word : bodyWords) { if (!word.startsWith("https://") && !word.startsWith("http://")) 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); const QList mediaTypes = QMimeDatabase().mimeTypesForFileName(url.fileName()); for (const QMimeType &type : mediaTypes) { MessageType mType = Message::mediaTypeFromMimeType(type); if (mType == MessageType::MessageImage || mType == MessageType::MessageAudio || mType == MessageType::MessageVideo || mType == MessageType::MessageDocument || mType == MessageType::MessageFile) { message.setMediaType(mType); message.setMediaContentType(type.name()); message.setOutOfBandUrl(url.toEncoded()); break; } } break; // we can only handle one link } // get possible delay (timestamp) message.setStamp((msg.stamp().isNull() || !msg.stamp().isValid()) ? QDateTime::currentDateTimeUtc() : msg.stamp().toUTC()); // save the message to the database #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) // in case of message correction, replace old message if (msg.replaceId().isEmpty()) { emit model->addMessageRequested(message); } else { message.setIsEdited(true); message.setId(QString()); emit model->updateMessageRequested(msg.replaceId(), [=] (Message &m) { // replace completely m = message; }); } #else // no message correction with old QXmpp emit model->addMessageRequested(message); #endif // 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 = message.sentByMe() ? message.to() : message.from(); // resolve user-defined name of this JID QString contactName = client->rosterManager().getRosterEntry(contactJid).name(); if (contactName.isEmpty()) contactName = contactJid; if (!message.sentByMe()) Notifications::sendMessageNotification(contactName.toStdString(), msg.body().toStdString()); // TODO: Move back following call to RosterManager::handleMessage when spoiler // messages are implemented in QXmpp const QString lastMessage = message.isSpoiler() ? message.spoilerHint().isEmpty() ? tr("Spoiler") : message.spoilerHint() : msg.body(); emit kaidan->getRosterModel()->updateItemRequested( contactJid, [=] (RosterItem &item) { item.setLastMessage(lastMessage); } ); } void MessageHandler::sendMessage(const QString& toJid, const QString& body, bool isSpoiler, const QString& spoilerHint) { // 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; } 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()); if (isSpoiler) { msg.setIsSpoiler(isSpoiler); msg.setSpoilerHint(spoilerHint); // parsing/serialization of spoilers isn't implemented in QXmpp QXmppElementList extensions = msg.extensions(); QXmppElement spoiler = QXmppElement(); spoiler.setTagName("spoiler"); spoiler.setValue(msg.spoilerHint()); spoiler.setAttribute("xmlns", NS_SPOILERS); extensions.append(spoiler); msg.setExtensions(extensions); } emit model->addMessageRequested(msg); if (client->sendPacket(static_cast(msg))) emit model->setMessageAsSentRequested(msg.id()); else emit kaidan->passiveNotificationRequested(tr("Message could not be sent.")); // TODO: handle error } void MessageHandler::correctMessage(const QString& toJid, const QString& msgId, const 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; } 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); #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) msg.setReplaceId(msgId); #endif 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.")); } void MessageHandler::handleDiscoInfo(const QXmppDiscoveryIq &info) { #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) if (info.from() != client->configuration().domain()) return; // enable carbons, if feature found if (info.features().contains(NS_CARBONS)) carbonManager->setCarbonsEnabled(true); #endif } diff --git a/src/RosterManager.cpp b/src/RosterManager.cpp index a54fa3c..52ec95f 100644 --- a/src/RosterManager.cpp +++ b/src/RosterManager.cpp @@ -1,192 +1,192 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * 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. * * 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. * * 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 * along with Kaidan. If not, see . */ #include "RosterManager.h" // Kaidan #include "ClientWorker.h" #include "Globals.h" #include "Kaidan.h" #include "VCardManager.h" // QXmpp #include #include #include RosterManager::RosterManager(Kaidan *kaidan, QXmppClient *client, RosterModel *model, AvatarFileStorage *avatarStorage, VCardManager *vCardManager, QObject *parent) : QObject(parent), kaidan(kaidan), client(client), model(model), avatarStorage(avatarStorage), vCardManager(vCardManager), manager(client->rosterManager()) { connect(&manager, &QXmppRosterManager::rosterReceived, this, &RosterManager::populateRoster); connect(&manager, &QXmppRosterManager::itemAdded, [this, vCardManager, model] (const QString &jid) { emit model->addItemRequested(RosterItem(manager.getRosterEntry(jid))); vCardManager->fetchVCard(jid); }); connect(&manager, &QXmppRosterManager::itemChanged, this, [this, model] (QString jid) { emit model->updateItemRequested(m_chatPartner, [this, &jid] (RosterItem &item) { item.setName(manager.getRosterEntry(jid).name()); }); }); connect(&manager, &QXmppRosterManager::itemRemoved, model, &RosterModel::removeItemRequested); connect(&manager, &QXmppRosterManager::subscriptionReceived, this, [kaidan] (const QString &jid) { // emit signal to ask user emit kaidan->subscriptionRequestReceived(jid, QString()); }); connect(kaidan, &Kaidan::subscriptionRequestAnswered, this, [=] (QString jid, bool accepted) { if (accepted) manager.acceptSubscription(jid); else manager.refuseSubscription(jid); }); // user actions connect(kaidan, &Kaidan::addContact, this, &RosterManager::addContact); connect(kaidan, &Kaidan::removeContact, this, &RosterManager::removeContact); connect(kaidan, &Kaidan::sendMessage, this, &RosterManager::handleSendMessage); connect(client, &QXmppClient::messageReceived, this, &RosterManager::handleMessage); } void RosterManager::populateRoster() { qDebug() << "[client] [RosterManager] Populating roster"; // create a new list of contacts QHash items; for (const auto &jid : manager.getRosterBareJids()) { items[jid] = RosterItem(manager.getRosterEntry(jid)); if (avatarStorage->getHashOfJid(jid).isEmpty()) vCardManager->fetchVCard(jid); } if (!items.isEmpty()) { // replace current contacts with new ones from server emit model->replaceItemsRequested(items); } } void RosterManager::addContact(const QString &jid, const QString &name, const QString &msg) { if (client->state() == QXmppClient::ConnectedState) { manager.addItem(jid, name); manager.subscribe(jid, msg); } else { emit kaidan->passiveNotificationRequested( tr("Could not add contact, as a result of not being connected.") ); qWarning() << "[client] [RosterManager] Could not add contact, as a result of " "not being connected."; } } void RosterManager::removeContact(const QString &jid) { if (client->state() == QXmppClient::ConnectedState) { manager.unsubscribe(jid); manager.removeItem(jid); } else { emit kaidan->passiveNotificationRequested( tr("Could not remove contact, as a result of not being connected.") ); qWarning() << "[client] [RosterManager] Could not remove contact, as a result of " "not being connected."; } } void RosterManager::handleSendMessage(const QString &jid, const QString &message, bool isSpoiler, const QString &spoilerHint) { if (client->state() == QXmppClient::ConnectedState) { // update roster item const QString lastMessage = isSpoiler ? spoilerHint.isEmpty() ? tr("Spoiler") : spoilerHint : message; // sorting order in contact list const QDateTime dateTime = QDateTime::currentDateTimeUtc(); emit model->updateItemRequested(jid, [=] (RosterItem &item) { item.setLastMessage(lastMessage); item.setLastExchanged(dateTime); }); } } void RosterManager::handleMessage(const QXmppMessage &msg) { - if (msg.body().isEmpty()) + if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error) return; // msg.from() can be our JID, if it's a carbon/forward from another client QString fromJid = QXmppUtils::jidToBareJid(msg.from()); bool sentByMe = fromJid == client->configuration().jidBare(); QString contactJid = sentByMe ? QXmppUtils::jidToBareJid(msg.to()) : fromJid; // update last exchanged datetime (sorting order in contact list) const QDateTime dateTime = QDateTime::currentDateTimeUtc(); // update unread message counter, if chat is not active if (sentByMe) { // if we sent a message (with another device), reset counter emit model->updateItemRequested(contactJid, [dateTime] (RosterItem &item) { item.setLastExchanged(dateTime); item.setUnreadMessages(0); }); } else if (m_chatPartner != contactJid) { emit model->updateItemRequested(contactJid, [dateTime] (RosterItem &item) { item.setLastExchanged(dateTime); item.setUnreadMessages(item.unreadMessages() + 1); }); } }