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