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

Rewrite UploadManager to QXmpp

parent 93976a16
......@@ -9,7 +9,7 @@ set(KAIDAN_SOURCES
${CURDIR}/Database.cpp
${CURDIR}/RosterModel.cpp
${CURDIR}/RosterManager.cpp
${CURDIR}/MessageHandler.cpp
${CURDIR}/MessageHandler.cpp
${CURDIR}/MessageModel.cpp
${CURDIR}/Notifications.cpp
${CURDIR}/PresenceCache.cpp
......@@ -17,7 +17,7 @@ set(KAIDAN_SOURCES
${CURDIR}/VCardManager.cpp
${CURDIR}/LogHandler.cpp
${CURDIR}/StatusBar.cpp
# ${CURDIR}/UploadHandler.cpp
${CURDIR}/UploadManager.cpp
# ${CURDIR}/QtHttpUploader.cpp
# SingleApplication
......@@ -43,4 +43,6 @@ set(KAIDAN_SOURCES
# kaidan QXmpp extensions (need to be merged into QXmpp upstream)
${CURDIR}/qxmpp-exts/QXmppHttpUploadIq.cpp
${CURDIR}/qxmpp-exts/QXmppUploadRequestManager.cpp
${CURDIR}/qxmpp-exts/QXmppUploadManager.cpp
)
......@@ -46,6 +46,7 @@
#include "MessageHandler.h"
#include "DiscoveryManager.h"
#include "VCardManager.h"
#include "UploadManager.h"
ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QGuiApplication *app,
QObject* parent)
......@@ -59,6 +60,7 @@ ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, Q
caches->avatarStorage, vCardManager, this);
msgHandler = new MessageHandler(kaidan, client, caches->msgModel, this);
discoManager = new DiscoveryManager(client, this);
uploadManager = new UploadManager(kaidan, client, caches->msgModel, rosterManager, this);
connect(client, &QXmppClient::presenceReceived,
caches->presCache, &PresenceCache::updatePresenceRequested);
......@@ -79,6 +81,7 @@ ClientWorker::~ClientWorker()
delete msgHandler;
delete discoManager;
delete vCardManager;
delete uploadManager;
}
void ClientWorker::main()
......
......@@ -53,6 +53,7 @@ class RosterManager;
class MessageHandler;
class DiscoveryManager;
class VCardManager;
class UploadManager;
class ClientThread : public QThread
{
......@@ -78,6 +79,7 @@ protected:
class ClientWorker : public QObject
{
Q_OBJECT
Q_PROPERTY(UploadManager* uploadManager READ getUploadManager)
public:
struct Caches {
......@@ -126,6 +128,11 @@ public:
~ClientWorker();
UploadManager* getUploadManager()
{
return uploadManager;
}
public slots:
/**
* Main function of the client thread
......@@ -187,6 +194,7 @@ private:
MessageHandler *msgHandler;
DiscoveryManager *discoManager;
VCardManager *vCardManager;
UploadManager *uploadManager;
};
#endif // CLIENTWORKER_H
......@@ -76,7 +76,12 @@ void DiscoveryManager::handleInfo(const QXmppDiscoveryIq&)
// TODO: enable carbons and discovery http file upload
}
void DiscoveryManager::handleItems(const QXmppDiscoveryIq&)
void DiscoveryManager::handleItems(const QXmppDiscoveryIq &iq)
{
// TODO: request disco info for every item to discover http file upload and other services
// request info from all items
for (const QXmppDiscoveryIq::Item &item : iq.items()) {
if (item.jid() == client->configuration().domain())
continue;
manager->requestInfo(item.jid());
}
}
......@@ -152,12 +152,6 @@ void Kaidan::setConnectionState(QXmppClient::State state)
openUriCache = "";
});
}
// on disconnection, disable file upload
if (connectionState == ConnectionState::StateDisconnected) {
hasHttpUpload = false;
emit httpUploadChanged();
}
}
void Kaidan::setDisconnReason(Enums::DisconnectionReason reason)
......@@ -205,20 +199,6 @@ quint8 Kaidan::getDisconnReason() const
return (quint8) disconnReason;
}
void Kaidan::sendFile(QString jid, QString filePath, QString message)
{
if (connectionState == ConnectionState::StateConnected) {
// convert file-URLs to file paths
filePath.replace("file://", "");
filePath.replace("qrc:", "");
emit client->sendFileRequested(jid, filePath, message);
} else {
emit passiveNotificationRequested(tr("Could not send file, as a result of not being "
"connected."));
qWarning() << "[main] Could not send file, as a result of not being connected.";
}
}
QString Kaidan::getResourcePath(QString name) const
{
// We generally prefer to first search for files in application resources
......
......@@ -68,17 +68,11 @@ class Kaidan : public QObject
Q_PROPERTY(QString jidResource READ getJidResource WRITE setJidResource NOTIFY jidResourceChanged)
Q_PROPERTY(QString password READ getPassword WRITE setPassword NOTIFY passwordChanged)
Q_PROPERTY(QString chatPartner READ getChatPartner WRITE setChatPartner NOTIFY chatPartnerChanged)
Q_PROPERTY(bool httpUploadEnabled READ getHttpUploadEnabled NOTIFY httpUploadChanged)
Q_PROPERTY(bool uploadServiceFound READ getUploadServiceFound NOTIFY uploadServiceFoundChanged)
public:
/**
* Constructor
*/
Kaidan(QGuiApplication *app, bool enableLogging = true, QObject *parent = 0);
/**
* Destructor
*/
~Kaidan();
/**
......@@ -105,11 +99,6 @@ public:
*/
Q_INVOKABLE void mainDisconnect(bool openLogInPage = false);
/**
* Upload and send file
*/
Q_INVOKABLE void sendFile(QString jid, QString filePath, QString message);
/**
* Returns a URL to a given resource file name
*
......@@ -195,7 +184,7 @@ public:
/**
* Set the currently opened chat
*
* This will set a filter on the database to only view the wanted messages.
* This will set a filter on the database to only view the related messages.
*/
void setChatPartner(QString jid);
......@@ -207,33 +196,21 @@ public:
return chatPartner;
}
/**
* Get the roster model
*/
RosterModel* getRosterModel() const
{
return caches->rosterModel;
}
/**
* Get the message model
*/
MessageModel* getMessageModel() const
{
return caches->msgModel;
}
/**
* Get the avatar storage
*/
AvatarFileStorage* getAvatarStorage() const
{
return caches->avatarStorage;
}
/**
* Get the presence cache
*/
PresenceCache* getPresenceCache() const
{
return caches->presCache;
......@@ -250,11 +227,11 @@ public:
Q_INVOKABLE void copyToClipboard(QString text);
/**
* Returns true, if XEP-0363: HTTP File Upload was discovered
* Returns whether an HTTP File Upload service has been found
*/
bool getHttpUploadEnabled()
bool getUploadServiceFound() const
{
return hasHttpUpload;
return uploadServiceFound;
}
signals:
......@@ -345,9 +322,9 @@ signals:
void uploadProgressMade(QString msgId, unsigned long sent, unsigned long total);
/**
* XEP-0363: HTTP File Upload was discovered (or isn't working anymore)
* An HTTP File Upload service was discovered
*/
void httpUploadChanged();
void uploadServiceFoundChanged();
/**
* Send a text message to any JID
......@@ -359,6 +336,11 @@ signals:
*/
void sendMessage(QString jid, QString message);
/**
* Upload and send file
*/
void sendFile(QString jid, QString filePath, QString message);
/**
* Add a contact to your roster
*
......@@ -395,12 +377,12 @@ public slots:
}
/**
* Enables XEP-0363: HTTP File Upload
* Enables HTTP File Upload to be used (will be called from UploadManager)
*/
void enableHttpUpload()
void setUploadServiceFound(bool enabled)
{
hasHttpUpload = true;
emit httpUploadChanged();
uploadServiceFound = enabled;
emit uploadServiceFoundChanged();
}
private:
......@@ -415,7 +397,7 @@ private:
QString openUriCache;
bool hasHttpUpload = false;
bool uploadServiceFound = false;
ConnectionState connectionState = ConnectionState::StateDisconnected;
DisconnReason disconnReason = DisconnReason::ConnNoError;
};
......
......@@ -105,7 +105,7 @@ void MessageHandler::sendMessage(QString toJid, QString body)
qWarning() << "[client] [MessageHandler] Could not send message, as a result of "
"not being connected.";
return;
}
}
MessageModel::Message msg;
msg.author = client->configuration().jidBare();
......
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 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
* 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 <http://www.gnu.org/licenses/>.
*/
#include "UploadHandler.h"
#include "QtHttpUploader.h"
#include "MessageHandler.h"
// gloox
#include <gloox/message.h>
#include <gloox/base64.h>
#include "gloox-extensions/httpuploadmanager.h"
#include "gloox-extensions/reference.h"
#include "gloox-extensions/sims.h"
#include "gloox-extensions/processinghints.h"
#include "gloox-extensions/jinglefile.h"
#include "gloox-extensions/hash.h"
#include "gloox-extensions/thumb.h"
#include "gloox-extensions/bitsofbinarydata.h"
// Qt
#include <QMimeDatabase>
#include <QMimeType>
#include <QDateTime>
#include <QBuffer>
#include <QImage>
#include <QDebug>
#include <QFileInfo>
UploadHandler::UploadHandler(gloox::Client *client, MessageHandler *msgHandler,
MessageModel *msgModel, QObject *parent)
: QObject(parent), client(client), msgHandler(msgHandler), msgModel(msgModel)
{
manager = new gloox::HttpUploadManager(client);
uploader = new QtHttpUploader(manager);
manager->registerHttpUploadHandler(this);
}
void UploadHandler::uploadFile(QString jid, QString filePath, QString message)
{
// get MIME-type
QMimeDatabase mimeDb;
QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
QString mimeTypeStr = mimeType.name();
int id = manager->uploadFile(filePath.toStdString(), true,
mimeTypeStr.toStdString());
if (id < 0) {
// failure
// TODO: send passive notification
} else {
// save for later processing/sending
MediaSharingMeta meta;
meta.jid = jid;
meta.msgId = client->getID();
meta.message = message;
meta.filePath = filePath;
meta.type = msgHandler->getMessageType(mimeType);
mediaShares[id] = meta;
msgHandler->addMessageToDb(
jid, message, QString::fromStdString(meta.msgId),
MessageType::MessageFile, filePath
);
}
}
void UploadHandler::handleUploadFailed(int id, gloox::HttpUploadError error,
const std::string &text,
const std::string &stamp)
{
qDebug() << "[client] A file upload has failed." << error;
// the media meta isn't needed anymore, so delete it
mediaShares.remove(id);
}
void UploadHandler::handleUploadFinished(int id, std::string &name,
std::string &getUrl,
std::string &contentType,
unsigned long &size)
{
qDebug() << "[client] A file upload has finished.";
// prepare new database message
MessageModel::Message msg;
msg.mediaUrl = QString::fromStdString(getUrl);
msg.mediaContentType = QString::fromStdString(contentType);
msg.mediaSize = size;
// Hashes
QtHttpUploader::HashResult hashResults = uploader->getHashResults(id);
std::list<gloox::Hash> hashes;
hashes.emplace_back(gloox::Hash("sha-256", hashResults.sha256.toBase64().toStdString()));
hashes.emplace_back(gloox::Hash("sha3-256", hashResults.sha3_256.toBase64().toStdString()));
// save hashes in database field
for (gloox::Hash &hash : hashes)
msg.mediaHashes.append(QString::fromStdString(hash.tag()->xml()));
// last modified date
QFileInfo fInfo(mediaShares[id].filePath);
QDateTime modifyTime = fInfo.lastModified();
msg.mediaLastModified = modifyTime.isValid() ? modifyTime.toMSecsSinceEpoch() : -1;
std::string modifyTimeStr = modifyTime.isValid() ? modifyTime.toUTC()
.toString(Qt::ISODate).toStdString() : "";
// thumbnail
QSize thumbSize = generateMediaThumb(mediaShares[id].filePath, mediaShares[id].type,
&msg.mediaThumb);
gloox::Jingle::Thumb *thumb = nullptr;
gloox::BitsOfBinaryData *thumbBob = nullptr;
if (!msg.mediaThumb.isEmpty()) {
// encode to base64
std::string thumbData = gloox::Base64::encode64(
std::string(msg.mediaThumb.constData(), msg.mediaThumb.length())
);
// create BoB content identifier
std::string thumbCid = gloox::BitsOfBinaryData::generateContentId(thumbData);
// create BoB data object
thumbBob = new gloox::BitsOfBinaryData(thumbData, thumbCid, "image/jpeg");
// create thumb object (links to BoB data)
thumb = new gloox::Jingle::Thumb(
"cid:" + thumbCid, thumbSize.width(), thumbSize.height(), "image/jpeg"
);
}
// file meta information
gloox::Jingle::File *fileInfo = new gloox::Jingle::File(
name, size, hashes, contentType, modifyTimeStr,
mediaShares[id].message.toStdString(), thumb
);
// list of sources for the file (TODO: jingle as fallback)
gloox::StringList sources;
sources.emplace_back(getUrl);
// create message
gloox::SIMS *sims = new gloox::SIMS(fileInfo, sources);
gloox::Reference *simsRef = new gloox::Reference(gloox::Reference::Data);
simsRef->embedSIMS(sims);
gloox::JID to(mediaShares[id].jid.toStdString());
// message body for clients without SIMS support
std::string msgBody = getUrl;
if (!mediaShares[id].message.isEmpty())
msgBody = msgBody.append("\n").append(mediaShares[id].message.toStdString());
gloox::Message message(gloox::Message::Chat, to.bareJID(), msgBody);
message.setID(mediaShares[id].msgId);
message.addExtension((gloox::StanzaExtension*) simsRef);
if (thumbBob)
message.addExtension(thumbBob); // thumbnail BoB data
client->send(message);
// save to database
emit msgModel->updateMessageRequested(
QString::fromStdString(mediaShares[id].msgId), msg
);
// the media meta isn't needed anymore, so delete it
mediaShares.remove(id);
}
void UploadHandler::handleUploadProcess(int id, long unsigned int sent,
long unsigned int total)
{
}
void UploadHandler::handleUploadServiceRemoved(const gloox::JID &jid)
{
}
void UploadHandler::handleUploadServiceAdded(const gloox::JID &jid,
unsigned long maxFileSize)
{
emit uploadServiceFound();
}
void UploadHandler::handleFileSizeLimitChanged(unsigned long maxFileSize)
{
}
QSize UploadHandler::generateMediaThumb(QString &filePath, MessageType type,
QByteArray *bytes)
{
if (type != MessageType::MessageImage)
return QSize(0, 0);
// results should be about 1-3 kB large
int finalSize = 40;
QImage img(filePath);
// Qt::FastTransformation is used because quality doesn't matter at this
// size; you won't be able to really see a large difference
img = img.scaled(finalSize, finalSize, Qt::KeepAspectRatio, Qt::FastTransformation);
// save image to byte array
QBuffer buffer(bytes);
buffer.open(QIODevice::WriteOnly);
img.save(&buffer, "JPEG", 90);
return img.size();
}
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* 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
* 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 <http://www.gnu.org/licenses/>.
*/
#include "UploadManager.h"
#include "Kaidan.h"
#include "MessageHandler.h"
#include "RosterManager.h"
// QXmpp
#include <QXmppUtils.h>
// Qt
#include <QMimeDatabase>
#include <QMimeType>
#include <QDateTime>
#include <QBuffer>
#include <QImage>
#include <QDebug>
#include <QFileInfo>
UploadManager::UploadManager(Kaidan *kaidan, QXmppClient *client, MessageModel *msgModel,
RosterManager *rosterManager, QObject *parent)
: QObject(parent), kaidan(kaidan), client(client), msgModel(msgModel),
rosterManager(rosterManager)
{
client->addExtension(&manager);
connect(kaidan, &Kaidan::sendFile, this, &UploadManager::sendFile);
connect(&manager, &QXmppUploadManager::serviceFoundChanged, [this]() {
// needed because kaidan is in main thread
QMetaObject::invokeMethod(this->kaidan, "setUploadServiceFound", Qt::QueuedConnection,
Q_ARG(bool, manager.serviceFound()));
});
connect(&manager, &QXmppUploadManager::uploadSucceeded,
this, &UploadManager::handleUploadSucceeded);
connect(&manager, &QXmppUploadManager::uploadFailed,
this, &UploadManager::handleUploadFailed);
}
void UploadManager::sendFile(QString jid, QString fileUrl, QString body)
{
// TODO: Add offline media message cache and send when connnected again
if (client->state() != QXmppClient::ConnectedState) {
emit kaidan->passiveNotificationRequested(
tr("Could not send file, as a result of not being connected.")
);
qWarning() << "[client] [UploadManager] Could not send file, as a result of "
"not being connected.";
return;
}
qDebug() << "[client] [UploadManager] Adding upload for file:" << fileUrl;
QFileInfo file(QUrl(fileUrl).toLocalFile());
int id = manager.uploadFile(file);
MessageModel::Message *msg = new MessageModel::Message();
msg->author = client->configuration().jidBare();
msg->recipient = jid;
msg->id = QXmppUtils::generateStanzaHash(48);
msg->sentByMe = true;
msg->message = body;
msg->type = MessageType::MessageFile;
msg->timestamp = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate);
msg->mediaSize = file.size();
msg->mediaContentType = QMimeDatabase().mimeTypeForFile(file).name();
msg->mediaLastModified = file.lastModified().currentMSecsSinceEpoch();
msg->mediaLocation = file.absolutePath();
emit msgModel->addMessageRequested(*msg);
// message cache to edit on success/failure
messages.insert(id, msg);
// update last message
QString lastMessage = tr("File");
if (!body.isEmpty())
lastMessage = lastMessage.append(": ").append(body);
rosterManager->handleSendMessage(jid, lastMessage);
}
void UploadManager::handleUploadSucceeded(const QXmppHttpUpload *upload)
{
qDebug() << "[client] [UploadManager] A file upload has succeeded. Now sending message.";
MessageModel::Message *originalMsg = messages.value(upload->id());
MessageModel::Message msgUpdate;
msgUpdate.mediaUrl = upload->slot().getUrl().toEncoded();
msgUpdate.message = upload->slot().getUrl().toDisplayString();
if (!originalMsg->message.isEmpty())
msgUpdate.message = msgUpdate.message.prepend(originalMsg->message + "\n");