From f0d840002aa91c9df9ac27a9251ad6c317ee916d Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Thu, 6 Jun 2019 08:41:21 -0700 Subject: [PATCH] refactor(messages): Create class to manage sending/receiving group messages from core --- CMakeLists.txt | 4 + cmake/Testing.cmake | 2 + src/core/core.h | 31 +- src/core/icoregroupmessagesender.h | 33 +++ src/core/icoregroupquery.h | 44 +++ src/core/icoreidhandler.h | 37 +++ src/grouplist.cpp | 6 +- src/model/friendmessagedispatcher.cpp | 8 +- src/model/friendmessagedispatcher.h | 4 +- src/model/group.cpp | 13 +- src/model/group.h | 7 +- src/model/groupmessagedispatcher.cpp | 88 ++++++ src/model/groupmessagedispatcher.h | 58 ++++ src/model/message.cpp | 42 ++- src/model/message.h | 76 ++++- src/persistence/igroupsettings.h | 35 +++ src/persistence/settings.h | 10 +- test/model/friendmessagedispatcher_test.cpp | 9 +- test/model/groupmessagedispatcher_test.cpp | 300 ++++++++++++++++++++ test/model/messageprocessor_test.cpp | 113 ++++++++ 20 files changed, 886 insertions(+), 34 deletions(-) create mode 100644 src/core/icoregroupmessagesender.h create mode 100644 src/core/icoregroupquery.h create mode 100644 src/core/icoreidhandler.h create mode 100644 src/model/groupmessagedispatcher.cpp create mode 100644 src/model/groupmessagedispatcher.h create mode 100644 src/persistence/igroupsettings.h create mode 100644 test/model/groupmessagedispatcher_test.cpp create mode 100644 test/model/messageprocessor_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b73ec7626..bc00660e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,6 +331,10 @@ set(${PROJECT_NAME}_SOURCES src/model/imessagedispatcher.h src/model/friendmessagedispatcher.h src/model/friendmessagedispatcher.cpp + src/model/groupmessagedispatcher.h + src/model/groupmessagedispatcher.cpp + src/model/message.h + src/model/message.cpp src/model/groupinvite.cpp src/model/groupinvite.h src/model/group.cpp diff --git a/cmake/Testing.cmake b/cmake/Testing.cmake index 499273085..7fe9df70f 100644 --- a/cmake/Testing.cmake +++ b/cmake/Testing.cmake @@ -29,6 +29,8 @@ auto_test(persistence paths) auto_test(persistence dbschema) auto_test(persistence offlinemsgengine) auto_test(model friendmessagedispatcher) +auto_test(model groupmessagedispatcher) +auto_test(model messageprocessor) if (UNIX) auto_test(platform posixsignalnotifier) diff --git a/src/core/core.h b/src/core/core.h index 61048b829..7c75d0624 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -23,6 +23,9 @@ #include "groupid.h" #include "icorefriendmessagesender.h" +#include "icoregroupmessagesender.h" +#include "icoregroupquery.h" +#include "icoreidhandler.h" #include "receiptnum.h" #include "toxfile.h" #include "toxid.h" @@ -50,7 +53,11 @@ class Core; using ToxCorePtr = std::unique_ptr; -class Core : public QObject, public ICoreFriendMessageSender +class Core : public QObject, + public ICoreFriendMessageSender, + public ICoreIdHandler, + public ICoreGroupMessageSender, + public ICoreGroupQuery { Q_OBJECT public: @@ -74,12 +81,12 @@ public: static QStringList splitMessage(const QString& message); QString getPeerName(const ToxPk& id) const; QVector getFriendList() const; - GroupId getGroupPersistentId(uint32_t groupNumber) const; - uint32_t getGroupNumberPeers(int groupId) const; - QString getGroupPeerName(int groupId, int peerId) const; - ToxPk getGroupPeerPk(int groupId, int peerId) const; - QStringList getGroupPeerNames(int groupId) const; - bool getGroupAvEnabled(int groupId) const; + GroupId getGroupPersistentId(uint32_t groupNumber) const override; + uint32_t getGroupNumberPeers(int groupId) const override; + QString getGroupPeerName(int groupId, int peerId) const override; + ToxPk getGroupPeerPk(int groupId, int peerId) const override; + QStringList getGroupPeerNames(int groupId) const override; + bool getGroupAvEnabled(int groupId) const override; ToxPk getFriendPublicKey(uint32_t friendNumber) const; QString getFriendUsername(uint32_t friendNumber) const; @@ -88,11 +95,11 @@ public: uint32_t joinGroupchat(const GroupInvite& inviteInfo); void quitGroupChat(int groupId) const; - QString getUsername() const; + QString getUsername() const override; Status::Status getStatus() const; QString getStatusMessage() const; - ToxId getSelfId() const; - ToxPk getSelfPublicKey() const; + ToxId getSelfId() const override; + ToxPk getSelfPublicKey() const override; QPair getKeypair() const; void sendFile(uint32_t friendId, QString filename, QString filePath, long long filesize); @@ -115,8 +122,8 @@ public slots: void setStatusMessage(const QString& message); bool sendMessage(uint32_t friendId, const QString& message, ReceiptNum& receipt) override; - void sendGroupMessage(int groupId, const QString& message); - void sendGroupAction(int groupId, const QString& message); + void sendGroupMessage(int groupId, const QString& message) override; + void sendGroupAction(int groupId, const QString& message) override; void changeGroupTitle(int groupId, const QString& title); bool sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt) override; void sendTyping(uint32_t friendId, bool typing); diff --git a/src/core/icoregroupmessagesender.h b/src/core/icoregroupmessagesender.h new file mode 100644 index 000000000..537c8a0a8 --- /dev/null +++ b/src/core/icoregroupmessagesender.h @@ -0,0 +1,33 @@ +/* + Copyright © 2019 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre 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. + + qTox 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 qTox. If not, see . +*/ + +#ifndef ICORE_GROUP_MESSAGE_SENDER_H +#define ICORE_GROUP_MESSAGE_SENDER_H + +#include + +class ICoreGroupMessageSender +{ +public: + virtual ~ICoreGroupMessageSender() = default; + virtual void sendGroupAction(int groupId, const QString& message) = 0; + virtual void sendGroupMessage(int groupId, const QString& message) = 0; +}; + +#endif /*ICORE_GROUP_MESSAGE_SENDER_H*/ diff --git a/src/core/icoregroupquery.h b/src/core/icoregroupquery.h new file mode 100644 index 000000000..b9f4c6c0e --- /dev/null +++ b/src/core/icoregroupquery.h @@ -0,0 +1,44 @@ +/* + Copyright (C) 2013 by Maxim Biro + Copyright © 2014-2018 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + This program 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. + + qTox 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 qTox. If not, see . +*/ + +#ifndef ICORE_GROUP_QUERY_H +#define ICORE_GROUP_QUERY_H + +#include "groupid.h" +#include "toxpk.h" + +#include +#include + +#include + +class ICoreGroupQuery +{ +public: + virtual ~ICoreGroupQuery() = default; + virtual GroupId getGroupPersistentId(uint32_t groupNumber) const = 0; + virtual uint32_t getGroupNumberPeers(int groupId) const = 0; + virtual QString getGroupPeerName(int groupId, int peerId) const = 0; + virtual ToxPk getGroupPeerPk(int groupId, int peerId) const = 0; + virtual QStringList getGroupPeerNames(int groupId) const = 0; + virtual bool getGroupAvEnabled(int groupId) const = 0; +}; + +#endif /*ICORE_GROUP_QUERY_H*/ diff --git a/src/core/icoreidhandler.h b/src/core/icoreidhandler.h new file mode 100644 index 000000000..eb3717dcb --- /dev/null +++ b/src/core/icoreidhandler.h @@ -0,0 +1,37 @@ +/* + Copyright © 2019 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre 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. + + qTox 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 qTox. If not, see . +*/ + +#ifndef ICORE_ID_HANDLER_H +#define ICORE_ID_HANDLER_H + +#include "toxid.h" +#include "toxpk.h" + +class ICoreIdHandler +{ + +public: + virtual ~ICoreIdHandler() = default; + virtual ToxId getSelfId() const = 0; + virtual ToxPk getSelfPublicKey() const = 0; + virtual QString getUsername() const = 0; +}; + + +#endif /*ICORE_ID_HANDLER_H*/ diff --git a/src/grouplist.cpp b/src/grouplist.cpp index 85fa327e3..baba76ab6 100644 --- a/src/grouplist.cpp +++ b/src/grouplist.cpp @@ -18,6 +18,7 @@ */ #include "grouplist.h" +#include "src/core/core.h" #include "src/model/group.h" #include #include @@ -31,7 +32,10 @@ Group* GroupList::addGroup(int groupNum, const GroupId& groupId, const QString& if (checker != groupList.end()) qWarning() << "addGroup: groupId already taken"; - Group* newGroup = new Group(groupNum, groupId, name, isAvGroupchat, selfName); + // TODO: Core instance is bad but grouplist is also an instance so we can + // deal with this later + auto core = Core::getInstance(); + Group* newGroup = new Group(groupNum, groupId, name, isAvGroupchat, selfName, *core, *core); groupList[groupId] = newGroup; id2key[groupNum] = groupId; return newGroup; diff --git a/src/model/friendmessagedispatcher.cpp b/src/model/friendmessagedispatcher.cpp index ecfa5dbb7..803b4645c 100644 --- a/src/model/friendmessagedispatcher.cpp +++ b/src/model/friendmessagedispatcher.cpp @@ -42,10 +42,12 @@ bool sendMessageToCore(ICoreFriendMessageSender& messageSender, const Friend& f, } } // namespace -FriendMessageDispatcher::FriendMessageDispatcher(Friend& f_, ICoreFriendMessageSender& messageSender_) +FriendMessageDispatcher::FriendMessageDispatcher(Friend& f_, MessageProcessor processor_, + ICoreFriendMessageSender& messageSender_) : f(f_) , messageSender(messageSender_) , offlineMsgEngine(&f_, &messageSender_) + , processor(std::move(processor_)) { connect(&f, &Friend::statusChanged, this, &FriendMessageDispatcher::onFriendStatusChange); } @@ -58,7 +60,7 @@ FriendMessageDispatcher::sendMessage(bool isAction, const QString& content) { const auto firstId = nextMessageId; auto lastId = nextMessageId; - for (const auto& message : processOutgoingMessage(isAction, content)) { + for (const auto& message : processor.processOutgoingMessage(isAction, content)) { auto messageId = nextMessageId++; lastId = messageId; auto onOfflineMsgComplete = [this, messageId] { emit this->messageComplete(messageId); }; @@ -89,7 +91,7 @@ FriendMessageDispatcher::sendMessage(bool isAction, const QString& content) */ void FriendMessageDispatcher::onMessageReceived(bool isAction, const QString& content) { - emit this->messageReceived(f.getPublicKey(), processIncomingMessage(isAction, content)); + emit this->messageReceived(f.getPublicKey(), processor.processIncomingMessage(isAction, content)); } /** diff --git a/src/model/friendmessagedispatcher.h b/src/model/friendmessagedispatcher.h index f44b2796a..74b210916 100644 --- a/src/model/friendmessagedispatcher.h +++ b/src/model/friendmessagedispatcher.h @@ -35,7 +35,8 @@ class FriendMessageDispatcher : public IMessageDispatcher { Q_OBJECT public: - FriendMessageDispatcher(Friend& f, ICoreFriendMessageSender& messageSender); + FriendMessageDispatcher(Friend& f, MessageProcessor processor, + ICoreFriendMessageSender& messageSender); std::pair sendMessage(bool isAction, const QString& content) override; @@ -51,6 +52,7 @@ private: ICoreFriendMessageSender& messageSender; OfflineMsgEngine offlineMsgEngine; + MessageProcessor processor; }; diff --git a/src/model/group.cpp b/src/model/group.cpp index d33ab5ded..ee3e21c4a 100644 --- a/src/model/group.cpp +++ b/src/model/group.cpp @@ -33,12 +33,14 @@ static const int MAX_GROUP_TITLE_LENGTH = 128; Group::Group(int groupId, const GroupId persistentGroupId, const QString& name, bool isAvGroupchat, - const QString& selfName) + const QString& selfName, ICoreGroupQuery& groupQuery, ICoreIdHandler& idHandler) : selfName{selfName} , title{name} , toxGroupNum(groupId) , groupId{persistentGroupId} , avGroupchat{isAvGroupchat} + , groupQuery(groupQuery) + , idHandler(idHandler) { // in groupchats, we only notify on messages containing your name <-- dumb // sound notifications should be on all messages, but system popup notification @@ -88,15 +90,14 @@ void Group::regeneratePeerList() // receive the name changed signal a little later, we will emit userJoined before we have their // username, using just their ToxPk, then shortly after emit another peerNameChanged signal. // This can cause double-updated to UI and chatlog, but is unavoidable given the API of toxcore. - const Core* core = Core::getInstance(); - QStringList peers = core->getGroupPeerNames(toxGroupNum); + QStringList peers = groupQuery.getGroupPeerNames(toxGroupNum); const auto oldPeerNames = peerDisplayNames; peerDisplayNames.clear(); const int nPeers = peers.size(); for (int i = 0; i < nPeers; ++i) { - const auto pk = core->getGroupPeerPk(toxGroupNum, i); - if (pk == core->getSelfPublicKey()) { - peerDisplayNames[pk] = core->getUsername(); + const auto pk = groupQuery.getGroupPeerPk(toxGroupNum, i); + if (pk == idHandler.getSelfPublicKey()) { + peerDisplayNames[pk] = idHandler.getUsername(); } else { peerDisplayNames[pk] = FriendList::decideNickname(pk, peers[i]); } diff --git a/src/model/group.h b/src/model/group.h index bd785d91a..1511d6fc3 100644 --- a/src/model/group.h +++ b/src/model/group.h @@ -24,6 +24,8 @@ #include "src/core/contactid.h" #include "src/core/groupid.h" +#include "src/core/icoregroupquery.h" +#include "src/core/icoreidhandler.h" #include "src/core/toxpk.h" #include @@ -34,7 +36,8 @@ class Group : public Contact { Q_OBJECT public: - Group(int groupId, const GroupId persistentGroupId, const QString& name, bool isAvGroupchat, const QString& selfName); + Group(int groupId, const GroupId persistentGroupId, const QString& name, bool isAvGroupchat, + const QString& selfName, ICoreGroupQuery& groupQuery, ICoreIdHandler& idHandler); bool isAvGroupchat() const; uint32_t getId() const override; const GroupId& getPersistentId() const override; @@ -70,6 +73,8 @@ private: void stopAudioOfDepartedPeers(const ToxPk& peerPk); private: + ICoreGroupQuery& groupQuery; + ICoreIdHandler& idHandler; QString selfName; QString title; QMap peerDisplayNames; diff --git a/src/model/groupmessagedispatcher.cpp b/src/model/groupmessagedispatcher.cpp new file mode 100644 index 000000000..f62327bbc --- /dev/null +++ b/src/model/groupmessagedispatcher.cpp @@ -0,0 +1,88 @@ +/* + Copyright © 2019 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre 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. + + qTox 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 qTox. If not, see . +*/ + +#include "groupmessagedispatcher.h" +#include "src/persistence/igroupsettings.h" + +#include + +GroupMessageDispatcher::GroupMessageDispatcher(Group& g_, MessageProcessor processor_, + ICoreIdHandler& idHandler_, + ICoreGroupMessageSender& messageSender_, + const IGroupSettings& groupSettings_) + : group(g_) + , processor(processor_) + , idHandler(idHandler_) + , messageSender(messageSender_) + , groupSettings(groupSettings_) +{ + processor.enableMentions(); +} + +std::pair +GroupMessageDispatcher::sendMessage(bool isAction, QString const& content) +{ + const auto firstMessageId = nextMessageId; + auto lastMessageId = firstMessageId; + + for (auto const& message : processor.processOutgoingMessage(isAction, content)) { + auto messageId = nextMessageId++; + lastMessageId = messageId; + if (group.getPeersCount() != 1) { + if (message.isAction) { + messageSender.sendGroupAction(group.getId(), message.content); + } else { + messageSender.sendGroupMessage(group.getId(), message.content); + } + } + + // Emit both signals since we do not have receipts for groups + // + // NOTE: We could in theory keep track of our sent message and wait for + // toxcore to send it back to us to indicate a completed message, but + // this isn't necessarily the design of toxcore and associating the + // received message back would be difficult. + emit this->messageSent(messageId, message); + emit this->messageComplete(messageId); + } + + return std::make_pair(firstMessageId, lastMessageId); +} + +/** + * @brief Processes and dispatches received message from toxcore + * @param[in] sender + * @param[in] isAction True if is action + * @param[in] content Message content + */ +void GroupMessageDispatcher::onMessageReceived(const ToxPk& sender, bool isAction, QString const& content) +{ + bool isSelf = sender == idHandler.getSelfPublicKey(); + + if (isSelf) { + return; + } + + if (groupSettings.getBlackList().contains(sender.toString())) { + qDebug() << "onGroupMessageReceived: Filtered:" << sender.toString(); + return; + } + + emit messageReceived(sender, processor.processIncomingMessage(isAction, content)); +} diff --git a/src/model/groupmessagedispatcher.h b/src/model/groupmessagedispatcher.h new file mode 100644 index 000000000..1ba1a788f --- /dev/null +++ b/src/model/groupmessagedispatcher.h @@ -0,0 +1,58 @@ +/* + Copyright © 2019 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre 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. + + qTox 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 qTox. If not, see . +*/ + +#ifndef GROUP_MESSAGE_DISPATCHER_H +#define GROUP_MESSAGE_DISPATCHER_H + +#include "src/core/icoregroupmessagesender.h" +#include "src/core/icoreidhandler.h" +#include "src/model/group.h" +#include "src/model/imessagedispatcher.h" +#include "src/model/message.h" + +#include +#include + +#include + +class IGroupSettings; + +class GroupMessageDispatcher : public IMessageDispatcher +{ + Q_OBJECT +public: + GroupMessageDispatcher(Group& group, MessageProcessor processor, ICoreIdHandler& idHandler, + ICoreGroupMessageSender& messageSender, + const IGroupSettings& groupSettings); + + std::pair sendMessage(bool isAction, + QString const& content) override; + void onMessageReceived(ToxPk const& sender, bool isAction, QString const& content); + +private: + Group& group; + MessageProcessor processor; + ICoreIdHandler& idHandler; + ICoreGroupMessageSender& messageSender; + const IGroupSettings& groupSettings; + DispatchedMessageId nextMessageId{0}; +}; + + +#endif /* IMESSAGE_DISPATCHER_H */ diff --git a/src/model/message.cpp b/src/model/message.cpp index 4f0f4f0bc..123d2a9d6 100644 --- a/src/model/message.cpp +++ b/src/model/message.cpp @@ -18,9 +18,25 @@ */ #include "message.h" +#include "friend.h" #include "src/core/core.h" -std::vector processOutgoingMessage(bool isAction, const QString& content) +void MessageProcessor::SharedParams::onUserNameSet(const QString& username) +{ + QString sanename = username; + sanename.remove(QRegExp("[\\t\\n\\v\\f\\r\\x0000]")); + nameMention = QRegExp("\\b" + QRegExp::escape(username) + "\\b", Qt::CaseInsensitive); + sanitizedNameMention = nameMention; +} + +MessageProcessor::MessageProcessor(const MessageProcessor::SharedParams& sharedParams) + : sharedParams(sharedParams) +{} + +/** + * @brief Converts an outgoing message into one (or many) sanitized Message(s) + */ +std::vector MessageProcessor::processOutgoingMessage(bool isAction, QString const& content) { std::vector ret; @@ -40,12 +56,34 @@ std::vector processOutgoingMessage(bool isAction, const QString& conten return ret; } -Message processIncomingMessage(bool isAction, const QString& message) + +/** + * @brief Converts an incoming message into a sanitized Message + */ +Message MessageProcessor::processIncomingMessage(bool isAction, QString const& message) { QDateTime timestamp = QDateTime::currentDateTime(); auto ret = Message{}; ret.isAction = isAction; ret.content = message; ret.timestamp = timestamp; + + if (detectingMentions) { + auto nameMention = sharedParams.GetNameMention(); + auto sanitizedNameMention = sharedParams.GetSanitizedNameMention(); + + for (auto const& mention : {nameMention, sanitizedNameMention}) { + if (mention.indexIn(ret.content) == -1) { + continue; + } + + auto pos = static_cast(mention.pos(0)); + auto length = static_cast(mention.matchedLength()); + + ret.metadata.push_back({MessageMetadataType::selfMention, pos, pos + length}); + break; + } + } + return ret; } diff --git a/src/model/message.h b/src/model/message.h index 990208efa..67ed0573e 100644 --- a/src/model/message.h +++ b/src/model/message.h @@ -25,15 +25,87 @@ #include +class Friend; + +// NOTE: This could be extended in the future to handle all text processing (see +// ChatMessage::createChatMessage) +enum class MessageMetadataType +{ + selfMention, +}; + +// May need to be extended in the future to have a more varianty type (imagine +// if we wanted to add message replies and shoved a reply id in here) +struct MessageMetadata +{ + MessageMetadataType type; + // Indicates start position within a Message::content + size_t start; + // Indicates end position within a Message::content + size_t end; +}; + struct Message { bool isAction; QString content; QDateTime timestamp; + std::vector metadata; }; -std::vector processOutgoingMessage(bool isAction, const QString& content); -Message processIncomingMessage(bool isAction, const QString& message); +class MessageProcessor +{ + +public: + /** + * Parameters needed by all message processors. Used to reduce duplication + * of expensive data looked at by all message processors + */ + class SharedParams + { + + public: + QRegExp GetNameMention() const + { + return nameMention; + } + QRegExp GetSanitizedNameMention() const + { + return sanitizedNameMention; + } + void onUserNameSet(const QString& username); + + private: + QRegExp nameMention; + QRegExp sanitizedNameMention; + }; + + MessageProcessor(const SharedParams& sharedParams); + + std::vector processOutgoingMessage(bool isAction, QString const& content); + + Message processIncomingMessage(bool isAction, QString const& message); + + /** + * @brief Enables mention detection in the processor + */ + inline void enableMentions() + { + detectingMentions = true; + } + + /** + * @brief Disables mention detection in the processor + */ + inline void disableMentions() + { + detectingMentions = false; + }; + +private: + bool detectingMentions = false; + const SharedParams& sharedParams; +}; #endif /*MESSAGE_H*/ diff --git a/src/persistence/igroupsettings.h b/src/persistence/igroupsettings.h new file mode 100644 index 000000000..ce9a94db5 --- /dev/null +++ b/src/persistence/igroupsettings.h @@ -0,0 +1,35 @@ +/* + Copyright © 2014-2019 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre 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. + + qTox 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 qTox. If not, see . +*/ + +#ifndef IGROUP_SETTINGS_H +#define IGROUP_SETTINGS_H + +#include + +class IGroupSettings +{ +public: + virtual ~IGroupSettings() = default; + virtual QStringList getBlackList() const = 0; + virtual void setBlackList(const QStringList& blist) = 0; + virtual bool getGroupAlwaysNotify() const = 0; + virtual void setGroupAlwaysNotify(bool newValue) = 0; +}; + +#endif /*IGROUP_SETTINGS_H*/ diff --git a/src/persistence/settings.h b/src/persistence/settings.h index 1b2ae33bc..e360b4fde 100644 --- a/src/persistence/settings.h +++ b/src/persistence/settings.h @@ -26,6 +26,7 @@ #include "src/core/toxencrypt.h" #include "src/core/toxfile.h" #include "src/persistence/ifriendsettings.h" +#include "src/persistence/igroupsettings.h" #include "src/video/ivideosettings.h" #include @@ -46,6 +47,7 @@ enum class syncType; class Settings : public QObject, public ICoreSettings, public IFriendSettings, + public IGroupSettings, public IAudioSettings, public IVideoSettings { @@ -343,8 +345,8 @@ public: bool getBusySound() const; void setBusySound(bool newValue); - bool getGroupAlwaysNotify() const; - void setGroupAlwaysNotify(bool newValue); + bool getGroupAlwaysNotify() const override; + void setGroupAlwaysNotify(bool newValue) override; QString getInDev() const override; void setInDev(const QString& deviceSpecifier) override; @@ -476,8 +478,8 @@ public: // Privacy bool getTypingNotification() const; void setTypingNotification(bool enabled); - QStringList getBlackList() const; - void setBlackList(const QStringList& blist); + QStringList getBlackList() const override; + void setBlackList(const QStringList& blist) override; // State QByteArray getWindowGeometry() const; diff --git a/test/model/friendmessagedispatcher_test.cpp b/test/model/friendmessagedispatcher_test.cpp index 606569782..1d323fdc1 100644 --- a/test/model/friendmessagedispatcher_test.cpp +++ b/test/model/friendmessagedispatcher_test.cpp @@ -74,6 +74,8 @@ private: // All unique_ptrs to make construction/init() easier to manage std::unique_ptr f; std::unique_ptr messageSender; + std::unique_ptr sharedProcessorParams; + std::unique_ptr messageProcessor; std::unique_ptr friendMessageDispatcher; std::map outgoingMessages; std::deque receivedMessages; @@ -89,8 +91,11 @@ void TestFriendMessageDispatcher::init() f = std::unique_ptr(new Friend(0, ToxPk())); f->setStatus(Status::Status::Online); messageSender = std::unique_ptr(new MockFriendMessageSender()); - friendMessageDispatcher = - std::unique_ptr(new FriendMessageDispatcher(*f, *messageSender)); + sharedProcessorParams = + std::unique_ptr(new MessageProcessor::SharedParams()); + messageProcessor = std::unique_ptr(new MessageProcessor(*sharedProcessorParams)); + friendMessageDispatcher = std::unique_ptr( + new FriendMessageDispatcher(*f, *messageProcessor, *messageSender)); connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageSent, this, &TestFriendMessageDispatcher::onMessageSent); diff --git a/test/model/groupmessagedispatcher_test.cpp b/test/model/groupmessagedispatcher_test.cpp new file mode 100644 index 000000000..85d23018d --- /dev/null +++ b/test/model/groupmessagedispatcher_test.cpp @@ -0,0 +1,300 @@ +#include "src/core/icoregroupmessagesender.h" +#include "src/model/group.h" +#include "src/model/groupmessagedispatcher.h" +#include "src/model/message.h" +#include "src/persistence/settings.h" + +#include +#include + +#include + + +class MockGroupMessageSender : public ICoreGroupMessageSender +{ +public: + void sendGroupAction(int groupId, const QString& action) override + { + numSentActions++; + } + + void sendGroupMessage(int groupId, const QString& message) override + { + numSentMessages++; + } + + size_t numSentActions = 0; + size_t numSentMessages = 0; +}; + +/** + * Mock 1 peer at group number 0 + */ +class MockGroupQuery : public ICoreGroupQuery +{ +public: + GroupId getGroupPersistentId(uint32_t groupNumber) const override + { + return GroupId(0); + } + + uint32_t getGroupNumberPeers(int groupId) const override + { + if (emptyGroup) { + return 1; + } + + return 2; + } + + QString getGroupPeerName(int groupId, int peerId) const override + { + return QString("peer") + peerId; + } + + ToxPk getGroupPeerPk(int groupId, int peerId) const override + { + uint8_t id[TOX_PUBLIC_KEY_SIZE] = {static_cast(peerId)}; + return ToxPk(id); + } + + QStringList getGroupPeerNames(int groupId) const override + { + if (emptyGroup) { + return QStringList({QString("me")}); + } + return QStringList({QString("me"), QString("other")}); + } + + bool getGroupAvEnabled(int groupId) const override + { + return false; + } + + void setAsEmptyGroup() + { + emptyGroup = true; + } + + void setAsFunctionalGroup() + { + emptyGroup = false; + } + +private: + bool emptyGroup = false; +}; + +class MockCoreIdHandler : public ICoreIdHandler +{ +public: + ToxId getSelfId() const override + { + std::terminate(); + return ToxId(); + } + + ToxPk getSelfPublicKey() const override + { + static uint8_t id[TOX_PUBLIC_KEY_SIZE] = {0}; + return ToxPk(id); + } + + QString getUsername() const override + { + return "me"; + } +}; + +class MockGroupSettings : public IGroupSettings +{ +public: + QStringList getBlackList() const override + { + return blacklist; + } + + void setBlackList(const QStringList& blist) override + { + blacklist = blist; + } + + bool getGroupAlwaysNotify() const override + { + return false; + } + + void setGroupAlwaysNotify(bool newValue) override {} + +private: + QStringList blacklist; +}; + +class TestGroupMessageDispatcher : public QObject +{ + Q_OBJECT + +public: + TestGroupMessageDispatcher(); + +private slots: + void init(); + void testSignals(); + void testMessageSending(); + void testEmptyGroup(); + void testSelfReceive(); + void testBlacklist(); + + void onMessageSent(DispatchedMessageId id, Message message) + { + auto it = outgoingMessages.find(id); + QVERIFY(it == outgoingMessages.end()); + outgoingMessages.emplace(id); + sentMessages.push_back(std::move(message)); + } + + void onMessageComplete(DispatchedMessageId id) + { + auto it = outgoingMessages.find(id); + QVERIFY(it != outgoingMessages.end()); + outgoingMessages.erase(it); + } + + void onMessageReceived(const ToxPk& sender, Message message) + { + receivedMessages.push_back(std::move(message)); + } + +private: + // All unique_ptrs to make construction/init() easier to manage + std::unique_ptr groupSettings; + std::unique_ptr groupQuery; + std::unique_ptr coreIdHandler; + std::unique_ptr g; + std::unique_ptr messageSender; + std::unique_ptr sharedProcessorParams; + std::unique_ptr messageProcessor; + std::unique_ptr groupMessageDispatcher; + std::set outgoingMessages; + std::deque sentMessages; + std::deque receivedMessages; +}; + +TestGroupMessageDispatcher::TestGroupMessageDispatcher() {} + +/** + * @brief Test initialization. Resets all members to initial state + */ +void TestGroupMessageDispatcher::init() +{ + groupSettings = std::unique_ptr(new MockGroupSettings()); + groupQuery = std::unique_ptr(new MockGroupQuery()); + coreIdHandler = std::unique_ptr(new MockCoreIdHandler()); + g = std::unique_ptr( + new Group(0, GroupId(0), "TestGroup", false, "me", *groupQuery, *coreIdHandler)); + messageSender = std::unique_ptr(new MockGroupMessageSender()); + sharedProcessorParams = + std::unique_ptr(new MessageProcessor::SharedParams()); + messageProcessor = std::unique_ptr(new MessageProcessor(*sharedProcessorParams)); + groupMessageDispatcher = std::unique_ptr( + new GroupMessageDispatcher(*g, *messageProcessor, *coreIdHandler, *messageSender, + *groupSettings)); + + connect(groupMessageDispatcher.get(), &GroupMessageDispatcher::messageSent, this, + &TestGroupMessageDispatcher::onMessageSent); + connect(groupMessageDispatcher.get(), &GroupMessageDispatcher::messageComplete, this, + &TestGroupMessageDispatcher::onMessageComplete); + connect(groupMessageDispatcher.get(), &GroupMessageDispatcher::messageReceived, this, + &TestGroupMessageDispatcher::onMessageReceived); + + outgoingMessages = std::set(); + sentMessages = std::deque(); + receivedMessages = std::deque(); +} + +/** + * @brief Tests that the signals emitted by the dispatcher are all emitted at the correct times + */ +void TestGroupMessageDispatcher::testSignals() +{ + groupMessageDispatcher->sendMessage(false, "test"); + + // For groups we pair our sent and completed signals since we have no receiver reports + QVERIFY(outgoingMessages.size() == 0); + QVERIFY(!sentMessages.empty()); + QVERIFY(sentMessages.front().isAction == false); + QVERIFY(sentMessages.front().content == "test"); + + // If signals are emitted correctly we should have one message in our received message buffer + QVERIFY(receivedMessages.empty()); + groupMessageDispatcher->onMessageReceived(ToxPk(), false, "test2"); + + QVERIFY(!receivedMessages.empty()); + QVERIFY(receivedMessages.front().isAction == false); + QVERIFY(receivedMessages.front().content == "test2"); +} + +/** + * @brief Tests that sent messages actually go through to core + */ +void TestGroupMessageDispatcher::testMessageSending() +{ + groupMessageDispatcher->sendMessage(false, "Test"); + + QVERIFY(messageSender->numSentMessages == 1); + QVERIFY(messageSender->numSentActions == 0); + + groupMessageDispatcher->sendMessage(true, "Test"); + + QVERIFY(messageSender->numSentMessages == 1); + QVERIFY(messageSender->numSentActions == 1); +} + +/** + * @brief Tests that if we are the only member in a group we do _not_ send messages to core. Toxcore + * isn't too happy if we send messages and we're the only one in the group + */ +void TestGroupMessageDispatcher::testEmptyGroup() +{ + groupQuery->setAsEmptyGroup(); + g->regeneratePeerList(); + + groupMessageDispatcher->sendMessage(false, "Test"); + groupMessageDispatcher->sendMessage(true, "Test"); + + QVERIFY(messageSender->numSentMessages == 0); + QVERIFY(messageSender->numSentActions == 0); +} + +/** + * @brief Tests that we do not emit any signals if we receive a message from ourself. Toxcore will send us back messages we sent + */ +void TestGroupMessageDispatcher::testSelfReceive() +{ + uint8_t selfId[TOX_PUBLIC_KEY_SIZE] = {0}; + groupMessageDispatcher->onMessageReceived(ToxPk(selfId), false, "Test"); + QVERIFY(receivedMessages.size() == 0); + + uint8_t id[TOX_PUBLIC_KEY_SIZE] = {1}; + groupMessageDispatcher->onMessageReceived(ToxPk(id), false, "Test"); + QVERIFY(receivedMessages.size() == 1); +} + +/** + * @brief Tests that messages from blacklisted peers do not get propogated from the dispatcher + */ +void TestGroupMessageDispatcher::testBlacklist() +{ + uint8_t id[TOX_PUBLIC_KEY_SIZE] = {1}; + auto otherPk = ToxPk(id); + groupMessageDispatcher->onMessageReceived(otherPk, false, "Test"); + QVERIFY(receivedMessages.size() == 1); + + groupSettings->setBlackList({otherPk.toString()}); + groupMessageDispatcher->onMessageReceived(otherPk, false, "Test"); + QVERIFY(receivedMessages.size() == 1); +} + +// Cannot be guiless due to a settings instance in GroupMessageDispatcher +QTEST_GUILESS_MAIN(TestGroupMessageDispatcher) +#include "groupmessagedispatcher_test.moc" diff --git a/test/model/messageprocessor_test.cpp b/test/model/messageprocessor_test.cpp new file mode 100644 index 000000000..817002629 --- /dev/null +++ b/test/model/messageprocessor_test.cpp @@ -0,0 +1,113 @@ +#include "src/model/message.h" + +#include + +#include +#include + +namespace { +bool messageHasSelfMention(const Message& message) +{ + return std::any_of(message.metadata.begin(), message.metadata.end(), [](MessageMetadata meta) { + return meta.type == MessageMetadataType::selfMention; + }); +} +} // namespace + +class TestMessageProcessor : public QObject +{ + Q_OBJECT + +public: + TestMessageProcessor(){}; + +private slots: + void testSelfMention(); + void testOutgoingMessage(); + void testIncomingMessage(); +}; + + +/** + * @brief Tests detection of username + */ +void TestMessageProcessor::testSelfMention() +{ + MessageProcessor::SharedParams sharedParams; + sharedParams.onUserNameSet("MyUserName"); + + auto messageProcessor = MessageProcessor(sharedParams); + messageProcessor.enableMentions(); + + // Using my name should match + auto processedMessage = messageProcessor.processIncomingMessage(false, "MyUserName hi"); + QVERIFY(messageHasSelfMention(processedMessage)); + + // Action messages should match too + processedMessage = messageProcessor.processIncomingMessage(true, "MyUserName hi"); + QVERIFY(messageHasSelfMention(processedMessage)); + + // Too much text shouldn't match + processedMessage = messageProcessor.processIncomingMessage(false, "MyUserName2"); + QVERIFY(!messageHasSelfMention(processedMessage)); + + // Unless it's a colon + processedMessage = messageProcessor.processIncomingMessage(false, "MyUserName: test"); + QVERIFY(messageHasSelfMention(processedMessage)); + + // Too little text shouldn't match + processedMessage = messageProcessor.processIncomingMessage(false, "MyUser"); + QVERIFY(!messageHasSelfMention(processedMessage)); + + // The regex should be case insensitive + processedMessage = messageProcessor.processIncomingMessage(false, "myusername hi"); + QVERIFY(messageHasSelfMention(processedMessage)); + + // New user name changes should be detected + sharedParams.onUserNameSet("NewUserName"); + processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi"); + QVERIFY(messageHasSelfMention(processedMessage)); + + // Special characters should be removed + sharedParams.onUserNameSet("New\nUserName"); + processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi"); + QVERIFY(messageHasSelfMention(processedMessage)); +} + +/** + * @brief Tests behavior of the processor for outgoing messages + */ +void TestMessageProcessor::testOutgoingMessage() +{ + auto sharedParams = MessageProcessor::SharedParams(); + auto messageProcessor = MessageProcessor(sharedParams); + + QString testStr; + + for (size_t i = 0; i < tox_max_message_length() + 50; ++i) { + testStr += "a"; + } + + auto messages = messageProcessor.processOutgoingMessage(false, testStr); + + // The message processor should split our messages + QVERIFY(messages.size() == 2); +} + +/** + * @brief Tests behavior of the processor for incoming messages + */ +void TestMessageProcessor::testIncomingMessage() +{ + // Nothing too special happening on the incoming side if we aren't looking for self mentions + auto sharedParams = MessageProcessor::SharedParams(); + auto messageProcessor = MessageProcessor(sharedParams); + auto message = messageProcessor.processIncomingMessage(false, "test"); + + QVERIFY(message.isAction == false); + QVERIFY(message.content == "test"); + QVERIFY(message.timestamp.isValid()); +} + +QTEST_GUILESS_MAIN(TestMessageProcessor) +#include "messageprocessor_test.moc"