Verified Commit f5f04736 authored by Linus Jahn's avatar Linus Jahn

Rewrite MessageHandler to QXmpp

parent a4c6def7
......@@ -141,3 +141,5 @@ install_manifest.txt
*.swp
.vscode/
.directory
src/tags
......@@ -4,14 +4,12 @@ set(CURDIR ${CMAKE_CURRENT_LIST_DIR})
set(KAIDAN_SOURCES
${CURDIR}/main.cpp
${CURDIR}/Kaidan.cpp
# ${CURDIR}/ClientThread.cpp
${CURDIR}/ClientWorker.cpp
${CURDIR}/AvatarFileStorage.cpp
${CURDIR}/Database.cpp
${CURDIR}/RosterModel.cpp
${CURDIR}/RosterManager.cpp
# ${CURDIR}/MessageHandler.cpp
# ${CURDIR}/MessageSessionHandler.cpp
${CURDIR}/MessageHandler.cpp
${CURDIR}/MessageModel.cpp
${CURDIR}/Notifications.cpp
${CURDIR}/PresenceCache.cpp
......
......@@ -40,22 +40,24 @@
// Kaidan
#include "Kaidan.h"
#include "RosterManager.h"
#include "MessageHandler.h"
ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QGuiApplication *app,
QObject* parent)
: QObject(parent), caches(caches), kaidan(kaidan), enableLogging(enableLogging), app(app)
{
client = new QXmppClient();
client->moveToThread(thread);
rosterManager = new RosterManager(kaidan, client, caches->rosterModel);
rosterManager->moveToThread(thread);
client = new QXmppClient(this);
rosterManager = new RosterManager(kaidan, client, caches->rosterModel, this);
msgHandler = new MessageHandler(kaidan, client, caches->msgModel, this);
connect(this, &ClientWorker::credentialsUpdated, this, &ClientWorker::setCredentials);
}
ClientWorker::~ClientWorker()
{
delete client;
delete rosterManager;
delete msgHandler;
}
void ClientWorker::main()
......
......@@ -49,6 +49,7 @@ class QGuiApplication;
class Kaidan;
class ClientWorker;
class RosterManager;
class MessageHandler;
class ClientThread : public QThread
{
......@@ -77,11 +78,11 @@ class ClientWorker : public QObject
public:
struct Caches {
Caches(Database *database)
: msgModel(new MessageModel(database->getDatabase())),
rosterModel(new RosterModel(database->getDatabase())),
avatarStorage(new AvatarFileStorage()),
presCache(new PresenceCache()),
Caches(Database *database, QObject *parent = nullptr)
: msgModel(new MessageModel(database->getDatabase(), parent)),
rosterModel(new RosterModel(database->getDatabase(), parent)),
avatarStorage(new AvatarFileStorage(parent)),
presCache(new PresenceCache(parent)),
settings(new QSettings(APPLICATION_NAME, APPLICATION_NAME))
{
}
......@@ -179,6 +180,7 @@ private:
QGuiApplication *app;
RosterManager *rosterManager;
MessageHandler *msgHandler;
};
#endif // CLIENTWORKER_H
......@@ -57,7 +57,7 @@ Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent) : QObj
database->convertDatabase();
// Caching components
caches = new ClientWorker::Caches(database);
caches = new ClientWorker::Caches(database, this);
// Connect the avatar changed signal of the avatarStorage with the NOTIFY signal
// of the Q_PROPERTY for the avatar storage (so all avatars are updated in QML)
......@@ -97,6 +97,7 @@ Kaidan::~Kaidan()
delete client;
delete caches;
delete database;
delete cltThrd;
}
void Kaidan::start()
......@@ -201,16 +202,6 @@ quint8 Kaidan::getDisconnReason() const
return (quint8) disconnReason;
}
void Kaidan::sendMessage(QString jid, QString message)
{
if (connectionState == ConnectionState::StateConnected) {
emit client->sendMessageRequested(jid, message);
} else {
emit passiveNotificationRequested(tr("Could not send message, as a result of not being connected."));
qWarning() << "[main] Could not send message, as a result of not being connected.";
}
}
void Kaidan::sendFile(QString jid, QString filePath, QString message)
{
if (connectionState == ConnectionState::StateConnected) {
......
......@@ -105,16 +105,6 @@ public:
*/
Q_INVOKABLE void mainDisconnect(bool openLogInPage = false);
/**
* Send a text message to any JID
*
* Currently only contacts are displayed on the RosterPage (there is no
* way to view a list of all chats -> for contacts and non-contacts), so
* you should only send messages to JIDs from your roster, otherwise you
* won't be able to see the message history.
*/
Q_INVOKABLE void sendMessage(QString jid, QString message);
/**
* Upload and send file
*/
......@@ -359,6 +349,16 @@ signals:
*/
void httpUploadChanged();
/**
* Send a text message to any JID
*
* Currently only contacts are displayed on the RosterPage (there is no
* way to view a list of all chats -> for contacts and non-contacts), so
* you should only send messages to JIDs from your roster, otherwise you
* won't be able to see the message history.
*/
void sendMessage(QString jid, QString message);
/**
* Add a contact to your roster
*
......
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2017-2018 Kaidan developers and contributors
* Copyright (C) 2016-2018 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
......@@ -29,318 +29,100 @@
*/
#include "MessageHandler.h"
// Std
#include <iostream>
// Qt
#include <QDateTime>
#include <QDebug>
#include <QString>
#include <QMimeDatabase>
#include <QMimeType>
// gloox
#include <gloox/client.h>
#include <gloox/message.h>
#include <gloox/messagesession.h>
#include <gloox/receipt.h>
#include <gloox/carbons.h>
#include "gloox-extensions/gloox-extensions.h"
#include "gloox-extensions/reference.h"
#include "gloox-extensions/sims.h"
#include "gloox-extensions/jinglefile.h"
#include "gloox-extensions/bitsofbinarydata.h"
// QXmpp
#include <QXmppClient.h>
#include <QXmppUtils.h>
#include <QXmppRosterManager.h>
// Kaidan
#include "Kaidan.h"
#include "MessageModel.h"
#include "Notifications.h"
QDateTime stringToQDateTime(std::string stamp)
{
QString qStamp = QString::fromStdString(stamp);
QDateTime dateTime;
if (qStamp.contains('.'))
dateTime = QDateTime::fromString(qStamp, Qt::ISODateWithMs);
else
dateTime = QDateTime::fromString(qStamp, Qt::ISODate);
if (!dateTime.isValid())
return QDateTime::currentDateTime().toUTC();
// XMPP timestamps are always in UTC
// also read it as such if 'Z' is missing in ISO timestamp
dateTime.setTimeSpec(Qt::UTC);
return dateTime;
}
MessageHandler::MessageHandler(gloox::Client *client, MessageModel *messageModel,
RosterModel *rosterModel, QObject *parent):
QObject(parent), client(client),
messageModel(messageModel), rosterModel(rosterModel)
{
}
MessageHandler::~MessageHandler()
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);
client->addExtension(&receiptManager);
connect(&receiptManager, &QXmppMessageReceiptManager::messageDelivered,
[=] (const QString&, const QString &id) {
emit model->setMessageAsDeliveredRequested(id);
});
}
void MessageHandler::setChatPartner(QString chatPartner)
{
this->chatPartner = chatPartner;
resetUnreadMessagesForJid(this->chatPartner);
}
void MessageHandler::handleMessage(const gloox::Message &stanza, gloox::MessageSession *session)
void MessageHandler::handleMessage(const QXmppMessage &msg)
{
// TODO: Enable carbons (currently needs QXmpp master)
bool isCarbonMessage = false;
// 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
if (carbon && carbon->embeddedStanza()) {
isCarbonMessage = true;
message = static_cast<gloox::Message*>(carbon->embeddedStanza());
}
}
QString body = QString::fromStdString(message->body());
if (!body.isEmpty()) {
//
// Extract information of the message
//
// author is only the 'bare' JID: e.g. 'albert@einstein.ch'
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()));
msg.message = body;
msg.type = MessageType::MessageText; // only text, no media
// handle media sharing (SIMS) content
handleMediaSharing(const_cast<gloox::Message*>(message), &msg);
//
// If it is a delayed delivery (containing a timestamp), use its timestamp
//
const gloox::DelayedDelivery *delayedDelivery = message->when();
if (delayedDelivery)
msg.timestamp = stringToQDateTime(delayedDelivery->stamp())
.toString(Qt::ISODate);
// fallback: use current time from local clock
if (msg.timestamp.isEmpty())
msg.timestamp = QDateTime::currentDateTime().toUTC()
.toString(Qt::ISODate);
// add the message to the database
emit messageModel->addMessageRequested(msg);
//
// Send a new notification | TODO: Resolve nickname from JID
//
if (!msg.sentByMe && !isCarbonMessage)
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
const QString contactJid = msg.sentByMe ? msg.recipient : msg.author;
// update the last message for this contact
emit rosterModel->setLastMessageRequested(contactJid, body);
// update the last exchanged for this contact
updateLastExchangedOfJid(contactJid);
// Increase unread message counter
// don't add new unread message if chat is opened or we wrote the message
if (!isCarbonMessage && chatPartner != contactJid && !msg.sentByMe)
newUnreadMessageForJid(contactJid);
}
// XEP-0184: Message Delivery Receipts
// try to handle a possible delivery receipt
handleReceiptMessage(message, isCarbonMessage);
}
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()).toMSecsSinceEpoch();
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())
);
}
}
}
}
}
}
void MessageHandler::handleReceiptMessage(const gloox::Message *message,
bool isCarbonMessage)
{
// check if message contains receipt
gloox::Receipt *receipt = (gloox::Receipt*) message->findExtension<gloox::Receipt>(gloox::ExtReceipt);
if (!receipt)
if (msg.body().isEmpty())
return;
// 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
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();
entry.type = MessageType::MessageText; // text message without media
// 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());
// get possible delay (timestamp)
QDateTime stamp = msg.stamp();
if (!stamp.isValid() || stamp.isNull())
stamp = QDateTime::currentDateTime();
entry.timestamp = stamp.toUTC().toString(Qt::ISODate);
// add the receipt extension containing the request's message id
gloox::Receipt *receiptPayload = new gloox::Receipt(gloox::Receipt::Received, message->id());
receiptMessage.addExtension(receiptPayload);
// save the message to the database
emit model->addMessageRequested(entry);
// 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()));
}
// 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());
}
void MessageHandler::sendMessage(QString toJid, QString body)
{
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)
{
// create a new message
gloox::Message message(gloox::Message::Chat, gloox::JID(toJid.toStdString()),
body.toStdString());
message.setID(id);
// 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);
}
// 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;
}
void MessageHandler::addMessageToDb(QString &toJid, QString &body, QString id,
MessageType type, QString mediaLocation)
{
// add the message to the database
MessageModel::Message msg;
msg.timestamp = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate);
msg.author = QString::fromStdString(client->jid().bare());
msg.author = client->configuration().jidBare();
msg.recipient = toJid;
msg.message = body;
msg.id = id;
msg.id = QXmppUtils::generateStanzaHash(48);
msg.sentByMe = true;
msg.type = type;
msg.mediaLocation = mediaLocation;
emit messageModel->addMessageRequested(msg);
// update the last message for this contact
emit rosterModel->setLastMessageRequested(toJid, body);
// update the last exchanged date
updateLastExchangedOfJid(toJid);
}
msg.message = body;
msg.type = MessageType::MessageText; // text message without media
msg.timestamp = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate);
void MessageHandler::updateLastExchangedOfJid(const QString &jid)
{
QString dateTime = QDateTime::currentDateTime().toString(Qt::ISODate);
emit rosterModel->setLastExchangedRequested(jid, dateTime);
}
emit model->addMessageRequested(msg);
void MessageHandler::newUnreadMessageForJid(const QString &jid)
{
// add a new unread message to the contact
emit rosterModel->newUnreadMessageRequested(jid);
}
QXmppMessage m(msg.author, msg.recipient, body);
m.setId(msg.id);
m.setReceiptRequested(true);
void MessageHandler::resetUnreadMessagesForJid(const QString &jid)
{
// reset the unread message count to 0
emit rosterModel->setUnreadMessageCountRequested(jid, 0);
// TODO: check return code
client->sendPacket(m);
}
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;
}
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2017-2018 Kaidan developers and contributors
* Copyright (C) 2016-2018 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
......@@ -33,20 +33,14 @@
// Qt
#include <QObject>
#include <QSqlDatabase>
// gloox
#include <gloox/messagehandler.h>
// QXmpp
#include <QXmppMessage.h>
#include <QXmppMessageReceiptManager.h>
// Kaidan
#include "MessageModel.h"
#include "RosterModel.h"
#include "Enums.h"
namespace gloox {
class Client;
class Message;
class MessageSession;
}
class Kaidan;
class MessageModel;
class QMimeType;
using namespace Enums;
......@@ -54,67 +48,30 @@ using namespace Enums;
/**
* @class MessageHandler Handler for incoming and outgoing messages
*/
class MessageHandler : public QObject, public gloox::MessageHandler
class MessageHandler : public QObject
{
Q_OBJECT
public:
MessageHandler(gloox::Client *client, MessageModel *messageModel,
RosterModel *rosterModel, QObject *parent = nullptr);
~MessageHandler();
virtual void handleMessage(const gloox::Message &message, gloox::MessageSession *session = 0);
/**
* Handles and processes media sharing content of messages
*/
void handleMediaSharing(const gloox::Message *message,
MessageModel::Message *msg);
/**
* Handles a message with a possible receipt or receipt request
*/
void handleReceiptMessage(const gloox::Message *message,
bool isCarbonMessage);
void updateLastExchangedOfJid(const QString &jid);
void newUnreadMessageForJid(const QString &jid);
void resetUnreadMessagesForJid(const QString &jid);
/**
* (Only) sends the message to the recipient
*
* @param toJid Recipient (bare JID)