diff --git a/src/Globals.h b/src/Globals.h index 5894f35..c8679dd 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -1,58 +1,60 @@ /* * 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 GLOBALS_H #define GLOBALS_H // Kaidan settings #define KAIDAN_SETTINGS_AUTH_JID "auth/jid" #define KAIDAN_SETTINGS_AUTH_RESOURCE "auth/resource" #define KAIDAN_SETTINGS_AUTH_PASSWD "auth/password" #define KAIDAN_RESOURCE_RANDOM_CHARS \ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" #define INVITATION_URL "https://i.kaidan.im/#" #define APPLICATION_SOURCE_CODE_URL "https://invent.kde.org/kde/kaidan" +#define MESSAGE_MAX_CHARS 1e4 + // XML namespaces #define NS_SPOILERS "urn:xmpp:spoiler:0" #define NS_CARBONS "urn:xmpp:carbons:2" #define NS_REGISTER "jabber:iq:register" // SQL #define DB_CONNECTION "kaidan-messages" #define DB_FILENAME "messages.sqlite3" #define DB_MSG_QUERY_LIMIT 20 #define DB_TABLE_INFO "dbinfo" #define DB_TABLE_ROSTER "Roster" #define DB_TABLE_MESSAGES "Messages" #endif // GLOBALS_H diff --git a/src/MessageModel.cpp b/src/MessageModel.cpp index 7186eab..de62855 100644 --- a/src/MessageModel.cpp +++ b/src/MessageModel.cpp @@ -1,320 +1,333 @@ /* * 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 "MessageModel.h" // Kaidan #include "Kaidan.h" #include "MessageDb.h" // Qt 5 // QXmpp #include MessageModel::MessageModel(Kaidan *kaidan, MessageDb *msgDb, QObject *parent) : QAbstractListModel(parent), kaidan(kaidan), msgDb(msgDb) { connect(msgDb, &MessageDb::messagesFetched, this, &MessageModel::handleMessagesFetched); connect(this, &MessageModel::addMessageRequested, this, &MessageModel::addMessage); connect(this, &MessageModel::addMessageRequested, msgDb, &MessageDb::addMessage); connect(this, &MessageModel::updateMessageRequested, this, &MessageModel::updateMessage); connect(this, &MessageModel::updateMessageRequested, msgDb, &MessageDb::updateMessage); connect(this, &MessageModel::setMessageAsSentRequested, this, &MessageModel::setMessageAsSent); connect(this, &MessageModel::setMessageAsSentRequested, msgDb, &MessageDb::setMessageAsSent); connect(this, &MessageModel::setMessageAsDeliveredRequested, this, &MessageModel::setMessageAsDelivered); connect(this, &MessageModel::setMessageAsDeliveredRequested, msgDb, &MessageDb::setMessageAsDelivered); } MessageModel::~MessageModel() = default; bool MessageModel::isEmpty() const { return m_messages.isEmpty(); } int MessageModel::rowCount(const QModelIndex&) const { return m_messages.length(); } QHash MessageModel::roleNames() const { QHash roles; roles[Timestamp] = "timestamp"; roles[Id] = "id"; roles[Sender] = "sender"; roles[Recipient] = "recipient"; roles[Body] = "body"; roles[SentByMe] = "sentByMe"; roles[MediaType] = "mediaType"; roles[IsEdited] = "isEdited"; roles[IsSent] = "isSent"; roles[IsDelivered] = "isDelivered"; roles[MediaUrl] = "mediaUrl"; roles[MediaSize] = "mediaSize"; roles[MediaContentType] = "mediaContentType"; roles[MediaLastModified] = "mediaLastModifed"; roles[MediaLocation] = "mediaLocation"; roles[MediaThumb] = "mediaThumb"; roles[IsSpoiler] = "isSpoiler"; roles[SpoilerHint] = "spoilerHint"; return roles; } QVariant MessageModel::data(const QModelIndex &index, int role) const { if (!hasIndex(index.row(), index.column(), index.parent())) { qWarning() << "Could not get data from message model." << index << role; return {}; } Message msg = m_messages.at(index.row()); switch (role) { case Timestamp: return msg.stamp(); case Id: return msg.id(); case Sender: return msg.from(); case Recipient: return msg.to(); case Body: return msg.body(); case SentByMe: return msg.sentByMe(); case MediaType: return int(msg.mediaType()); case IsEdited: return msg.isEdited(); case IsSent: return msg.isSent(); case IsDelivered: return msg.isDelivered(); case MediaUrl: return msg.outOfBandUrl(); case MediaLocation: return msg.mediaLocation(); case MediaContentType: return msg.mediaContentType(); case MediaSize: return msg.mediaLastModified(); case MediaLastModified: return msg.mediaLastModified(); case IsSpoiler: return msg.isSpoiler(); case SpoilerHint: return msg.spoilerHint(); // TODO: add (only useful as soon as we have got SIMS) case MediaThumb: return {}; } return {}; } void MessageModel::fetchMore(const QModelIndex &) { emit msgDb->fetchMessagesRequested(kaidan->getJid(), chatPartner(), m_messages.size()); } bool MessageModel::canFetchMore(const QModelIndex &) const { return !m_fetchedAll; } QString MessageModel::chatPartner() { return m_chatPartner; } void MessageModel::setChatPartner(const QString &chatPartner) { if (chatPartner == m_chatPartner) return; m_chatPartner = chatPartner; m_fetchedAll = false; emit chatPartnerChanged(chatPartner); clearAll(); } bool MessageModel::canCorrectMessage(const QString &msgId) const { // Only allow correction of the latest message sent by us for (const auto &msg : m_messages) { if (msg.from() == kaidan->getJid()) return msg.id() == msgId; } return false; } void MessageModel::handleMessagesFetched(const QVector &msgs) { if (msgs.isEmpty()) return; beginInsertRows(QModelIndex(), rowCount(), rowCount() + msgs.length() - 1); for (auto msg : msgs) { msg.setSentByMe(kaidan->getJid() == msg.from()); + processMessage(msg); m_messages << msg; } endInsertRows(); if (msgs.length() < DB_MSG_QUERY_LIMIT) m_fetchedAll = true; } void MessageModel::clearAll() { if (!m_messages.isEmpty()) { beginRemoveRows(QModelIndex(), 0, rowCount() - 1); m_messages.clear(); endRemoveRows(); } } void MessageModel::insertMessage(int idx, const Message &msg) { beginInsertRows(QModelIndex(), idx, idx); m_messages.insert(idx, msg); endInsertRows(); } -void MessageModel::addMessage(const Message &msg) +void MessageModel::addMessage(Message msg) { if (QXmppUtils::jidToBareJid(msg.from()) == m_chatPartner || QXmppUtils::jidToBareJid(msg.to()) == m_chatPartner) { + + processMessage(msg); + // index where to add the new message int i = 0; for (const auto &message : qAsConst(m_messages)) { if (msg.stamp() > message.stamp()) { insertMessage(i, msg); return; } i++; } // add message to the end of the list insertMessage(i, msg); } } void MessageModel::updateMessage(const QString &id, const std::function &updateMsg) { for (int i = 0; i < m_messages.length(); i++) { if (m_messages.at(i).id() == id) { // update message Message msg = m_messages.at(i); updateMsg(msg); // check if item was actually modified if (m_messages.at(i) == msg) return; // check, if the position of the new message may be different if (msg.stamp() == m_messages.at(i).stamp()) { beginRemoveRows(QModelIndex(), i, i); m_messages.removeAt(i); endRemoveRows(); // add the message at the same position insertMessage(i, msg); } else { beginRemoveRows(QModelIndex(), i, i); m_messages.removeAt(i); endRemoveRows(); // put to new position addMessage(msg); } break; } } } void MessageModel::setMessageAsSent(const QString &msgId) { updateMessage(msgId, [] (Message &msg) { msg.setIsSent(true); }); } void MessageModel::setMessageAsDelivered(const QString &msgId) { updateMessage(msgId, [] (Message &msg) { msg.setIsDelivered(true); }); } int MessageModel::searchForMessageFromNewToOld(const QString &searchString, const int startIndex) const { int indexOfFoundMessage = startIndex; if (indexOfFoundMessage >= m_messages.size()) indexOfFoundMessage = 0; for (; indexOfFoundMessage < m_messages.size(); indexOfFoundMessage++) { if (m_messages.at(indexOfFoundMessage).body().contains(searchString, Qt::CaseInsensitive)) return indexOfFoundMessage; } return -1; } int MessageModel::searchForMessageFromOldToNew(const QString &searchString, const int startIndex) const { int indexOfFoundMessage = startIndex; if (indexOfFoundMessage < 0) indexOfFoundMessage = m_messages.size() - 1; for (; indexOfFoundMessage >= 0; indexOfFoundMessage--) { if (m_messages.at(indexOfFoundMessage).body().contains(searchString, Qt::CaseInsensitive)) break; } return indexOfFoundMessage; } + +void MessageModel::processMessage(Message &msg) +{ + if (msg.body().size() > MESSAGE_MAX_CHARS) { + auto body = msg.body(); + body.truncate(MESSAGE_MAX_CHARS); + msg.setBody(body); + } +} diff --git a/src/MessageModel.h b/src/MessageModel.h index a5b9a93..d0a8bb7 100644 --- a/src/MessageModel.h +++ b/src/MessageModel.h @@ -1,139 +1,145 @@ /* * 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 MESSAGEMODEL_H #define MESSAGEMODEL_H #include #include "Message.h" class MessageDb; class Kaidan; class MessageModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString chatPartner READ chatPartner WRITE setChatPartner NOTIFY chatPartnerChanged) public: enum MessageRoles { Timestamp = Qt::UserRole + 1, Id, Sender, Recipient, Body, SentByMe, MediaType, IsEdited, IsSent, IsDelivered, MediaUrl, MediaSize, MediaContentType, MediaLastModified, MediaLocation, MediaThumb, IsSpoiler, SpoilerHint }; Q_ENUM(MessageRoles) MessageModel(Kaidan *kaidan, MessageDb *msgDb, QObject *parent = nullptr); ~MessageModel(); Q_REQUIRED_RESULT bool isEmpty() const; Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT QHash roleNames() const override; Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role) const override; Q_INVOKABLE void fetchMore(const QModelIndex &parent) override; Q_INVOKABLE bool canFetchMore(const QModelIndex &parent) const override; QString chatPartner(); void setChatPartner(const QString &chatPartner); Q_INVOKABLE bool canCorrectMessage(const QString &msgId) const; /** * Searches from the most recent to the oldest message to find a given substring (case insensitive). * * If no index is passed, the search begins from the most recent message. * * @param searchString substring to search for * @param startIndex index of the first message to search for the given string * * @return index of the first found message containing the given string or -1 if no message containing the given string could be found */ Q_INVOKABLE int searchForMessageFromNewToOld(const QString &searchString, const int startIndex = 0) const; /** * Searches from the oldest to the most recent message to find a given substring (case insensitive). * * If no index is passed, the search begins from the oldest message. * * @param searchString substring to search for * @param startIndex index of the first message to search for the given string * * @return index of the first found message containing the given string or -1 if no message containing the given string could be found */ Q_INVOKABLE int searchForMessageFromOldToNew(const QString &searchString, const int startIndex = -1) const; signals: void chatPartnerChanged(const QString &chatPartner); void addMessageRequested(const Message &msg); void updateMessageRequested(const QString &id, const std::function &updateMsg); void setMessageAsSentRequested(const QString &msgId); void setMessageAsDeliveredRequested(const QString &msgId); private slots: void handleMessagesFetched(const QVector &m_messages); - void addMessage(const Message &msg); + void addMessage(Message msg); void updateMessage(const QString &id, const std::function &updateMsg); void setMessageAsSent(const QString &msgId); void setMessageAsDelivered(const QString &msgId); private: void clearAll(); void insertMessage(int i, const Message &msg); + /** + * Shortens messages to 10000 if longer to prevent DoS + * @param message to process + */ + void processMessage(Message &msg); + Kaidan *kaidan; MessageDb *msgDb; QVector m_messages; QString m_chatPartner; bool m_fetchedAll = false; }; #endif // MESSAGEMODEL_H