diff --git a/src/MessageDb.cpp b/src/MessageDb.cpp index a5f21e0..3a8c169 100644 --- a/src/MessageDb.cpp +++ b/src/MessageDb.cpp @@ -1,301 +1,316 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2020 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 "MessageDb.h" // Kaidan #include "Globals.h" #include "Message.h" #include "Utils.h" // Qt #include #include #include #include #include +MessageDb *MessageDb::s_instance = nullptr; + MessageDb::MessageDb(QObject *parent) : QObject(parent) { + Q_ASSERT(!MessageDb::s_instance); + s_instance = this; + connect(this, &MessageDb::fetchMessagesRequested, this, &MessageDb::fetchMessages); } +MessageDb::~MessageDb() +{ + s_instance = nullptr; +} + +MessageDb *MessageDb::instance() +{ + return s_instance; +} + void MessageDb::parseMessagesFromQuery(QSqlQuery &query, QVector &msgs) { // get indexes of attributes QSqlRecord rec = query.record(); int idxFrom = rec.indexOf("author"); int idxTo = rec.indexOf("recipient"); int idxStamp = rec.indexOf("timestamp"); int idxId = rec.indexOf("id"); int idxBody = rec.indexOf("message"); int idxIsSent = rec.indexOf("isSent"); int idxIsDelivered = rec.indexOf("isDelivered"); int idxMediaType = rec.indexOf("type"); int idxOutOfBandUrl = rec.indexOf("mediaUrl"); int idxMediaContentType = rec.indexOf("mediaContentType"); int idxMediaLocation = rec.indexOf("mediaLocation"); int idxMediaSize = rec.indexOf("mediaSize"); int idxMediaLastModified = rec.indexOf("mediaLastModified"); int idxIsEdited = rec.indexOf("edited"); int idxSpoilerHint = rec.indexOf("spoilerHint"); int idxIsSpoiler = rec.indexOf("isSpoiler"); while (query.next()) { Message msg; msg.setFrom(query.value(idxFrom).toString()); msg.setTo(query.value(idxTo).toString()); msg.setStamp(QDateTime::fromString( query.value(idxStamp).toString(), Qt::ISODate )); msg.setId(query.value(idxId).toString()); msg.setBody(query.value(idxBody).toString()); msg.setIsSent(query.value(idxIsSent).toBool()); msg.setIsDelivered(query.value(idxIsDelivered).toBool()); msg.setMediaType(static_cast(query.value(idxMediaType).toInt())); msg.setOutOfBandUrl(query.value(idxOutOfBandUrl).toString()); msg.setMediaContentType(query.value(idxMediaContentType).toString()); msg.setMediaLocation(query.value(idxMediaLocation).toString()); msg.setMediaSize(query.value(idxMediaSize).toLongLong()); msg.setMediaLastModified(QDateTime::fromMSecsSinceEpoch( query.value(idxMediaLastModified).toLongLong() )); msg.setIsEdited(query.value(idxIsEdited).toBool()); msg.setSpoilerHint(query.value(idxSpoilerHint).toString()); msg.setIsSpoiler(query.value(idxIsSpoiler).toBool()); msgs << msg; } } QSqlRecord MessageDb::createUpdateRecord(const Message &oldMsg, const Message &newMsg) { QSqlRecord rec; if (oldMsg.from() != newMsg.from()) rec.append(Utils::createSqlField("author", newMsg.from())); if (oldMsg.to() != newMsg.to()) rec.append(Utils::createSqlField("recipient", newMsg.to())); if (oldMsg.stamp() != newMsg.stamp()) rec.append(Utils::createSqlField( "timestamp", newMsg.stamp().toString(Qt::ISODate) )); if (oldMsg.id() != newMsg.id()) { // TODO: remove as soon as 'NOT NULL' was removed from id column if (newMsg.id().isEmpty()) rec.append(Utils::createSqlField("id", QStringLiteral(" "))); else rec.append(Utils::createSqlField("id", newMsg.id())); } if (oldMsg.body() != newMsg.body()) rec.append(Utils::createSqlField("message", newMsg.body())); if (oldMsg.isSent() != newMsg.isSent()) rec.append(Utils::createSqlField("isSent", newMsg.isSent())); if (oldMsg.isDelivered() != newMsg.isDelivered()) rec.append(Utils::createSqlField("isDelivered", newMsg.isDelivered())); if (oldMsg.mediaType() != newMsg.mediaType()) rec.append(Utils::createSqlField("type", int(newMsg.mediaType()))); if (oldMsg.outOfBandUrl() != newMsg.outOfBandUrl()) rec.append(Utils::createSqlField("mediaUrl", newMsg.outOfBandUrl())); if (oldMsg.mediaContentType() != newMsg.mediaContentType()) rec.append(Utils::createSqlField( "mediaContentType", newMsg.mediaContentType() )); if (oldMsg.mediaLocation() != newMsg.mediaLocation()) rec.append(Utils::createSqlField( "mediaLocation", newMsg.mediaLocation() )); if (oldMsg.mediaSize() != newMsg.mediaSize()) rec.append(Utils::createSqlField("mediaSize", newMsg.mediaSize())); if (oldMsg.mediaLastModified() != newMsg.mediaLastModified()) rec.append(Utils::createSqlField( "mediaLastModified", newMsg.mediaLastModified().toMSecsSinceEpoch() )); if (oldMsg.isEdited() != newMsg.isEdited()) rec.append(Utils::createSqlField("edited", newMsg.isEdited())); if (oldMsg.spoilerHint() != newMsg.spoilerHint()) rec.append(Utils::createSqlField("spoilerHint", newMsg.spoilerHint())); if (oldMsg.isSpoiler() != newMsg.isSpoiler()) rec.append(Utils::createSqlField("isSpoiler", newMsg.isSpoiler())); return rec; } void MessageDb::fetchMessages(const QString &user1, const QString &user2, int index) { QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); query.setForwardOnly(true); QMap bindValues; bindValues[":user1"] = user1; bindValues[":user2"] = user2; bindValues[":index"] = index; bindValues[":limit"] = DB_MSG_QUERY_LIMIT; Utils::execQuery( query, "SELECT * FROM Messages " "WHERE (author = :user1 AND recipient = :user2) OR " "(author = :user2 AND recipient = :user1) " "ORDER BY timestamp DESC " "LIMIT :index, :limit", bindValues ); QVector messages; parseMessagesFromQuery(query, messages); emit messagesFetched(messages); } void MessageDb::addMessage(const Message &msg) { QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlRecord record = db.record(DB_TABLE_MESSAGES); record.setValue("author", msg.from()); record.setValue("recipient", msg.to()); record.setValue("timestamp", msg.stamp().toString(Qt::ISODate)); record.setValue("message", msg.body()); record.setValue("id", msg.id().isEmpty() ? " " : msg.id()); record.setValue("isSent", msg.isSent()); record.setValue("isDelivered", msg.isDelivered()); record.setValue("type", int(msg.mediaType())); record.setValue("edited", msg.isEdited()); record.setValue("isSpoiler", msg.isSpoiler()); record.setValue("spoilerHint", msg.spoilerHint()); record.setValue("mediaUrl", msg.outOfBandUrl()); record.setValue("mediaContentType", msg.mediaContentType()); record.setValue("mediaLocation", msg.mediaLocation()); record.setValue("mediaSize", msg.mediaSize()); record.setValue("mediaLastModified", msg.mediaLastModified().toMSecsSinceEpoch()); QSqlQuery query(db); Utils::execQuery(query, db.driver()->sqlStatement( QSqlDriver::InsertStatement, DB_TABLE_MESSAGES, record, false )); } void MessageDb::removeMessage(const QString &id) { QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); Utils::execQuery( query, "DELETE FROM Messages WHERE id = ?", QVector() << id ); } void MessageDb::removeAllMessages() { QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); Utils::execQuery(query, "DELETE FROM Messages"); } void MessageDb::updateMessage(const QString &id, const std::function &updateMsg) { // load current message item from db QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); query.setForwardOnly(true); Utils::execQuery( query, "SELECT * FROM Messages WHERE id = ? LIMIT 1", QVector() << id ); QVector msgs; parseMessagesFromQuery(query, msgs); // update loaded item if (!msgs.isEmpty()) { Message msg = msgs.first(); updateMsg(msg); // replace old message with updated one, if message has changed if (msgs.first() != msg) { // create an SQL record with only the differences QSqlRecord rec = createUpdateRecord(msgs.first(), msg); Utils::execQuery( query, db.driver()->sqlStatement( QSqlDriver::UpdateStatement, DB_TABLE_MESSAGES, rec, false ) + Utils::simpleWhereStatement(db.driver(), "id", id) ); } } } void MessageDb::updateMessageRecord(const QString &id, const QSqlRecord &updateRecord) { QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); Utils::execQuery( query, db.driver()->sqlStatement( QSqlDriver::UpdateStatement, DB_TABLE_MESSAGES, updateRecord, false ) + Utils::simpleWhereStatement(db.driver(), "id", id) ); } void MessageDb::setMessageAsSent(const QString &msgId) { QSqlRecord rec; rec.append(Utils::createSqlField("isSent", true)); updateMessageRecord(msgId, rec); } void MessageDb::setMessageAsDelivered(const QString &msgId) { QSqlRecord rec; rec.append(Utils::createSqlField("isDelivered", true)); updateMessageRecord(msgId, rec); } diff --git a/src/MessageDb.h b/src/MessageDb.h index 38ae239..f14841e 100644 --- a/src/MessageDb.h +++ b/src/MessageDb.h @@ -1,138 +1,144 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2020 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 . */ #ifndef MESSAGEDB_H #define MESSAGEDB_H #include #include class Message; class QSqlQuery; class QSqlRecord; /** * @class The MessageDb is used to query the 'messages' database table. It's used by the * MessageModel to load messages and by the MessageHandler to insert messages. * * All queries must be executed only after the Kaidan SQL connection has been opened in * the Database class. */ class MessageDb : public QObject { Q_OBJECT public: explicit MessageDb(QObject *parent = nullptr); + ~MessageDb(); + + static MessageDb *instance(); /** * Parses a list of messages from a SELECT query. */ static void parseMessagesFromQuery(QSqlQuery &query, QVector &msgs); /** * Creates an @c QSqlRecord for updating an old message to a new message. * * @param oldMsg Full message as it is currently saved * @param newMsg Full message as it should be after the update query ran. */ static QSqlRecord createUpdateRecord(const Message &oldMsg, const Message &newMsg); signals: /** * Can be used to triggerd fetchMessages() */ void fetchMessagesRequested(const QString &user1, const QString &user2, int index); /** * Emitted, when new messages have been fetched */ void messagesFetched(const QVector &messages); public slots: /** * @brief Fetches more entries from the database and emits messagesFetched() with * the results. * * @param user1 Messages are from or to this JID. * @param user2 Messages are from or to this JID. * @param index Number of entries to be skipped, used for paging. */ void fetchMessages(const QString &user1, const QString &user2, int index); /** * Adds a message to the database. */ void addMessage(const Message &msg); /** * Deletes a message from the database. */ void removeMessage(const QString &id); /** * Removes all messages from the database. */ void removeAllMessages(); /** * Loads a message, runs the update lambda and writes it to the DB again. * * @param updateMsg Function that changes the message */ void updateMessage(const QString &id, const std::function &updateMsg); /** * Updates message by @c UPDATE record: This means it doesn't load the message * from the database and writes it again, but executes an UPDATE query. * * @param updateRecord */ void updateMessageRecord(const QString &id, const QSqlRecord &updateRecord); /** * Marks a message as sent using an UPDATE query. */ void setMessageAsSent(const QString &msgId); /** * Marks a message as delivered using an UPDATE query. */ void setMessageAsDelivered(const QString &msgId); + +private: + static MessageDb *s_instance; }; #endif // MESSAGEDB_H diff --git a/src/RosterDb.cpp b/src/RosterDb.cpp index 1c4fa7e..e29957c 100644 --- a/src/RosterDb.cpp +++ b/src/RosterDb.cpp @@ -1,256 +1,271 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2020 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 "RosterDb.h" // Kaidan #include "Database.h" #include "Globals.h" #include "Utils.h" // Qt #include #include #include #include #include +RosterDb *RosterDb::s_instance = nullptr; + RosterDb::RosterDb(Database *db, QObject *parent) : QObject(parent), m_db(db) { + Q_ASSERT(!RosterDb::s_instance); + s_instance = this; + connect(this, &RosterDb::fetchItemsRequested, this, &RosterDb::fetchItems); } +RosterDb::~RosterDb() +{ + s_instance = nullptr; +} + +RosterDb *RosterDb::instance() +{ + return s_instance; +} + void RosterDb::parseItemsFromQuery(QSqlQuery &query, QVector &items) { QSqlRecord rec = query.record(); int idxJid = rec.indexOf("jid"); int idxName = rec.indexOf("name"); int idxLastExchanged = rec.indexOf("lastExchanged"); int idxUnreadMessages = rec.indexOf("unreadMessages"); int idxLastMessage = rec.indexOf("lastMessage"); while (query.next()) { RosterItem item; item.setJid(query.value(idxJid).toString()); item.setName(query.value(idxName).toString()); item.setLastExchanged(QDateTime::fromString( query.value(idxLastExchanged).toString(), Qt::ISODateWithMs )); item.setUnreadMessages(query.value(idxUnreadMessages).toInt()); item.setLastMessage(query.value(idxLastMessage).toString()); items << item; } } QSqlRecord RosterDb::createUpdateRecord(const RosterItem &oldItem, const RosterItem &newItem) { QSqlRecord rec; if (oldItem.jid() != newItem.jid()) rec.append(Utils::createSqlField("jid", newItem.jid())); if (oldItem.name() != newItem.name()) rec.append(Utils::createSqlField("name", oldItem.name())); if (oldItem.lastMessage() != newItem.lastMessage()) rec.append(Utils::createSqlField("lastMessage", newItem.lastMessage())); if (oldItem.lastExchanged() != newItem.lastExchanged()) rec.append(Utils::createSqlField( "lastExchanged", newItem.lastExchanged().toString(Qt::ISODateWithMs) )); if (oldItem.unreadMessages() != newItem.unreadMessages()) rec.append(Utils::createSqlField( "unreadMessages", newItem.unreadMessages() )); return rec; } void RosterDb::addItem(const RosterItem &item) { addItems(QVector() << item); } void RosterDb::addItems(const QVector &items) { QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); m_db->transaction(); QSqlQuery query(db); Utils::prepareQuery(query, db.driver()->sqlStatement( QSqlDriver::InsertStatement, DB_TABLE_ROSTER, db.record(DB_TABLE_ROSTER), true )); for (const auto &item : items) { query.addBindValue(item.jid()); query.addBindValue(item.name()); query.addBindValue(item.lastExchanged().toString(Qt::ISODateWithMs)); query.addBindValue(item.unreadMessages()); query.addBindValue(item.lastMessage()); Utils::execQuery(query); } m_db->commit(); } void RosterDb::removeItem(const QString &jid) { QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); Utils::execQuery( query, "DELETE FROM Roster WHERE jid = ?", QVector() << jid ); } void RosterDb::updateItem(const QString &jid, const std::function &updateItem) { // load current roster item from db QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); query.setForwardOnly(true); Utils::execQuery( query, "SELECT * FROM Roster WHERE jid = ? LIMIT 1", QVector() << jid ); QVector items; parseItemsFromQuery(query, items); // update loaded item if (!items.isEmpty()) { RosterItem item = items.first(); updateItem(item); // replace old item with updated one, if item has changed if (items.first() != item) { // create an SQL record with only the differences QSqlRecord rec = createUpdateRecord(items.first(), item); Utils::execQuery( query, db.driver()->sqlStatement( QSqlDriver::UpdateStatement, DB_TABLE_ROSTER, rec, false ) + Utils::simpleWhereStatement(db.driver(), "jid", jid) ); } } } void RosterDb::replaceItems(const QHash &items) { // load current items QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); query.setForwardOnly(true); Utils::execQuery(query, "SELECT * FROM Roster"); QVector currentItems; parseItemsFromQuery(query, currentItems); m_db->transaction(); QSet newJids = items.keys().toSet(); for (const auto &oldItem : qAsConst(currentItems)) { // We will remove the already existing JIDs, so we get a set of JIDs that // are completely new. // // By calling remove(), we also find out whether the JID is already // existing or not. if (newJids.remove(oldItem.jid())) { // item is also included in newJids -> update // name is (currently) the only attribute that is defined by the // XMPP roster and so could cause a change if (oldItem.name() != items[oldItem.jid()].name()) setItemName(oldItem.jid(), items[oldItem.jid()].name()); } else { // item is not included in newJids -> delete removeItem(oldItem.jid()); } } // now add the completely new JIDs for (const QString &jid : newJids) addItem(items[jid]); m_db->commit(); } void RosterDb::setItemName(const QString &jid, const QString &name) { QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); QSqlRecord rec; rec.append(Utils::createSqlField("name", name)); Utils::execQuery( query, db.driver()->sqlStatement( QSqlDriver::UpdateStatement, DB_TABLE_ROSTER, rec, false ) + Utils::simpleWhereStatement(db.driver(), "jid", jid) ); } void RosterDb::clearAll() { QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); Utils::execQuery(query, "DELETE FROM Roster"); } void RosterDb::fetchItems() { QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); query.setForwardOnly(true); Utils::execQuery(query, "SELECT * FROM Roster"); QVector items; parseItemsFromQuery(query, items); emit itemsFetched(items); } diff --git a/src/RosterDb.h b/src/RosterDb.h index 2afcedc..85c0aad 100644 --- a/src/RosterDb.h +++ b/src/RosterDb.h @@ -1,82 +1,88 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2020 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 . */ #ifndef ROSTERDB_H #define ROSTERDB_H // C++ #include // Qt #include class QSqlQuery; class QSqlRecord; // Kaidan #include "RosterItem.h" class Database; class RosterDb : public QObject { Q_OBJECT + public: RosterDb(Database *db, QObject *parent = nullptr); + ~RosterDb(); + + static RosterDb *instance(); static void parseItemsFromQuery(QSqlQuery &query, QVector &items); /** * Creates an @c QSqlRecord for updating an old item to a new item. * * @param oldMsg Full item as it is currently saved * @param newMsg Full item as it should be after the update query ran. */ static QSqlRecord createUpdateRecord(const RosterItem &oldItem, const RosterItem &newItem); signals: void fetchItemsRequested(); void itemsFetched(const QVector &items); public slots: void addItem(const RosterItem &item); void addItems(const QVector &items); void removeItem(const QString &jid); void updateItem(const QString &jid, const std::function &updateItem); void replaceItems(const QHash &items); void setItemName(const QString &jid, const QString &name); void clearAll(); private slots: void fetchItems(); private: Database *m_db; + + static RosterDb *s_instance; }; #endif // ROSTERDB_H