From 7474c6d8acff9347786b569a8cf1badb54c4b1ae Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Tue, 12 Nov 2019 20:12:05 -0800 Subject: [PATCH 1/6] feat(messages): Multipacket message support * Introduced ToxExt and CoreExt abstraction * Along with interfaces for mocking and unit testing * Add "supportedExtensions" concept to Friend * Dispatch messages to CoreExt instead of Core when friend supports extended messages * Only split messages for core when extended messages are unavailable * Offline message engine/History not altered. Currently only valid for an existing session after extension negotiation has completed --- CMakeLists.txt | 2 + cmake/Dependencies.cmake | 7 + src/core/core.cpp | 27 ++++ src/core/core.h | 7 + src/core/coreext.cpp | 159 ++++++++++++++++++++ src/core/coreext.h | 145 ++++++++++++++++++ src/core/extension.h | 32 ++++ src/core/icoreextpacket.h | 68 +++++++++ src/model/friend.cpp | 11 ++ src/model/friend.h | 8 +- src/model/friendmessagedispatcher.cpp | 40 ++++- src/model/friendmessagedispatcher.h | 6 +- src/model/groupmessagedispatcher.cpp | 4 +- src/model/message.cpp | 26 +++- src/model/message.h | 9 +- src/nexus.cpp | 2 + src/widget/widget.cpp | 42 +++++- src/widget/widget.h | 4 + test/model/friendmessagedispatcher_test.cpp | 66 +++++++- test/model/messageprocessor_test.cpp | 24 +-- 20 files changed, 659 insertions(+), 30 deletions(-) create mode 100644 src/core/coreext.cpp create mode 100644 src/core/coreext.h create mode 100644 src/core/extension.h create mode 100644 src/core/icoreextpacket.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1aa47bcb1..152a3e36a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -218,6 +218,8 @@ set(${PROJECT_NAME}_SOURCES src/chatlog/textformatter.h src/core/coreav.cpp src/core/coreav.h + src/core/coreext.cpp + src/core/coreext.h src/core/core.cpp src/core/corefile.cpp src/core/corefile.h diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index fd402eba9..09606accd 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -33,6 +33,9 @@ find_package(Qt5Test REQUIRED) find_package(Qt5Widgets REQUIRED) find_package(Qt5Xml REQUIRED) +find_package(ToxExt REQUIRED) +find_package(ToxExtensionMessages REQUIRED) + function(add_dependency) set(ALL_LIBRARIES ${ALL_LIBRARIES} ${ARGN} PARENT_SCOPE) endfunction() @@ -47,6 +50,10 @@ add_dependency( Qt5::Widgets Qt5::Xml) +add_dependency( + ToxExt::ToxExt + ToxExtensionMessages::ToxExtensionMessages) + include(CMakeParseArguments) function(search_dependency pkg) diff --git a/src/core/core.cpp b/src/core/core.cpp index 3aa6c2169..3ac5ef347 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -21,6 +21,8 @@ #include "core.h" #include "coreav.h" #include "corefile.h" + +#include "src/core/coreext.h" #include "src/core/dhtserver.h" #include "src/core/icoresettings.h" #include "src/core/toxlogger.h" @@ -515,6 +517,7 @@ void Core::registerCallbacks(Tox* tox) tox_callback_conference_peer_list_changed(tox, onGroupPeerListChange); tox_callback_conference_peer_name(tox, onGroupPeerNameChange); tox_callback_conference_title(tox, onGroupTitleChange); + tox_callback_friend_lossless_packet(tox, onLosslessPacket); } /** @@ -639,6 +642,9 @@ ToxCorePtr Core::makeToxCore(const QByteArray& savedata, const ICoreSettings* co return {}; } + core->ext = CoreExt::makeCoreExt(core->tox.get()); + connect(core.get(), &Core::friendStatusChanged, core->ext.get(), &CoreExt::onFriendStatusChanged); + registerCallbacks(core->tox.get()); // connect the thread with the Core @@ -714,6 +720,16 @@ QMutex &Core::getCoreLoopLock() const return coreLoopLock; } +const CoreExt* Core::getExt() const +{ + return ext.get(); +} + +CoreExt* Core::getExt() +{ + return ext.get(); +} + /* Using the now commented out statements in checkConnection(), I watched how * many ticks disconnects-after-initial-connect lasted. Out of roughly 15 trials, * 5 disconnected; 4 were DCd for less than 20 ticks, while the 5th was ~50 ticks. @@ -734,6 +750,7 @@ void Core::process() static int tolerance = CORE_DISCONNECT_TOLERANCE; tox_iterate(tox.get(), this); + ext->process(); #ifdef DEBUG // we want to see the debug messages immediately @@ -988,6 +1005,16 @@ void Core::onGroupTitleChange(Tox*, uint32_t groupId, uint32_t peerId, const uin emit core->groupTitleChanged(groupId, author, ToxString(cTitle, length).getQString()); } +/** + * @brief Handling of custom lossless packets received by toxcore. Currently only used to forward toxext packets to CoreExt + */ +void Core::onLosslessPacket(Tox*, uint32_t friendId, + const uint8_t* data, size_t length, void* vCore) +{ + Core* core = static_cast(vCore); + core->ext->onLosslessPacket(friendId, data, length); +} + void Core::onReadReceiptCallback(Tox*, uint32_t friendId, uint32_t receipt, void* core) { emit static_cast(core)->receiptRecieved(friendId, ReceiptNum{receipt}); diff --git a/src/core/core.h b/src/core/core.h index 1ccbdd07c..fd622cb27 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -44,6 +44,7 @@ class CoreAV; class CoreFile; +class CoreExt; class IAudioControl; class ICoreSettings; class GroupInvite; @@ -79,6 +80,8 @@ public: Tox* getTox() const; QMutex& getCoreLoopLock() const; + const CoreExt* getExt() const; + CoreExt* getExt(); ~Core(); static const QString TOX_EXT; @@ -215,6 +218,9 @@ private: size_t length, void* core); static void onGroupTitleChange(Tox* tox, uint32_t groupId, uint32_t peerId, const uint8_t* cTitle, size_t length, void* vCore); + + static void onLosslessPacket(Tox* tox, uint32_t friendId, + const uint8_t* data, size_t length, void* core); static void onReadReceiptCallback(Tox* tox, uint32_t friendId, uint32_t receipt, void* core); void sendGroupMessageWithType(int groupId, const QString& message, Tox_Message_Type type); @@ -249,6 +255,7 @@ private: std::unique_ptr file; CoreAV* av = nullptr; + std::unique_ptr ext; QTimer* toxTimer = nullptr; // recursive, since we might call our own functions mutable QMutex coreLoopLock{QMutex::Recursive}; diff --git a/src/core/coreext.cpp b/src/core/coreext.cpp new file mode 100644 index 000000000..0d9c7fe5d --- /dev/null +++ b/src/core/coreext.cpp @@ -0,0 +1,159 @@ +/* + Copyright © 2019-2020 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 "coreext.h" +#include "toxstring.h" + +#include +#include +#include + +#include +#include + +extern "C" { +#include +#include +} + +std::unique_ptr CoreExt::makeCoreExt(Tox* core) { + auto toxExtPtr = toxext_init(core); + if (!toxExtPtr) { + return nullptr; + } + + auto toxExt = ExtensionPtr(toxExtPtr, toxext_free); + return std::unique_ptr(new CoreExt(std::move(toxExt))); +} + +CoreExt::CoreExt(ExtensionPtr toxExt_) + : toxExt(std::move(toxExt_)) + , toxExtMessages(nullptr, nullptr) +{ + toxExtMessages = ExtensionPtr( + tox_extension_messages_register( + toxExt.get(), + CoreExt::onExtendedMessageReceived, + CoreExt::onExtendedMessageReceipt, + CoreExt::onExtendedMessageNegotiation, + this, + TOX_EXTENSION_MESSAGES_DEFAULT_MAX_RECEIVING_MESSAGE_SIZE), + tox_extension_messages_free); +} + +void CoreExt::process() +{ + toxext_iterate(toxExt.get()); +} + +void CoreExt::onLosslessPacket(uint32_t friendId, const uint8_t* data, size_t length) +{ + if (is_toxext_packet(data, length)) { + toxext_handle_lossless_custom_packet(toxExt.get(), friendId, data, length); + } +} + +CoreExt::Packet::Packet( + ToxExtPacketList* packetList, + ToxExtensionMessages* toxExtMessages, + uint32_t friendId, + PacketPassKey) + : toxExtMessages(toxExtMessages) + , packetList(packetList) + , friendId(friendId) +{} + +std::unique_ptr CoreExt::getPacket(uint32_t friendId) +{ + return std::unique_ptr(new Packet( + toxext_packet_list_create(toxExt.get(), friendId), + toxExtMessages.get(), + friendId, + PacketPassKey{})); +} + +uint64_t CoreExt::Packet::addExtendedMessage(QString message) +{ + if (hasBeenSent) { + assert(false); + qWarning() << "Invalid use of CoreExt::Packet"; + // Hope that UINT64_MAX will never collide with an actual receipt num + // that we care about + return UINT64_MAX; + } + + ToxString toxString(message); + Tox_Extension_Messages_Error err; + + return tox_extension_messages_append( + toxExtMessages, + packetList, + toxString.data(), + toxString.size(), + friendId, + &err); +} + +bool CoreExt::Packet::send() +{ + auto ret = toxext_send(packetList); + if (ret != TOXEXT_SUCCESS) { + qWarning() << "Failed to send packet"; + } + // Indicate we've sent the packet even on failure since our packetlist will + // be invalid no matter what + hasBeenSent = true; + return ret == TOXEXT_SUCCESS; +} + +void CoreExt::onFriendStatusChanged(uint32_t friendId, Status::Status status) +{ + const auto prevStatusIt = currentStatuses.find(friendId); + const auto prevStatus = prevStatusIt == currentStatuses.end() + ? Status::Status::Offline : prevStatusIt->second; + + currentStatuses[friendId] = status; + + // Does not depend on prevStatus since prevStatus could be newly + // constructed. In this case we should still ensure the rest of the system + // knows there is no extension support + if (status == Status::Status::Offline) { + emit extendedMessageSupport(friendId, false); + } else if (prevStatus == Status::Status::Offline) { + tox_extension_messages_negotiate(toxExtMessages.get(), friendId); + } +} + +void CoreExt::onExtendedMessageReceived(uint32_t friendId, const uint8_t* data, size_t size, void* userData) +{ + QString msg = ToxString(data, size).getQString(); + emit static_cast(userData)->extendedMessageReceived(friendId, msg); +} + +void CoreExt::onExtendedMessageReceipt(uint32_t friendId, uint64_t receiptId, void* userData) +{ + emit static_cast(userData)->extendedReceiptReceived(friendId, receiptId); +} + +void CoreExt::onExtendedMessageNegotiation(uint32_t friendId, bool compatible, uint64_t maxMessageSize, void* userData) +{ + auto coreExt = static_cast(userData); + emit coreExt->extendedMessageSupport(friendId, compatible); +} + diff --git a/src/core/coreext.h b/src/core/coreext.h new file mode 100644 index 000000000..41a937cb8 --- /dev/null +++ b/src/core/coreext.h @@ -0,0 +1,145 @@ +/* + Copyright © 2019-2020 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 . +*/ + +#pragma once + +#include "src/model/status.h" +#include "icoreextpacket.h" + +#include +#include + +#include +#include +#include + +struct Tox; +struct ToxExt; +struct ToxExtensionMessages; +struct ToxExtPacketList; + +/** + * Bridge between the toxext library and the rest of qTox. + */ +class CoreExt : public QObject, public ICoreExtPacketAllocator +{ + Q_OBJECT +private: + // PassKey idiom to prevent others from making PacketBuilders + struct PacketPassKey {}; +public: + + /** + * @brief Creates a CoreExt instance. Using a pointer here makes our + * registrations with extensions significantly easier to manage + * + * @param[in] pointer to core tox instance + * @return CoreExt on success, nullptr on failure + */ + static std::unique_ptr makeCoreExt(Tox* core); + + // We do registration with our own pointer, need to ensure we're in a stable location + CoreExt(CoreExt const& other) = delete; + CoreExt(CoreExt&& other) = delete; + CoreExt& operator=(CoreExt const& other) = delete; + CoreExt& operator=(CoreExt&& other) = delete; + + /** + * @brief Periodic service function + */ + void process(); + + /** + * @brief Handles extension related lossless packets + * @param[in] friendId Core id of friend + * @param[in] data Packet data + * @param[in] length Length of packet data + */ + void onLosslessPacket(uint32_t friendId, const uint8_t* data, size_t length); + + /** + * See documentation of ICoreExtPacket + */ + class Packet : public ICoreExtPacket + { + public: + /** + * @brief Internal constructor for a packet. + */ + Packet( + ToxExtPacketList* packetList, + ToxExtensionMessages* toxExtMessages, + uint32_t friendId, + PacketPassKey); + + // Delete copy constructor, we shouldn't be able to copy + Packet(Packet const& other) = delete; + + Packet(Packet&& other) + { + toxExtMessages = other.toxExtMessages; + packetList = other.packetList; + friendId = other.friendId; + hasBeenSent = other.hasBeenSent; + other.toxExtMessages = nullptr; + other.packetList = nullptr; + other.friendId = 0; + other.hasBeenSent = false; + } + + uint64_t addExtendedMessage(QString message) override; + + bool send() override; + private: + bool hasBeenSent = false; + // Note: non-owning pointer + ToxExtensionMessages* toxExtMessages; + // Note: packetList is freed on send() call + ToxExtPacketList* packetList; + uint32_t friendId; + }; + + std::unique_ptr getPacket(uint32_t friendId) override; + +signals: + void extendedMessageReceived(uint32_t friendId, const QString& message); + void extendedReceiptReceived(uint32_t friendId, uint64_t receiptId); + void extendedMessageSupport(uint32_t friendId, bool supported); + +public slots: + void onFriendStatusChanged(uint32_t friendId, Status::Status status); + +private: + + static void onExtendedMessageReceived(uint32_t friendId, const uint8_t* data, size_t size, void* userData); + static void onExtendedMessageReceipt(uint32_t friendId, uint64_t receiptId, void* userData); + static void onExtendedMessageNegotiation(uint32_t friendId, bool compatible, uint64_t maxMessageSize, void* userData); + + // A little extra cost to hide the deleters, but this lets us fwd declare + // and prevent any extension headers from leaking out to the rest of the + // system + template + using ExtensionPtr = std::unique_ptr; + + CoreExt(ExtensionPtr toxExt); + + std::unordered_map currentStatuses; + ExtensionPtr toxExt; + ExtensionPtr toxExtMessages; +}; diff --git a/src/core/extension.h b/src/core/extension.h new file mode 100644 index 000000000..0a9785313 --- /dev/null +++ b/src/core/extension.h @@ -0,0 +1,32 @@ +/* + 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 . +*/ + +#pragma once + +#include + +// Do not use enum class because we use these as indexes frequently (see ExtensionSet) +struct ExtensionType +{ + enum { + messages, + max + }; +}; +using ExtensionSet = std::bitset; diff --git a/src/core/icoreextpacket.h b/src/core/icoreextpacket.h new file mode 100644 index 000000000..efa344a01 --- /dev/null +++ b/src/core/icoreextpacket.h @@ -0,0 +1,68 @@ +/* + 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 . +*/ + +#pragma once + +#include + +#include +#include + +/** + * Abstraction around the toxext packet. The toxext flow is to allow several extensions + * to tack onto the same packet before sending it to avoid needing the toxext overhead + * for every single extension. This abstraction models a toxext packet list. + * + * Intent is to retrieve a ICoreExtPacket from an ICoreExtPacketAllocator, append all + * relevant extension data, and then finally send the packet. After sending the packet + * is no longer guaranteed to be valid. + */ +class ICoreExtPacket +{ +public: + virtual ~ICoreExtPacket() = default; + + /** + * @brief Adds message to packet + * @return Extended message receipt, UINT64_MAX on failure + * @note Any other extensions related to this message have to be added + * _before_ the message itself + */ + virtual uint64_t addExtendedMessage(QString message) = 0; + + /** + * @brief Consumes the packet constructed with PacketBuilder packet and + * sends it to toxext + */ + virtual bool send() = 0; +}; + +/** + * Provider of toxext packets + */ +class ICoreExtPacketAllocator +{ +public: + virtual ~ICoreExtPacketAllocator() = default; + + /** + * @brief Gets a new packet builder for friend with core friend id friendId + */ + virtual std::unique_ptr getPacket(uint32_t friendId) = 0; +}; diff --git a/src/model/friend.cpp b/src/model/friend.cpp index c34058e18..a2e9ffebf 100644 --- a/src/model/friend.cpp +++ b/src/model/friend.cpp @@ -173,3 +173,14 @@ bool Friend::useHistory() const { return true; } + +void Friend::setExtendedMessageSupport(bool supported) +{ + supportedExtensions[ExtensionType::messages] = supported; + emit extensionSupportChanged(supportedExtensions); +} + +ExtensionSet Friend::getSupportedExtensions() const +{ + return supportedExtensions; +} diff --git a/src/model/friend.h b/src/model/friend.h index fa6481677..4db5b91e6 100644 --- a/src/model/friend.h +++ b/src/model/friend.h @@ -21,6 +21,7 @@ #include "contact.h" #include "src/core/core.h" +#include "src/core/extension.h" #include "src/core/toxid.h" #include "src/core/contactid.h" #include "src/model/status.h" @@ -50,20 +51,24 @@ public: uint32_t getId() const override; const ContactId& getPersistentId() const override; + void finishNegotiation(); void setStatus(Status::Status s); Status::Status getStatus() const; bool useHistory() const final; + void setExtendedMessageSupport(bool supported); + ExtensionSet getSupportedExtensions() const; + signals: void nameChanged(const ToxPk& friendId, const QString& name); void aliasChanged(const ToxPk& friendId, QString alias); void statusChanged(const ToxPk& friendId, Status::Status status); void onlineOfflineChanged(const ToxPk& friendId, bool isOnline); void statusMessageChanged(const ToxPk& friendId, const QString& message); + void extensionSupportChanged(ExtensionSet extensions); void loadChatHistory(); public slots: - private: QString userName; QString userAlias; @@ -72,4 +77,5 @@ private: uint32_t friendId; bool hasNewEvents; Status::Status friendStatus; + ExtensionSet supportedExtensions; }; diff --git a/src/model/friendmessagedispatcher.cpp b/src/model/friendmessagedispatcher.cpp index 9a7a10cc6..c725e79ab 100644 --- a/src/model/friendmessagedispatcher.cpp +++ b/src/model/friendmessagedispatcher.cpp @@ -21,7 +21,6 @@ #include "src/persistence/settings.h" #include "src/model/status.h" - namespace { /** @@ -43,12 +42,15 @@ bool sendMessageToCore(ICoreFriendMessageSender& messageSender, const Friend& f, } } // namespace + FriendMessageDispatcher::FriendMessageDispatcher(Friend& f_, MessageProcessor processor_, - ICoreFriendMessageSender& messageSender_) + ICoreFriendMessageSender& messageSender_, + ICoreExtPacketAllocator& coreExtPacketAllocator_) : f(f_) , messageSender(messageSender_) , offlineMsgEngine(&f_, &messageSender_) , processor(std::move(processor_)) + , coreExtPacketAllocator(coreExtPacketAllocator_) { connect(&f, &Friend::onlineOfflineChanged, this, &FriendMessageDispatcher::onFriendOnlineOfflineChanged); } @@ -61,7 +63,9 @@ FriendMessageDispatcher::sendMessage(bool isAction, const QString& content) { const auto firstId = nextMessageId; auto lastId = nextMessageId; - for (const auto& message : processor.processOutgoingMessage(isAction, content)) { + auto supportedExtensions = f.getSupportedExtensions(); + const bool needsSplit = !supportedExtensions[ExtensionType::messages]; + for (const auto& message : processor.processOutgoingMessage(isAction, content, needsSplit)) { auto messageId = nextMessageId++; lastId = messageId; auto onOfflineMsgComplete = [this, messageId] { emit this->messageComplete(messageId); }; @@ -70,8 +74,22 @@ FriendMessageDispatcher::sendMessage(bool isAction, const QString& content) bool messageSent = false; + // NOTE: This branch is getting a little hairy but will be cleaned up in the following commit if (Status::isOnline(f.getStatus())) { - messageSent = sendMessageToCore(messageSender, f, message, receipt); + + // Action messages go over the regular mesage channel so we cannot use extensions with them + if (supportedExtensions[ExtensionType::messages] && !isAction) { + auto packet = coreExtPacketAllocator.getPacket(f.getId()); + + if (supportedExtensions[ExtensionType::messages]) { + // NOTE: Dirty hack to get extensions working that will be fixed in the following commit + receipt.get() = packet->addExtendedMessage(message.content); + } + + messageSent = packet->send(); + } else { + messageSent = sendMessageToCore(messageSender, f, message, receipt); + } } if (!messageSent) { @@ -92,7 +110,7 @@ FriendMessageDispatcher::sendMessage(bool isAction, const QString& content) */ void FriendMessageDispatcher::onMessageReceived(bool isAction, const QString& content) { - emit this->messageReceived(f.getPublicKey(), processor.processIncomingMessage(isAction, content)); + emit this->messageReceived(f.getPublicKey(), processor.processIncomingCoreMessage(isAction, content)); } /** @@ -104,6 +122,18 @@ void FriendMessageDispatcher::onReceiptReceived(ReceiptNum receipt) offlineMsgEngine.onReceiptReceived(receipt); } +void FriendMessageDispatcher::onExtMessageReceived(const QString& content) +{ + auto message = processor.processIncomingExtMessage(content); + emit this->messageReceived(f.getPublicKey(), message); +} + +void FriendMessageDispatcher::onExtReceiptReceived(uint64_t receiptId) +{ + // NOTE: Reusing ReceiptNum is a dirty hack that will be cleaned up in the following commit + offlineMsgEngine.onReceiptReceived(ReceiptNum(receiptId)); +} + /** * @brief Handles status change for friend * @note Parameters just to fit slot api diff --git a/src/model/friendmessagedispatcher.h b/src/model/friendmessagedispatcher.h index 66011447d..9cd966e21 100644 --- a/src/model/friendmessagedispatcher.h +++ b/src/model/friendmessagedispatcher.h @@ -35,18 +35,22 @@ class FriendMessageDispatcher : public IMessageDispatcher Q_OBJECT public: FriendMessageDispatcher(Friend& f, MessageProcessor processor, - ICoreFriendMessageSender& messageSender); + ICoreFriendMessageSender& messageSender, + ICoreExtPacketAllocator& coreExt); std::pair sendMessage(bool isAction, const QString& content) override; void onMessageReceived(bool isAction, const QString& content); void onReceiptReceived(ReceiptNum receipt); + void onExtMessageReceived(const QString& message); + void onExtReceiptReceived(uint64_t receiptId); void clearOutgoingMessages(); private slots: void onFriendOnlineOfflineChanged(const ToxPk& key, bool isOnline); private: Friend& f; + ICoreExtPacketAllocator& coreExtPacketAllocator; DispatchedMessageId nextMessageId = DispatchedMessageId(0); ICoreFriendMessageSender& messageSender; diff --git a/src/model/groupmessagedispatcher.cpp b/src/model/groupmessagedispatcher.cpp index f62327bbc..653c6fad1 100644 --- a/src/model/groupmessagedispatcher.cpp +++ b/src/model/groupmessagedispatcher.cpp @@ -41,7 +41,7 @@ GroupMessageDispatcher::sendMessage(bool isAction, QString const& content) const auto firstMessageId = nextMessageId; auto lastMessageId = firstMessageId; - for (auto const& message : processor.processOutgoingMessage(isAction, content)) { + for (auto const& message : processor.processOutgoingMessage(isAction, content, true /*needsSplit*/)) { auto messageId = nextMessageId++; lastMessageId = messageId; if (group.getPeersCount() != 1) { @@ -84,5 +84,5 @@ void GroupMessageDispatcher::onMessageReceived(const ToxPk& sender, bool isActio return; } - emit messageReceived(sender, processor.processIncomingMessage(isAction, content)); + emit messageReceived(sender, processor.processIncomingCoreMessage(isAction, content)); } diff --git a/src/model/message.cpp b/src/model/message.cpp index 6aa1abc92..9ff555757 100644 --- a/src/model/message.cpp +++ b/src/model/message.cpp @@ -21,6 +21,8 @@ #include "friend.h" #include "src/core/core.h" +#include + void MessageProcessor::SharedParams::onUserNameSet(const QString& username) { QString sanename = username; @@ -49,11 +51,14 @@ MessageProcessor::MessageProcessor(const MessageProcessor::SharedParams& sharedP /** * @brief Converts an outgoing message into one (or many) sanitized Message(s) */ -std::vector MessageProcessor::processOutgoingMessage(bool isAction, QString const& content) +std::vector MessageProcessor::processOutgoingMessage(bool isAction, QString const& content, bool needsSplit) { std::vector ret; - QStringList splitMsgs = Core::splitMessage(content); + const auto splitMsgs = needsSplit + ? Core::splitMessage(content) + : QStringList({content}); + ret.reserve(splitMsgs.size()); QDateTime timestamp = QDateTime::currentDateTime(); @@ -69,11 +74,10 @@ std::vector MessageProcessor::processOutgoingMessage(bool isAction, QSt return ret; } - /** * @brief Converts an incoming message into a sanitized Message */ -Message MessageProcessor::processIncomingMessage(bool isAction, QString const& message) +Message MessageProcessor::processIncomingCoreMessage(bool isAction, QString const& message) { QDateTime timestamp = QDateTime::currentDateTime(); auto ret = Message{}; @@ -109,3 +113,17 @@ Message MessageProcessor::processIncomingMessage(bool isAction, QString const& m return ret; } + +Message MessageProcessor::processIncomingExtMessage(const QString& content) +{ + // Note: detectingMentions not implemented here since mentions are only + // currently useful in group messages which do not support extensions. If we + // were to support mentions we would probably want to do something more + // intelligent anyways + assert(detectingMentions == false); + auto message = Message(); + message.timestamp = QDateTime::currentDateTime(); + message.content = content; + + return message; +} diff --git a/src/model/message.h b/src/model/message.h index 059ce64c6..66eb51339 100644 --- a/src/model/message.h +++ b/src/model/message.h @@ -19,6 +19,8 @@ #pragma once +#include "src/core/coreext.h" + #include #include #include @@ -26,6 +28,7 @@ #include class Friend; +class CoreExt; // NOTE: This could be extended in the future to handle all text processing (see // ChatMessage::createChatMessage) @@ -89,9 +92,9 @@ public: MessageProcessor(const SharedParams& sharedParams); - std::vector processOutgoingMessage(bool isAction, QString const& content); - - Message processIncomingMessage(bool isAction, QString const& message); + std::vector processOutgoingMessage(bool isAction, const QString& content, bool needsSplit); + Message processIncomingCoreMessage(bool isAction, const QString& content); + Message processIncomingExtMessage(const QString& content); /** * @brief Enables mention detection in the processor diff --git a/src/nexus.cpp b/src/nexus.cpp index 5b6ba6943..0db1919d9 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -108,6 +108,8 @@ void Nexus::start() qRegisterMetaType("GroupInvite"); qRegisterMetaType("ReceiptNum"); qRegisterMetaType("RowId"); + qRegisterMetaType("uint64_t"); + qRegisterMetaType("ExtensionSet"); qApp->setQuitOnLastWindowClosed(false); diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index e5ab011c1..cf36af646 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -695,6 +695,13 @@ void Widget::onCoreChanged(Core& core) connect(&core, &Core::friendTypingChanged, this, &Widget::onFriendTypingChanged); connect(&core, &Core::groupSentFailed, this, &Widget::onGroupSendFailed); connect(&core, &Core::usernameSet, this, &Widget::refreshPeerListsLocal); + + auto coreExt = core.getExt(); + + connect(coreExt, &CoreExt::extendedMessageReceived, this, &Widget::onFriendExtMessageReceived); + connect(coreExt, &CoreExt::extendedReceiptReceived, this, &Widget::onExtReceiptReceived); + connect(coreExt, &CoreExt::extendedMessageSupport, this, &Widget::onExtendedMessageSupport); + connect(this, &Widget::statusSet, &core, &Core::setStatus); connect(this, &Widget::friendRequested, &core, &Core::requestFriendship); connect(this, &Widget::friendRequestAccepted, &core, &Core::acceptFriendRequest); @@ -1146,7 +1153,7 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk) auto messageProcessor = MessageProcessor(sharedMessageProcessorParams); auto friendMessageDispatcher = - std::make_shared(*newfriend, std::move(messageProcessor), *core); + std::make_shared(*newfriend, std::move(messageProcessor), *core, *core->getExt()); // Note: We do not have to connect the message dispatcher signals since // ChatHistory hooks them up in a very specific order @@ -1401,6 +1408,39 @@ void Widget::onReceiptReceived(int friendId, ReceiptNum receipt) friendMessageDispatchers[f->getPublicKey()]->onReceiptReceived(receipt); } +void Widget::onExtendedMessageSupport(uint32_t friendNumber, bool compatible) +{ + const auto& friendKey = FriendList::id2Key(friendNumber); + Friend* f = FriendList::findFriend(friendKey); + if (!f) { + return; + } + + f->setExtendedMessageSupport(compatible); +} + +void Widget::onFriendExtMessageReceived(uint32_t friendNumber, const QString& message) +{ + const auto& friendKey = FriendList::id2Key(friendNumber); + Friend* f = FriendList::findFriend(friendKey); + if (!f) { + return; + } + + friendMessageDispatchers[f->getPublicKey()]->onExtMessageReceived(message); +} + +void Widget::onExtReceiptReceived(uint32_t friendNumber, uint64_t receiptId) +{ + const auto& friendKey = FriendList::id2Key(friendNumber); + Friend* f = FriendList::findFriend(friendKey); + if (!f) { + return; + } + + friendMessageDispatchers[f->getPublicKey()]->onExtReceiptReceived(receiptId); +} + void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog) { const ToxPk& friendPk = frnd->getPublicKey(); diff --git a/src/widget/widget.h b/src/widget/widget.h index ed6184b35..d000e5625 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -172,6 +172,9 @@ public slots: void onFriendAliasChanged(const ToxPk& friendId, const QString& alias); void onFriendMessageReceived(uint32_t friendnumber, const QString& message, bool isAction); void onReceiptReceived(int friendId, ReceiptNum receipt); + void onExtendedMessageSupport(uint32_t friendNumber, bool supported); + void onFriendExtMessageReceived(uint32_t friendNumber, const QString& message); + void onExtReceiptReceived(uint32_t friendNumber, uint64_t receiptId); void onFriendRequestReceived(const ToxPk& friendPk, const QString& message); void onFileReceiveRequested(const ToxFile& file); void onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title); @@ -344,6 +347,7 @@ private: QMap> friendChatLogs; QMap> friendChatrooms; QMap chatForms; + std::map> negotiateTimers; QMap groupWidgets; QMap> groupMessageDispatchers; diff --git a/test/model/friendmessagedispatcher_test.cpp b/test/model/friendmessagedispatcher_test.cpp index 07c724549..665375944 100644 --- a/test/model/friendmessagedispatcher_test.cpp +++ b/test/model/friendmessagedispatcher_test.cpp @@ -28,6 +28,45 @@ #include +class MockCoreExtPacket : public ICoreExtPacket +{ +public: + + MockCoreExtPacket(uint64_t& numSentMessages, uint64_t& currentReceiptId) + : numSentMessages(numSentMessages) + , currentReceiptId(currentReceiptId) + {} + + uint64_t addExtendedMessage(QString message) override + { + this->message = message; + return currentReceiptId++; + } + + bool send() override + { + numSentMessages++; + return true; + } + + uint64_t& numSentMessages; + uint64_t& currentReceiptId; + QDateTime senderTimestamp; + QString message; +}; + +class MockCoreExtPacketAllocator : public ICoreExtPacketAllocator +{ +public: + std::unique_ptr getPacket(uint32_t friendId) override + { + return std::unique_ptr(new MockCoreExtPacket(numSentMessages, currentReceiptId)); + } + + uint64_t numSentMessages; + uint64_t currentReceiptId; +}; + class MockFriendMessageSender : public ICoreFriendMessageSender { public: @@ -69,6 +108,8 @@ private slots: void testMessageSending(); void testOfflineMessages(); void testFailedMessage(); + void testNegotiationFailure(); + void testNegotiationSuccess(); void onMessageSent(DispatchedMessageId id, Message message) { @@ -93,6 +134,7 @@ private: // All unique_ptrs to make construction/init() easier to manage std::unique_ptr f; std::unique_ptr messageSender; + std::unique_ptr coreExtPacketAllocator; std::unique_ptr sharedProcessorParams; std::unique_ptr messageProcessor; std::unique_ptr friendMessageDispatcher; @@ -110,11 +152,12 @@ void TestFriendMessageDispatcher::init() f = std::unique_ptr(new Friend(0, ToxPk())); f->setStatus(Status::Status::Online); messageSender = std::unique_ptr(new MockFriendMessageSender()); + coreExtPacketAllocator = std::unique_ptr(new MockCoreExtPacketAllocator()); sharedProcessorParams = std::unique_ptr(new MessageProcessor::SharedParams()); messageProcessor = std::unique_ptr(new MessageProcessor(*sharedProcessorParams)); friendMessageDispatcher = std::unique_ptr( - new FriendMessageDispatcher(*f, *messageProcessor, *messageSender)); + new FriendMessageDispatcher(*f, *messageProcessor, *messageSender, *coreExtPacketAllocator)); connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageSent, this, &TestFriendMessageDispatcher::onMessageSent); @@ -227,5 +270,26 @@ void TestFriendMessageDispatcher::testFailedMessage() QVERIFY(messageSender->numSentMessages == 1); } +void TestFriendMessageDispatcher::testNegotiationFailure() +{ + friendMessageDispatcher->sendMessage(false, "test"); + + QVERIFY(messageSender->numSentMessages == 1); + QVERIFY(coreExtPacketAllocator->numSentMessages == 0); +} + +void TestFriendMessageDispatcher::testNegotiationSuccess() +{ + f->setExtendedMessageSupport(true); + + friendMessageDispatcher->sendMessage(false, "test"); + + QVERIFY(coreExtPacketAllocator->numSentMessages == 1); + + friendMessageDispatcher->sendMessage(false, "test"); + QVERIFY(coreExtPacketAllocator->numSentMessages == 2); + QVERIFY(messageSender->numSentMessages == 0); +} + QTEST_GUILESS_MAIN(TestFriendMessageDispatcher) #include "friendmessagedispatcher_test.moc" diff --git a/test/model/messageprocessor_test.cpp b/test/model/messageprocessor_test.cpp index b6438185d..9041e7503 100644 --- a/test/model/messageprocessor_test.cpp +++ b/test/model/messageprocessor_test.cpp @@ -66,55 +66,55 @@ void TestMessageProcessor::testSelfMention() for (const auto& str : {testUserName, testToxPk}) { // Using my name or public key should match - auto processedMessage = messageProcessor.processIncomingMessage(false, str % " hi"); + auto processedMessage = messageProcessor.processIncomingCoreMessage(false, str % " hi"); QVERIFY(messageHasSelfMention(processedMessage)); // Action messages should match too - processedMessage = messageProcessor.processIncomingMessage(true, str % " hi"); + processedMessage = messageProcessor.processIncomingCoreMessage(true, str % " hi"); QVERIFY(messageHasSelfMention(processedMessage)); // Too much text shouldn't match - processedMessage = messageProcessor.processIncomingMessage(false, str % "2"); + processedMessage = messageProcessor.processIncomingCoreMessage(false, str % "2"); QVERIFY(!messageHasSelfMention(processedMessage)); // Unless it's a colon - processedMessage = messageProcessor.processIncomingMessage(false, str % ": test"); + processedMessage = messageProcessor.processIncomingCoreMessage(false, str % ": test"); QVERIFY(messageHasSelfMention(processedMessage)); // remove last character QString chopped = str; chopped.chop(1); // Too little text shouldn't match - processedMessage = messageProcessor.processIncomingMessage(false, chopped); + processedMessage = messageProcessor.processIncomingCoreMessage(false, chopped); QVERIFY(!messageHasSelfMention(processedMessage)); // make lower case QString lower = QString(str).toLower(); // The regex should be case insensitive - processedMessage = messageProcessor.processIncomingMessage(false, lower % " hi"); + processedMessage = messageProcessor.processIncomingCoreMessage(false, lower % " hi"); QVERIFY(messageHasSelfMention(processedMessage)); } // New user name changes should be detected sharedParams.onUserNameSet("NewUserName"); - auto processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi"); + auto processedMessage = messageProcessor.processIncomingCoreMessage(false, "NewUserName: hi"); QVERIFY(messageHasSelfMention(processedMessage)); // Special characters should be removed sharedParams.onUserNameSet("New\nUserName"); - processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi"); + processedMessage = messageProcessor.processIncomingCoreMessage(false, "NewUserName: hi"); QVERIFY(messageHasSelfMention(processedMessage)); // Regression tests for: https://github.com/qTox/qTox/issues/2119 { // Empty usernames should not match sharedParams.onUserNameSet(""); - processedMessage = messageProcessor.processIncomingMessage(false, ""); + processedMessage = messageProcessor.processIncomingCoreMessage(false, ""); QVERIFY(!messageHasSelfMention(processedMessage)); // Empty usernames matched on everything, ensure this is not the case - processedMessage = messageProcessor.processIncomingMessage(false, "a"); + processedMessage = messageProcessor.processIncomingCoreMessage(false, "a"); QVERIFY(!messageHasSelfMention(processedMessage)); } } @@ -133,7 +133,7 @@ void TestMessageProcessor::testOutgoingMessage() testStr += "a"; } - auto messages = messageProcessor.processOutgoingMessage(false, testStr); + auto messages = messageProcessor.processOutgoingMessage(false, testStr, true /*needsSplit*/); // The message processor should split our messages QVERIFY(messages.size() == 2); @@ -147,7 +147,7 @@ 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"); + auto message = messageProcessor.processIncomingCoreMessage(false, "test"); QVERIFY(message.isAction == false); QVERIFY(message.content == "test"); From 5f5f612841a89d575239702edcff499a5ff6019d Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Sun, 17 Nov 2019 02:19:57 -0800 Subject: [PATCH 2/6] feat(messages): History and offline message support for extended messages * Added new negotiating friend state to allow delayed sending of offline messages * Added ability to flag currently outgoing message as broken in UI * Reworked OfflineMsgEngine to support multiple receipt types * Moved resending logic out of the OfflineMsgEngine * Moved coordination of receipt and DispatchedMessageId into helper class usable for both ExtensionReceiptNum and ReceiptNum * Resending logic now has a failure case when the friend's extension set is lower than the required extensions needed for the message * When a user is known to be offline we do not allow use of any extensions * Added DB support for broken message reasons * Added DB support to tie an faux_offline_pending message to a required extension set --- img/status/negotiating.svg | 10 + img/status/negotiating_notification.svg | 32 +++ res.qrc | 2 + src/chatlog/chatmessage.cpp | 5 + src/chatlog/chatmessage.h | 1 + src/core/core.cpp | 1 + src/core/receiptnum.h | 3 + src/model/brokenmessagereason.h | 27 ++ src/model/chathistory.cpp | 61 ++++- src/model/chathistory.h | 6 + src/model/friend.cpp | 63 ++++- src/model/friend.h | 2 + src/model/friendmessagedispatcher.cpp | 159 +++++++----- src/model/friendmessagedispatcher.h | 7 + src/model/groupmessagedispatcher.cpp | 12 +- src/model/groupmessagedispatcher.h | 2 + src/model/imessagedispatcher.h | 15 ++ src/model/message.cpp | 8 +- src/model/message.h | 4 +- src/model/sessionchatlog.cpp | 23 ++ src/model/sessionchatlog.h | 1 + src/model/status.cpp | 9 +- src/model/status.h | 3 +- src/persistence/history.cpp | 262 ++++++++++---------- src/persistence/history.h | 13 +- src/persistence/offlinemsgengine.cpp | 107 +++----- src/persistence/offlinemsgengine.h | 84 +++++-- src/persistence/profile.cpp | 2 +- src/widget/form/chatform.cpp | 9 +- src/widget/form/chatform.h | 2 +- src/widget/form/genericchatform.cpp | 2 + src/widget/friendwidget.cpp | 2 + src/widget/widget.cpp | 56 +++-- src/widget/widget.h | 3 +- test/model/friendmessagedispatcher_test.cpp | 99 +++++++- test/model/messageprocessor_test.cpp | 9 +- test/persistence/dbschema_test.cpp | 25 +- test/persistence/offlinemsgengine_test.cpp | 245 +++++++++--------- 38 files changed, 916 insertions(+), 460 deletions(-) create mode 100644 img/status/negotiating.svg create mode 100644 img/status/negotiating_notification.svg create mode 100644 src/model/brokenmessagereason.h diff --git a/img/status/negotiating.svg b/img/status/negotiating.svg new file mode 100644 index 000000000..7d17422f5 --- /dev/null +++ b/img/status/negotiating.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/img/status/negotiating_notification.svg b/img/status/negotiating_notification.svg new file mode 100644 index 000000000..53bb34986 --- /dev/null +++ b/img/status/negotiating_notification.svg @@ -0,0 +1,32 @@ + +image/svg+xml \ No newline at end of file diff --git a/res.qrc b/res.qrc index 2a6a8bc5c..9d200bef1 100644 --- a/res.qrc +++ b/res.qrc @@ -22,6 +22,8 @@ img/status/away_notification.svg img/status/busy.svg img/status/busy_notification.svg + img/status/negotiating.svg + img/status/negotiating_notification.svg img/status/offline.svg img/status/offline_notification.svg img/status/online.svg diff --git a/src/chatlog/chatmessage.cpp b/src/chatlog/chatmessage.cpp index e14672eef..c6f8ff1a1 100644 --- a/src/chatlog/chatmessage.cpp +++ b/src/chatlog/chatmessage.cpp @@ -228,6 +228,11 @@ void ChatMessage::markAsDelivered(const QDateTime& time) replaceContent(2, new Timestamp(time, Settings::getInstance().getTimestampFormat(), baseFont)); } +void ChatMessage::markAsBroken() +{ + replaceContent(2, new Broken(Style::getImagePath("chatArea/error.svg"), QSize(16, 16))); +} + QString ChatMessage::toString() const { ChatLineContent* c = getContent(1); diff --git a/src/chatlog/chatmessage.h b/src/chatlog/chatmessage.h index cc3087b44..abdce7ee5 100644 --- a/src/chatlog/chatmessage.h +++ b/src/chatlog/chatmessage.h @@ -60,6 +60,7 @@ public: static ChatMessage::Ptr createBusyNotification(); void markAsDelivered(const QDateTime& time); + void markAsBroken(); QString toString() const; bool isAction() const; void setAsAction(); diff --git a/src/core/core.cpp b/src/core/core.cpp index 3ac5ef347..78f0a77ad 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -1096,6 +1096,7 @@ bool Core::sendMessageWithType(uint32_t friendId, const QString& message, Tox_Me int size = message.toUtf8().size(); auto maxSize = static_cast(tox_max_message_length()); if (size > maxSize) { + assert(false); qCritical() << "Core::sendMessageWithType called with message of size:" << size << "when max is:" << maxSize << ". Ignoring."; return false; diff --git a/src/core/receiptnum.h b/src/core/receiptnum.h index 9171c9fa6..4a4ad507a 100644 --- a/src/core/receiptnum.h +++ b/src/core/receiptnum.h @@ -26,3 +26,6 @@ using ReceiptNum = NamedType; Q_DECLARE_METATYPE(ReceiptNum) + +using ExtendedReceiptNum = NamedType; +Q_DECLARE_METATYPE(ExtendedReceiptNum); diff --git a/src/model/brokenmessagereason.h b/src/model/brokenmessagereason.h new file mode 100644 index 000000000..00cfffc75 --- /dev/null +++ b/src/model/brokenmessagereason.h @@ -0,0 +1,27 @@ +/* + Copyright © 2015-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 . +*/ + +#pragma once + +// NOTE: Numbers are important here as this is cast to an int and persisted in the DB +enum class BrokenMessageReason : int +{ + unknown = 0, + unsupportedExtensions = 1 +}; diff --git a/src/model/chathistory.cpp b/src/model/chathistory.cpp index eefc76ce0..8e8bb6fa1 100644 --- a/src/model/chathistory.cpp +++ b/src/model/chathistory.cpp @@ -82,6 +82,8 @@ ChatHistory::ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& co &ChatHistory::onMessageComplete); connect(&messageDispatcher, &IMessageDispatcher::messageReceived, this, &ChatHistory::onMessageReceived); + connect(&messageDispatcher, &IMessageDispatcher::messageBroken, this, + &ChatHistory::onMessageBroken); if (canUseHistory()) { // Defer messageSent callback until we finish firing off all our unsent messages. @@ -266,7 +268,7 @@ void ChatHistory::onMessageReceived(const ToxPk& sender, const Message& message) content = ChatForm::ACTION_PREFIX + content; } - history->addNewMessage(friendPk, content, friendPk, message.timestamp, true, displayName); + history->addNewMessage(friendPk, content, friendPk, message.timestamp, true, message.extensionSet, displayName); } sessionChatLog.onMessageReceived(sender, message); @@ -287,7 +289,7 @@ void ChatHistory::onMessageSent(DispatchedMessageId id, const Message& message) auto onInsertion = [this, id](RowId historyId) { handleDispatchedMessage(id, historyId); }; - history->addNewMessage(friendPk, content, selfPk, message.timestamp, false, username, + history->addNewMessage(friendPk, content, selfPk, message.timestamp, false, message.extensionSet, username, onInsertion); } @@ -303,6 +305,15 @@ void ChatHistory::onMessageComplete(DispatchedMessageId id) sessionChatLog.onMessageComplete(id); } +void ChatHistory::onMessageBroken(DispatchedMessageId id, BrokenMessageReason reason) +{ + if (canUseHistory()) { + breakMessage(id, reason); + } + + sessionChatLog.onMessageBroken(id, reason); +} + /** * @brief Forces the given index and all future indexes to be in the chatlog * @param[in] idx @@ -405,6 +416,13 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher) { auto unsentMessages = history->getUndeliveredMessagesForFriend(f.getPublicKey()); + + auto requiredExtensions = std::accumulate( + unsentMessages.begin(), unsentMessages.end(), + ExtensionSet(), [] (const ExtensionSet& a, const History::HistMessage& b) { + return a | b.extensionSet; + }); + for (auto& message : unsentMessages) { // We should only store messages as unsent, if this changes in the // future we need to extend this logic @@ -418,12 +436,14 @@ void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher) // with the new timestamp. This is intentional as everywhere else we use // attempted send time (which is whenever the it was initially inserted // into history - auto dispatchIds = messageDispatcher.sendMessage(isAction, messageContent); + auto dispatchId = requiredExtensions.none() + // We should only send a single message, but in the odd case where we end + // up having to split more than when we added the message to history we'll + // just associate the last dispatched id with the history message + ? messageDispatcher.sendMessage(isAction, messageContent).second + : messageDispatcher.sendExtendedMessage(messageContent, requiredExtensions); - // We should only send a single message, but in the odd case where we end - // up having to split more than when we added the message to history we'll - // just associate the last dispatched id with the history message - handleDispatchedMessage(dispatchIds.second, message.id); + handleDispatchedMessage(dispatchId, message.id); // We don't add the messages to the underlying chatlog since // 1. We don't even know the ChatLogIdx of this message @@ -435,11 +455,20 @@ void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher) void ChatHistory::handleDispatchedMessage(DispatchedMessageId dispatchId, RowId historyId) { auto completedMessageIt = completedMessages.find(dispatchId); - if (completedMessageIt == completedMessages.end()) { - dispatchedMessageRowIdMap.insert(dispatchId, historyId); - } else { + auto brokenMessageIt = brokenMessages.find(dispatchId); + + const auto isCompleted = completedMessageIt != completedMessages.end(); + const auto isBroken = brokenMessageIt != brokenMessages.end(); + assert(!(isCompleted && isBroken)); + + if (isCompleted) { history->markAsDelivered(historyId); completedMessages.erase(completedMessageIt); + } else if (isBroken) { + history->markAsBroken(historyId, brokenMessageIt.value()); + brokenMessages.erase(brokenMessageIt); + } else { + dispatchedMessageRowIdMap.insert(dispatchId, historyId); } } @@ -455,6 +484,18 @@ void ChatHistory::completeMessage(DispatchedMessageId id) } } +void ChatHistory::breakMessage(DispatchedMessageId id, BrokenMessageReason reason) +{ + auto dispatchedMessageIt = dispatchedMessageRowIdMap.find(id); + + if (dispatchedMessageIt == dispatchedMessageRowIdMap.end()) { + brokenMessages.insert(id, reason); + } else { + history->markAsBroken(*dispatchedMessageIt, reason); + dispatchedMessageRowIdMap.erase(dispatchedMessageIt); + } +} + bool ChatHistory::canUseHistory() const { return history && settings.getEnableLogging(); diff --git a/src/model/chathistory.h b/src/model/chathistory.h index 1e771e96c..366aff110 100644 --- a/src/model/chathistory.h +++ b/src/model/chathistory.h @@ -21,6 +21,7 @@ #include "ichatlog.h" #include "sessionchatlog.h" +#include "src/model/brokenmessagereason.h" #include "src/persistence/history.h" #include @@ -51,6 +52,7 @@ private slots: void onMessageReceived(const ToxPk& sender, const Message& message); void onMessageSent(DispatchedMessageId id, const Message& message); void onMessageComplete(DispatchedMessageId id); + void onMessageBroken(DispatchedMessageId id, BrokenMessageReason reason); private: void ensureIdxInSessionChatLog(ChatLogIdx idx) const; @@ -58,6 +60,7 @@ private: void dispatchUnsentMessages(IMessageDispatcher& messageDispatcher); void handleDispatchedMessage(DispatchedMessageId dispatchId, RowId historyId); void completeMessage(DispatchedMessageId id); + void breakMessage(DispatchedMessageId id, BrokenMessageReason reason); bool canUseHistory() const; ChatLogIdx getInitialChatLogIdx() const; @@ -70,6 +73,9 @@ private: // If a message completes before it's inserted into history it will end up // in this set QSet completedMessages; + // If a message breaks before it's inserted into history it will end up + // in this set + QMap brokenMessages; // If a message is inserted into history before it gets a completion // callback it will end up in this map diff --git a/src/model/friend.cpp b/src/model/friend.cpp index a2e9ffebf..448d2a553 100644 --- a/src/model/friend.cpp +++ b/src/model/friend.cpp @@ -23,6 +23,9 @@ #include "src/persistence/profile.h" #include "src/widget/form/chatform.h" +#include +#include + Friend::Friend(uint32_t friendId, const ToxPk& friendPk, const QString& userAlias, const QString& userName) : userName{userName} , userAlias{userAlias} @@ -30,6 +33,7 @@ Friend::Friend(uint32_t friendId, const ToxPk& friendPk, const QString& userAlia , friendId{friendId} , hasNewEvents{false} , friendStatus{Status::Status::Offline} + , isNegotiating{false} { if (userName.isEmpty()) { this->userName = friendPk.toString(); @@ -151,22 +155,41 @@ bool Friend::getEventFlag() const void Friend::setStatus(Status::Status s) { - if (friendStatus != s) { - auto oldStatus = friendStatus; - friendStatus = s; - emit statusChanged(friendPk, friendStatus); - if (!Status::isOnline(oldStatus) && Status::isOnline(friendStatus)) { - emit onlineOfflineChanged(friendPk, true); - } else if (Status::isOnline(oldStatus) && !Status::isOnline(friendStatus)) { - emit onlineOfflineChanged(friendPk, false); - } + // Internal status should never be negotiating. We only expose this externally through the use of isNegotiating + assert(s != Status::Status::Negotiating); + const bool wasOnline = Status::isOnline(getStatus()); + if (friendStatus == s) { + return; + } + + // When a friend goes online we want to give them some time to negotiate + // extension support + const auto startNegotating = friendStatus == Status::Status::Offline; + + if (startNegotating) { + qDebug() << "Starting negotiation with friend " << friendId; + isNegotiating = true; + } + + friendStatus = s; + const bool nowOnline = Status::isOnline(getStatus()); + + const auto emitStatusChange = startNegotating || !isNegotiating; + if (emitStatusChange) { + const auto statusToEmit = isNegotiating ? Status::Status::Negotiating : friendStatus; + emit statusChanged(friendPk, statusToEmit); + if (wasOnline && !nowOnline) { + emit onlineOfflineChanged(friendPk, false); + } else if (!wasOnline && nowOnline) { + emit onlineOfflineChanged(friendPk, true); + } } } Status::Status Friend::getStatus() const { - return friendStatus; + return isNegotiating ? Status::Status::Negotiating : friendStatus; } bool Friend::useHistory() const @@ -178,9 +201,29 @@ void Friend::setExtendedMessageSupport(bool supported) { supportedExtensions[ExtensionType::messages] = supported; emit extensionSupportChanged(supportedExtensions); + + // If all extensions are supported we can exit early + if (supportedExtensions.all()) { + onNegotiationComplete(); + } } ExtensionSet Friend::getSupportedExtensions() const { return supportedExtensions; } + +void Friend::onNegotiationComplete() { + if (!isNegotiating) { + return; + } + + qDebug() << "Negotiation complete for friend " << friendId; + + isNegotiating = false; + emit statusChanged(friendPk, friendStatus); + + if (Status::isOnline(getStatus())) { + emit onlineOfflineChanged(friendPk, true); + } +} diff --git a/src/model/friend.h b/src/model/friend.h index 4db5b91e6..0dc964db1 100644 --- a/src/model/friend.h +++ b/src/model/friend.h @@ -69,6 +69,7 @@ signals: void loadChatHistory(); public slots: + void onNegotiationComplete(); private: QString userName; QString userAlias; @@ -77,5 +78,6 @@ private: uint32_t friendId; bool hasNewEvents; Status::Status friendStatus; + bool isNegotiating; ExtensionSet supportedExtensions; }; diff --git a/src/model/friendmessagedispatcher.cpp b/src/model/friendmessagedispatcher.cpp index c725e79ab..8a9f4eadc 100644 --- a/src/model/friendmessagedispatcher.cpp +++ b/src/model/friendmessagedispatcher.cpp @@ -21,34 +21,11 @@ #include "src/persistence/settings.h" #include "src/model/status.h" -namespace { - -/** - * @brief Sends message to friend using messageSender - * @param[in] messageSender - * @param[in] f - * @param[in] message - * @param[out] receipt - */ -bool sendMessageToCore(ICoreFriendMessageSender& messageSender, const Friend& f, - const Message& message, ReceiptNum& receipt) -{ - uint32_t friendId = f.getId(); - - auto sendFn = message.isAction ? std::mem_fn(&ICoreFriendMessageSender::sendAction) - : std::mem_fn(&ICoreFriendMessageSender::sendMessage); - - return sendFn(messageSender, friendId, message.content, receipt); -} -} // namespace - - FriendMessageDispatcher::FriendMessageDispatcher(Friend& f_, MessageProcessor processor_, ICoreFriendMessageSender& messageSender_, ICoreExtPacketAllocator& coreExtPacketAllocator_) : f(f_) , messageSender(messageSender_) - , offlineMsgEngine(&f_, &messageSender_) , processor(std::move(processor_)) , coreExtPacketAllocator(coreExtPacketAllocator_) { @@ -56,53 +33,43 @@ FriendMessageDispatcher::FriendMessageDispatcher(Friend& f_, MessageProcessor pr } /** - * @see IMessageSender::sendMessage + * @see IMessageDispatcher::sendMessage */ std::pair FriendMessageDispatcher::sendMessage(bool isAction, const QString& content) { const auto firstId = nextMessageId; auto lastId = nextMessageId; - auto supportedExtensions = f.getSupportedExtensions(); - const bool needsSplit = !supportedExtensions[ExtensionType::messages]; - for (const auto& message : processor.processOutgoingMessage(isAction, content, needsSplit)) { + for (const auto& message : processor.processOutgoingMessage(isAction, content, f.getSupportedExtensions())) { auto messageId = nextMessageId++; lastId = messageId; - auto onOfflineMsgComplete = [this, messageId] { emit this->messageComplete(messageId); }; - ReceiptNum receipt; - - bool messageSent = false; - - // NOTE: This branch is getting a little hairy but will be cleaned up in the following commit - if (Status::isOnline(f.getStatus())) { - - // Action messages go over the regular mesage channel so we cannot use extensions with them - if (supportedExtensions[ExtensionType::messages] && !isAction) { - auto packet = coreExtPacketAllocator.getPacket(f.getId()); - - if (supportedExtensions[ExtensionType::messages]) { - // NOTE: Dirty hack to get extensions working that will be fixed in the following commit - receipt.get() = packet->addExtendedMessage(message.content); - } - - messageSent = packet->send(); - } else { - messageSent = sendMessageToCore(messageSender, f, message, receipt); - } - } - - if (!messageSent) { - offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete); - } else { - offlineMsgEngine.addSentMessage(receipt, message, onOfflineMsgComplete); - } + auto onOfflineMsgComplete = getCompletionFn(messageId); + sendProcessedMessage(message, onOfflineMsgComplete); emit this->messageSent(messageId, message); } return std::make_pair(firstId, lastId); } +/** + * @see IMessageDispatcher::sendExtendedMessage + */ +DispatchedMessageId FriendMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions) +{ + auto messageId = nextMessageId++; + + auto messages = processor.processOutgoingMessage(false, content, extensions); + assert(messages.size() == 1); + + auto onOfflineMsgComplete = getCompletionFn(messageId); + sendProcessedMessage(messages[0], onOfflineMsgComplete); + + emit this->messageSent(messageId, messages[0]); + + return messageId; +} + /** * @brief Handles received message from toxcore * @param[in] isAction True if action message @@ -130,8 +97,7 @@ void FriendMessageDispatcher::onExtMessageReceived(const QString& content) void FriendMessageDispatcher::onExtReceiptReceived(uint64_t receiptId) { - // NOTE: Reusing ReceiptNum is a dirty hack that will be cleaned up in the following commit - offlineMsgEngine.onReceiptReceived(ReceiptNum(receiptId)); + offlineMsgEngine.onExtendedReceiptReceived(ExtendedReceiptNum(receiptId)); } /** @@ -141,7 +107,10 @@ void FriendMessageDispatcher::onExtReceiptReceived(uint64_t receiptId) void FriendMessageDispatcher::onFriendOnlineOfflineChanged(const ToxPk&, bool isOnline) { if (isOnline) { - offlineMsgEngine.deliverOfflineMsgs(); + auto messagesToResend = offlineMsgEngine.removeAllMessages(); + for (auto const& message : messagesToResend) { + sendProcessedMessage(message.message, message.callback); + } } } @@ -152,3 +121,77 @@ void FriendMessageDispatcher::clearOutgoingMessages() { offlineMsgEngine.removeAllMessages(); } + + +void FriendMessageDispatcher::sendProcessedMessage(Message const& message, OfflineMsgEngine::CompletionFn onOfflineMsgComplete) +{ + if (!Status::isOnline(f.getStatus())) { + offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete); + return; + } + + if (message.extensionSet[ExtensionType::messages] && !message.isAction) { + sendExtendedProcessedMessage(message, onOfflineMsgComplete); + } else { + sendCoreProcessedMessage(message, onOfflineMsgComplete); + } +} + + + +void FriendMessageDispatcher::sendExtendedProcessedMessage(Message const& message, OfflineMsgEngine::CompletionFn onOfflineMsgComplete) +{ + assert(!message.isAction); // Actions not supported with extensions + + if ((f.getSupportedExtensions() & message.extensionSet) != message.extensionSet) { + onOfflineMsgComplete(false); + return; + } + + auto receipt = ExtendedReceiptNum(); + + auto packet = coreExtPacketAllocator.getPacket(f.getId()); + + if (message.extensionSet[ExtensionType::messages]) { + receipt.get() = packet->addExtendedMessage(message.content); + } + + const auto messageSent = packet->send(); + + if (messageSent) { + offlineMsgEngine.addSentExtendedMessage(receipt, message, onOfflineMsgComplete); + } else { + offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete); + } +} + +void FriendMessageDispatcher::sendCoreProcessedMessage(Message const& message, OfflineMsgEngine::CompletionFn onOfflineMsgComplete) +{ + auto receipt = ReceiptNum(); + + uint32_t friendId = f.getId(); + + auto sendFn = message.isAction ? std::mem_fn(&ICoreFriendMessageSender::sendAction) + : std::mem_fn(&ICoreFriendMessageSender::sendMessage); + + const auto messageSent = sendFn(messageSender, friendId, message.content, receipt); + + if (messageSent) { + offlineMsgEngine.addSentCoreMessage(receipt, message, onOfflineMsgComplete); + } else { + offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete); + } +} + +OfflineMsgEngine::CompletionFn FriendMessageDispatcher::getCompletionFn(DispatchedMessageId messageId) +{ + return [this, messageId] (bool success) { + if (success) { + emit this->messageComplete(messageId); + } else { + // For now we know the only reason we can fail after giving to the + // offline message engine is due to a reduced extension set + emit this->messageBroken(messageId, BrokenMessageReason::unsupportedExtensions); + } + }; +} diff --git a/src/model/friendmessagedispatcher.h b/src/model/friendmessagedispatcher.h index 9cd966e21..e0688f112 100644 --- a/src/model/friendmessagedispatcher.h +++ b/src/model/friendmessagedispatcher.h @@ -40,6 +40,8 @@ public: std::pair sendMessage(bool isAction, const QString& content) override; + + DispatchedMessageId sendExtendedMessage(const QString& content, ExtensionSet extensions) override; void onMessageReceived(bool isAction, const QString& content); void onReceiptReceived(ReceiptNum receipt); void onExtMessageReceived(const QString& message); @@ -49,6 +51,11 @@ private slots: void onFriendOnlineOfflineChanged(const ToxPk& key, bool isOnline); private: + void sendProcessedMessage(Message const& msg, OfflineMsgEngine::CompletionFn fn); + void sendExtendedProcessedMessage(Message const& msg, OfflineMsgEngine::CompletionFn fn); + void sendCoreProcessedMessage(Message const& msg, OfflineMsgEngine::CompletionFn fn); + OfflineMsgEngine::CompletionFn getCompletionFn(DispatchedMessageId messageId); + Friend& f; ICoreExtPacketAllocator& coreExtPacketAllocator; DispatchedMessageId nextMessageId = DispatchedMessageId(0); diff --git a/src/model/groupmessagedispatcher.cpp b/src/model/groupmessagedispatcher.cpp index 653c6fad1..e4ca5b426 100644 --- a/src/model/groupmessagedispatcher.cpp +++ b/src/model/groupmessagedispatcher.cpp @@ -41,7 +41,7 @@ GroupMessageDispatcher::sendMessage(bool isAction, QString const& content) const auto firstMessageId = nextMessageId; auto lastMessageId = firstMessageId; - for (auto const& message : processor.processOutgoingMessage(isAction, content, true /*needsSplit*/)) { + for (auto const& message : processor.processOutgoingMessage(isAction, content, ExtensionSet())) { auto messageId = nextMessageId++; lastMessageId = messageId; if (group.getPeersCount() != 1) { @@ -65,6 +65,16 @@ GroupMessageDispatcher::sendMessage(bool isAction, QString const& content) return std::make_pair(firstMessageId, lastMessageId); } +DispatchedMessageId GroupMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions) +{ + // Stub this api to immediately fail + auto messageId = nextMessageId++; + auto messages = processor.processOutgoingMessage(false, content, ExtensionSet()); + emit this->messageSent(messageId, messages[0]); + emit this->messageBroken(messageId, BrokenMessageReason::unsupportedExtensions); + return messageId; +} + /** * @brief Processes and dispatches received message from toxcore * @param[in] sender diff --git a/src/model/groupmessagedispatcher.h b/src/model/groupmessagedispatcher.h index 0cbcc4024..807bd4f2a 100644 --- a/src/model/groupmessagedispatcher.h +++ b/src/model/groupmessagedispatcher.h @@ -42,6 +42,8 @@ public: std::pair sendMessage(bool isAction, QString const& content) override; + + DispatchedMessageId sendExtendedMessage(const QString& content, ExtensionSet extensions) override; void onMessageReceived(ToxPk const& sender, bool isAction, QString const& content); private: diff --git a/src/model/imessagedispatcher.h b/src/model/imessagedispatcher.h index 78b9466e5..abbdd7c1c 100644 --- a/src/model/imessagedispatcher.h +++ b/src/model/imessagedispatcher.h @@ -21,6 +21,7 @@ #include "src/model/friend.h" #include "src/model/message.h" +#include "src/model/brokenmessagereason.h" #include #include @@ -44,6 +45,18 @@ public: */ virtual std::pair sendMessage(bool isAction, const QString& content) = 0; + + /** + * @brief Sends message to associated chat ensuring that extensions are available + * @param[in] content Message content + * @param[in] extensions extensions required for given message + * @return Pair of first and last dispatched message IDs + * @note If the provided extensions are not supported the message will be flagged + * as broken + */ + virtual DispatchedMessageId + sendExtendedMessage(const QString& content, ExtensionSet extensions) = 0; + signals: /** * @brief Emitted when a message is received and processed @@ -62,4 +75,6 @@ signals: * @param id Id of message that is completed */ void messageComplete(DispatchedMessageId id); + + void messageBroken(DispatchedMessageId id, BrokenMessageReason reason); }; diff --git a/src/model/message.cpp b/src/model/message.cpp index 9ff555757..e7e018f3a 100644 --- a/src/model/message.cpp +++ b/src/model/message.cpp @@ -51,10 +51,11 @@ MessageProcessor::MessageProcessor(const MessageProcessor::SharedParams& sharedP /** * @brief Converts an outgoing message into one (or many) sanitized Message(s) */ -std::vector MessageProcessor::processOutgoingMessage(bool isAction, QString const& content, bool needsSplit) +std::vector MessageProcessor::processOutgoingMessage(bool isAction, QString const& content, ExtensionSet extensions) { std::vector ret; + const auto needsSplit = !extensions[ExtensionType::messages] || isAction; const auto splitMsgs = needsSplit ? Core::splitMessage(content) : QStringList({content}); @@ -68,6 +69,10 @@ std::vector MessageProcessor::processOutgoingMessage(bool isAction, QSt message.isAction = isAction; message.content = part; message.timestamp = timestamp; + // In theory we could limit this only to the extensions + // required but since Core owns the splitting logic it + // isn't trivial to do that now + message.extensionSet = extensions; return message; }); @@ -124,6 +129,7 @@ Message MessageProcessor::processIncomingExtMessage(const QString& content) auto message = Message(); message.timestamp = QDateTime::currentDateTime(); message.content = content; + message.extensionSet |= ExtensionType::messages; return message; } diff --git a/src/model/message.h b/src/model/message.h index 66eb51339..ac31183d3 100644 --- a/src/model/message.h +++ b/src/model/message.h @@ -20,6 +20,7 @@ #pragma once #include "src/core/coreext.h" +#include "src/core/extension.h" #include #include @@ -53,6 +54,7 @@ struct Message bool isAction; QString content; QDateTime timestamp; + ExtensionSet extensionSet; std::vector metadata; }; @@ -92,7 +94,7 @@ public: MessageProcessor(const SharedParams& sharedParams); - std::vector processOutgoingMessage(bool isAction, const QString& content, bool needsSplit); + std::vector processOutgoingMessage(bool isAction, const QString& content, ExtensionSet extensions); Message processIncomingCoreMessage(bool isAction, const QString& content); Message processIncomingExtMessage(const QString& content); diff --git a/src/model/sessionchatlog.cpp b/src/model/sessionchatlog.cpp index 204b7522e..5de8fa217 100644 --- a/src/model/sessionchatlog.cpp +++ b/src/model/sessionchatlog.cpp @@ -420,6 +420,29 @@ void SessionChatLog::onMessageComplete(DispatchedMessageId id) emit this->itemUpdated(messageIt->first); } +void SessionChatLog::onMessageBroken(DispatchedMessageId id, BrokenMessageReason) +{ + auto chatLogIdxIt = outgoingMessages.find(id); + + if (chatLogIdxIt == outgoingMessages.end()) { + qWarning() << "Failed to find outgoing message"; + return; + } + + const auto& chatLogIdx = *chatLogIdxIt; + auto messageIt = items.find(chatLogIdx); + + if (messageIt == items.end()) { + qWarning() << "Failed to look up message in chat log"; + return; + } + + // NOTE: Reason for broken message not currently shown in UI, but it could be + messageIt->second.getContentAsMessage().state = MessageState::broken; + + emit this->itemUpdated(messageIt->first); +} + /** * @brief Updates file state in the chatlog * @note The files need to be pre-filtered for the current chat since we do no validation diff --git a/src/model/sessionchatlog.h b/src/model/sessionchatlog.h index 96bcfb321..99c8fa52a 100644 --- a/src/model/sessionchatlog.h +++ b/src/model/sessionchatlog.h @@ -57,6 +57,7 @@ public slots: void onMessageReceived(const ToxPk& sender, const Message& message); void onMessageSent(DispatchedMessageId id, const Message& message); void onMessageComplete(DispatchedMessageId id); + void onMessageBroken(DispatchedMessageId id, BrokenMessageReason reason); void onFileUpdated(const ToxPk& sender, const ToxFile& file); void onFileTransferRemotePausedUnpaused(const ToxPk& sender, const ToxFile& file, bool paused); diff --git a/src/model/status.cpp b/src/model/status.cpp index 50220126a..eed021120 100644 --- a/src/model/status.cpp +++ b/src/model/status.cpp @@ -41,6 +41,8 @@ namespace Status return QObject::tr("offline", "contact status"); case Status::Blocked: return QObject::tr("blocked", "contact status"); + case Status::Negotiating: + return QObject::tr("negotitating", "contact status"); } assert(false); @@ -60,6 +62,8 @@ namespace Status return "offline"; case Status::Blocked: return "blocked"; + case Status::Negotiating: + return "negotiating"; } assert(false); return QStringLiteral(""); @@ -78,6 +82,9 @@ namespace Status bool isOnline(Status status) { - return status != Status::Offline && status != Status::Blocked; + return status != Status::Offline + && status != Status::Blocked + // We don't want to treat a friend as online unless we know their feature set + && status != Status::Negotiating; } } // namespace Status diff --git a/src/model/status.h b/src/model/status.h index e95a56cbd..2c29114d2 100644 --- a/src/model/status.h +++ b/src/model/status.h @@ -31,7 +31,8 @@ namespace Status Away, Busy, Offline, - Blocked + Blocked, + Negotiating, }; QString getIconPath(Status status, bool event = false); diff --git a/src/persistence/history.cpp b/src/persistence/history.cpp index abca18495..3bb037542 100644 --- a/src/persistence/history.cpp +++ b/src/persistence/history.cpp @@ -27,7 +27,7 @@ #include "src/core/toxpk.h" namespace { -static constexpr int SCHEMA_VERSION = 5; +static constexpr int SCHEMA_VERSION = 6; bool createCurrentSchema(RawDatabase& db) { @@ -67,8 +67,10 @@ bool createCurrentSchema(RawDatabase& db) "direction INTEGER NOT NULL, " "file_state INTEGER NOT NULL);" "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, " + "required_extensions INTEGER NOT NULL DEFAULT 0, " "FOREIGN KEY (id) REFERENCES history(id));" "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, " + "reason INTEGER NOT NULL DEFAULT 0, " "FOREIGN KEY (id) REFERENCES history(id));")); // sqlite doesn't support including the index as part of the CREATE TABLE statement, so add a second query queries += RawDatabase::Query( @@ -95,20 +97,17 @@ bool isNewDb(std::shared_ptr& db, bool& success) bool dbSchema0to1(RawDatabase& db) { QVector queries; - queries += - RawDatabase::Query(QStringLiteral( - "CREATE TABLE file_transfers " - "(id INTEGER PRIMARY KEY, " - "chat_id INTEGER NOT NULL, " - "file_restart_id BLOB NOT NULL, " - "file_name BLOB NOT NULL, " - "file_path BLOB NOT NULL, " - "file_hash BLOB NOT NULL, " - "file_size INTEGER NOT NULL, " - "direction INTEGER NOT NULL, " - "file_state INTEGER NOT NULL);")); - queries += - RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;")); + queries += RawDatabase::Query(QStringLiteral("CREATE TABLE file_transfers " + "(id INTEGER PRIMARY KEY, " + "chat_id INTEGER NOT NULL, " + "file_restart_id BLOB NOT NULL, " + "file_name BLOB NOT NULL, " + "file_path BLOB NOT NULL, " + "file_hash BLOB NOT NULL, " + "file_size INTEGER NOT NULL, " + "direction INTEGER NOT NULL, " + "file_state INTEGER NOT NULL);")); + queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;")); queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;")); return db.execNow(queries); } @@ -120,29 +119,29 @@ bool dbSchema1to2(RawDatabase& db) // faux_offline_pending to broken_messages // the last non-pending message in each chat - QString lastDeliveredQuery = QString( - "SELECT chat_id, MAX(history.id) FROM " - "history JOIN peers chat ON chat_id = chat.id " - "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " - "WHERE faux_offline_pending.id IS NULL " - "GROUP BY chat_id;"); + QString lastDeliveredQuery = + QString("SELECT chat_id, MAX(history.id) FROM " + "history JOIN peers chat ON chat_id = chat.id " + "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " + "WHERE faux_offline_pending.id IS NULL " + "GROUP BY chat_id;"); QVector upgradeQueries; - upgradeQueries += - RawDatabase::Query(QStringLiteral( - "CREATE TABLE broken_messages " - "(id INTEGER PRIMARY KEY);")); + upgradeQueries += RawDatabase::Query(QStringLiteral("CREATE TABLE broken_messages " + "(id INTEGER PRIMARY KEY);")); auto rowCallback = [&upgradeQueries](const QVector& row) { auto chatId = row[0].toLongLong(); auto lastDeliveredHistoryId = row[1].toLongLong(); upgradeQueries += QString("INSERT INTO broken_messages " - "SELECT faux_offline_pending.id FROM " - "history JOIN faux_offline_pending " - "ON faux_offline_pending.id = history.id " - "WHERE history.chat_id=%1 " - "AND history.id < %2;").arg(chatId).arg(lastDeliveredHistoryId); + "SELECT faux_offline_pending.id FROM " + "history JOIN faux_offline_pending " + "ON faux_offline_pending.id = history.id " + "WHERE history.chat_id=%1 " + "AND history.id < %2;") + .arg(chatId) + .arg(lastDeliveredHistoryId); }; // note this doesn't modify the db, just generate new queries, so is safe // to run outside of our upgrade transaction @@ -150,10 +149,9 @@ bool dbSchema1to2(RawDatabase& db) return false; } - upgradeQueries += QString( - "DELETE FROM faux_offline_pending " - "WHERE id in (" - "SELECT id FROM broken_messages);"); + upgradeQueries += QString("DELETE FROM faux_offline_pending " + "WHERE id in (" + "SELECT id FROM broken_messages);"); upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 2;")); @@ -172,16 +170,15 @@ bool dbSchema2to3(RawDatabase& db) QVector upgradeQueries; upgradeQueries += RawDatabase::Query{QString("INSERT INTO broken_messages " - "SELECT faux_offline_pending.id FROM " - "history JOIN faux_offline_pending " - "ON faux_offline_pending.id = history.id " - "WHERE history.message = ?;"), - {emptyActionMessageString.toUtf8()}}; + "SELECT faux_offline_pending.id FROM " + "history JOIN faux_offline_pending " + "ON faux_offline_pending.id = history.id " + "WHERE history.message = ?;"), + {emptyActionMessageString.toUtf8()}}; - upgradeQueries += QString( - "DELETE FROM faux_offline_pending " - "WHERE id in (" - "SELECT id FROM broken_messages);"); + upgradeQueries += QString("DELETE FROM faux_offline_pending " + "WHERE id in (" + "SELECT id FROM broken_messages);"); upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 3;")); @@ -277,14 +274,48 @@ bool dbSchema4to5(RawDatabase& db) return transactionPass; } +bool dbSchema5to6(RawDatabase& db) +{ + QVector upgradeQueries; + + upgradeQueries += RawDatabase::Query{QString("ALTER TABLE faux_offline_pending " + "ADD COLUMN required_extensions INTEGER NOT NULL " + "DEFAULT 0;")}; + + upgradeQueries += RawDatabase::Query{QString("ALTER TABLE broken_messages " + "ADD COLUMN reason INTEGER NOT NULL " + "DEFAULT 0;")}; + + upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 6;")); + return db.execNow(upgradeQueries); +} + /** -* @brief Upgrade the db schema -* @return True if the schema upgrade succeded, false otherwise -* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION -* variable and add another case to the switch statement below. Make sure to fall through on each case. -*/ + * @brief Upgrade the db schema + * @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION + * variable and add another case to the switch statement below. Make sure to fall through on each case. + */ bool dbSchemaUpgrade(std::shared_ptr& db) { + // If we're a new dB we can just make a new one and call it a day + bool success = false; + const bool newDb = isNewDb(db, success); + if (!success) { + qCritical() << "Failed to create current db schema"; + return false; + } + + if (newDb) { + if (!createCurrentSchema(*db)) { + qCritical() << "Failed to create current db schema"; + return false; + } + qDebug() << "Database created at schema version" << SCHEMA_VERSION; + return true; + } + + // Otherwise we have to do upgrades from our current version to the latest version + int64_t databaseSchemaVersion; if (!db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector& row) { @@ -295,8 +326,9 @@ bool dbSchemaUpgrade(std::shared_ptr& db) } if (databaseSchemaVersion > SCHEMA_VERSION) { - qWarning().nospace() << "Database version (" << databaseSchemaVersion << - ") is newer than we currently support (" << SCHEMA_VERSION << "). Please upgrade qTox"; + qWarning().nospace() << "Database version (" << databaseSchemaVersion + << ") is newer than we currently support (" << SCHEMA_VERSION + << "). Please upgrade qTox"; // We don't know what future versions have done, we have to disable db access until we re-upgrade return false; } else if (databaseSchemaVersion == SCHEMA_VERSION) { @@ -304,66 +336,24 @@ bool dbSchemaUpgrade(std::shared_ptr& db) return true; } - switch (databaseSchemaVersion) { - case 0: { - // Note: 0 is a special version that is actually two versions. - // possibility 1) it is a newly created database and it neesds the current schema to be created. - // possibility 2) it is a old existing database, before version 1 and before we saved schema version, - // and needs to be updated. - bool success = false; - const bool newDb = isNewDb(db, success); - if (!success) { - qCritical() << "Failed to create current db schema"; + using DbSchemaUpgradeFn = bool (*)(RawDatabase&); + std::vector upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3, + dbSchema3to4, dbSchema4to5, dbSchema5to6}; + + assert(databaseSchemaVersion < static_cast(upgradeFns.size())); + assert(upgradeFns.size() == SCHEMA_VERSION); + + for (int64_t i = databaseSchemaVersion; i < static_cast(upgradeFns.size()); ++i) { + auto const newDbVersion = i + 1; + if (!upgradeFns[i](*db)) { + qCritical() << "Failed to upgrade db to schema version " << newDbVersion << " aborting"; return false; } - if (newDb) { - if (!createCurrentSchema(*db)) { - qCritical() << "Failed to create current db schema"; - return false; - } - qDebug() << "Database created at schema version" << SCHEMA_VERSION; - break; // new db is the only case where we don't incrementally upgrade through each version - } else { - if (!dbSchema0to1(*db)) { - qCritical() << "Failed to upgrade db to schema version 1, aborting"; - return false; - } - qDebug() << "Database upgraded incrementally to schema version 1"; - } - } - // fallthrough - case 1: - if (!dbSchema1to2(*db)) { - qCritical() << "Failed to upgrade db to schema version 2, aborting"; - return false; - } - qDebug() << "Database upgraded incrementally to schema version 2"; - //fallthrough - case 2: - if (!dbSchema2to3(*db)) { - qCritical() << "Failed to upgrade db to schema version 3, aborting"; - return false; - } - qDebug() << "Database upgraded incrementally to schema version 3"; - case 3: - if (!dbSchema3to4(*db)) { - qCritical() << "Failed to upgrade db to schema version 4, aborting"; - return false; - } - qDebug() << "Database upgraded incrementally to schema version 4"; - //fallthrough - case 4: - if (!dbSchema4to5(*db)) { - qCritical() << "Failed to upgrade db to schema version 5, aborting"; - return false; - } - qDebug() << "Database upgraded incrementally to schema version 5"; - // etc. - default: - qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion - << "->" << SCHEMA_VERSION << ")"; + qDebug() << "Database upgraded incrementally to schema version " << newDbVersion; } + qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion << "->" + << SCHEMA_VERSION << ")"; return true; } @@ -371,6 +361,7 @@ MessageState getMessageState(bool isPending, bool isBroken) { assert(!(isPending && isBroken)); MessageState messageState; + if (isPending) { messageState = MessageState::pending; } else if (isBroken) { @@ -544,7 +535,8 @@ void History::removeFriendHistory(const ToxPk& friendPk) QVector History::generateNewMessageQueries(const ToxPk& friendPk, const QString& message, const ToxPk& sender, const QDateTime& time, bool isDelivered, - QString dispName, std::function insertIdCallback) + ExtensionSet extensionSet, QString dispName, + std::function insertIdCallback) { QVector queries; @@ -565,9 +557,10 @@ History::generateNewMessageQueries(const ToxPk& friendPk, const QString& message {message.toUtf8(), dispName.toUtf8()}, insertIdCallback); if (!isDelivered) { - queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES (" - " last_insert_rowid()" - ");"}; + queries += RawDatabase::Query{QString("INSERT INTO faux_offline_pending (id, required_extensions) VALUES (" + " last_insert_rowid(), %1" + ");") + .arg(extensionSet.to_ulong())}; } return queries; @@ -590,7 +583,8 @@ void History::onFileInsertionReady(FileDbInsertionData data) .arg(data.size) .arg(static_cast(data.direction)) .arg(ToxFile::CANCELED), - {data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(), QByteArray()}, + {data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(), + QByteArray()}, [weakThis, fileId](RowId id) { auto pThis = weakThis.lock(); if (pThis) { @@ -687,7 +681,7 @@ void History::addNewFileMessage(const ToxPk& friendPk, const QString& fileId, emit thisPtr->fileInsertionReady(std::move(insertionDataRw)); }; - addNewMessage(friendPk, "", sender, time, true, dispName, insertFileTransferFn); + addNewMessage(friendPk, "", sender, time, true, ExtensionSet(), dispName, insertFileTransferFn); } /** @@ -701,15 +695,15 @@ void History::addNewFileMessage(const ToxPk& friendPk, const QString& fileId, * @param insertIdCallback Function, called after query execution. */ void History::addNewMessage(const ToxPk& friendPk, const QString& message, const ToxPk& sender, - const QDateTime& time, bool isDelivered, QString dispName, - const std::function& insertIdCallback) + const QDateTime& time, bool isDelivered, ExtensionSet extensionSet, + QString dispName, const std::function& insertIdCallback) { if (historyAccessBlocked()) { return; } - db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered, dispName, - insertIdCallback)); + db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered, + extensionSet, dispName, insertIdCallback)); } void History::setFileFinished(const QString& fileId, bool success, const QString& filePath, @@ -785,7 +779,8 @@ QList History::getMessagesForFriend(const ToxPk& friendPk, "message, file_transfers.file_restart_id, " "file_transfers.file_path, file_transfers.file_name, " "file_transfers.file_size, file_transfers.direction, " - "file_transfers.file_state, broken_messages.id FROM history " + "file_transfers.file_state, broken_messages.id, " + "faux_offline_pending.required_extensions FROM history " "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " "JOIN peers chat ON history.chat_id = chat.id " "JOIN aliases ON sender_alias = aliases.id " @@ -808,12 +803,13 @@ QList History::getMessagesForFriend(const ToxPk& friendPk, auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', "")); auto sender_key = row[5].toString(); auto isBroken = !row[13].isNull(); + auto requiredExtensions = ExtensionSet(row[14].toLongLong()); MessageState messageState = getMessageState(isPending, isBroken); if (row[7].isNull()) { - messages += {id, messageState, timestamp, friend_key, - display_name, sender_key, row[6].toString()}; + messages += {id, messageState, requiredExtensions, timestamp, friend_key, + display_name, sender_key, row[6].toString()}; } else { ToxFile file; file.fileKind = TOX_FILE_KIND_DATA; @@ -823,8 +819,7 @@ QList History::getMessagesForFriend(const ToxPk& friendPk, file.filesize = row[10].toLongLong(); file.direction = static_cast(row[11].toLongLong()); file.status = static_cast(row[12].toInt()); - messages += - {id, messageState, timestamp, friend_key, display_name, sender_key, file}; + messages += {id, messageState, timestamp, friend_key, display_name, sender_key, file}; } }; @@ -841,7 +836,8 @@ QList History::getUndeliveredMessagesForFriend(const ToxPk auto queryText = QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, " - "aliases.display_name, sender.public_key, message, broken_messages.id " + "aliases.display_name, sender.public_key, message, broken_messages.id, " + "faux_offline_pending.required_extensions " "FROM history " "JOIN faux_offline_pending ON history.id = faux_offline_pending.id " "JOIN peers chat on history.chat_id = chat.id " @@ -862,11 +858,12 @@ QList History::getUndeliveredMessagesForFriend(const ToxPk auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', "")); auto sender_key = row[5].toString(); auto isBroken = !row[7].isNull(); + auto extensionSet = ExtensionSet(row[8].toLongLong()); MessageState messageState = getMessageState(isPending, isBroken); - ret += {id, messageState, timestamp, friend_key, - display_name, sender_key, row[6].toString()}; + ret += + {id, messageState, extensionSet, timestamp, friend_key, display_name, sender_key, row[6].toString()}; }; db->execNow({queryText, rowCallback}); @@ -1066,5 +1063,20 @@ bool History::historyAccessBlocked() } return false; - +} + +void History::markAsBroken(RowId messageId, BrokenMessageReason reason) +{ + if (!isValid()) { + return; + } + + QVector queries; + queries += RawDatabase::Query(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get())); + queries += RawDatabase::Query(QString("INSERT INTO broken_messages (id, reason) " + "VALUES (%1, %2);") + .arg(messageId.get()) + .arg(static_cast(reason))); + + db->execLater(queries); } diff --git a/src/persistence/history.h b/src/persistence/history.h index be7cae148..b5246c693 100644 --- a/src/persistence/history.h +++ b/src/persistence/history.h @@ -30,6 +30,8 @@ #include "src/core/toxfile.h" #include "src/core/toxpk.h" +#include "src/core/extension.h" +#include "src/model/brokenmessagereason.h" #include "src/persistence/db/rawdatabase.h" #include "src/widget/searchtypes.h" @@ -117,7 +119,7 @@ class History : public QObject, public std::enable_shared_from_this public: struct HistMessage { - HistMessage(RowId id, MessageState state, QDateTime timestamp, QString chat, QString dispName, + HistMessage(RowId id, MessageState state, ExtensionSet extensionSet, QDateTime timestamp, QString chat, QString dispName, QString sender, QString message) : chat{chat} , sender{sender} @@ -125,6 +127,7 @@ public: , timestamp{timestamp} , id{id} , state{state} + , extensionSet(extensionSet) , content(std::move(message)) {} @@ -146,6 +149,7 @@ public: QDateTime timestamp; RowId id; MessageState state; + ExtensionSet extensionSet; HistMessageContent content; }; @@ -166,8 +170,8 @@ public: void eraseHistory(); void removeFriendHistory(const ToxPk& friendPk); void addNewMessage(const ToxPk& friendPk, const QString& message, const ToxPk& sender, - const QDateTime& time, bool isDelivered, QString dispName, - const std::function& insertIdCallback = {}); + const QDateTime& time, bool isDelivered, ExtensionSet extensions, + QString dispName, const std::function& insertIdCallback = {}); void addNewFileMessage(const ToxPk& friendPk, const QString& fileId, const QString& fileName, const QString& filePath, int64_t size, @@ -184,12 +188,13 @@ public: const QDate& from, size_t maxNum); void markAsDelivered(RowId messageId); + void markAsBroken(RowId messageId, BrokenMessageReason reason); protected: QVector generateNewMessageQueries(const ToxPk& friendPk, const QString& message, const ToxPk& sender, const QDateTime& time, bool isDelivered, - QString dispName, std::function insertIdCallback = {}); + ExtensionSet extensionSet, QString dispName, std::function insertIdCallback = {}); signals: void fileInsertionReady(FileDbInsertionData data); diff --git a/src/persistence/offlinemsgengine.cpp b/src/persistence/offlinemsgengine.cpp index 6026145bf..86622329b 100644 --- a/src/persistence/offlinemsgengine.cpp +++ b/src/persistence/offlinemsgengine.cpp @@ -29,10 +29,8 @@ #include #include -OfflineMsgEngine::OfflineMsgEngine(Friend* frnd, ICoreFriendMessageSender* messageSender) +OfflineMsgEngine::OfflineMsgEngine() : mutex(QMutex::Recursive) - , f(frnd) - , messageSender(messageSender) {} /** @@ -43,12 +41,13 @@ OfflineMsgEngine::OfflineMsgEngine(Friend* frnd, ICoreFriendMessageSender* messa void OfflineMsgEngine::onReceiptReceived(ReceiptNum receipt) { QMutexLocker ml(&mutex); - if (receivedReceipts.contains(receipt)) { - qWarning() << "Receievd duplicate receipt" << receipt.get() << "from friend" << f->getId(); - return; - } - receivedReceipts.append(receipt); - checkForCompleteMessages(receipt); + receiptResolver.notifyReceiptReceived(receipt); +} + +void OfflineMsgEngine::onExtendedReceiptReceived(ExtendedReceiptNum receipt) +{ + QMutexLocker ml(&mutex); + extendedReceiptResolver.notifyReceiptReceived(receipt); } /** @@ -63,7 +62,7 @@ void OfflineMsgEngine::onReceiptReceived(ReceiptNum receipt) void OfflineMsgEngine::addUnsentMessage(Message const& message, CompletionFn completionCallback) { QMutexLocker ml(&mutex); - unsentMessages.append(OfflineMessage{message, std::chrono::steady_clock::now(), completionCallback}); + unsentMessages.push_back(OfflineMessage{message, std::chrono::steady_clock::now(), completionCallback}); } /** @@ -76,79 +75,51 @@ void OfflineMsgEngine::addUnsentMessage(Message const& message, CompletionFn com * @param[in] messageID database RowId of the message, used to eventually mark messages as received in history * @param[in] msg chat message line in the chatlog, used to eventually set the message's receieved timestamp */ -void OfflineMsgEngine::addSentMessage(ReceiptNum receipt, Message const& message, +void OfflineMsgEngine::addSentCoreMessage(ReceiptNum receipt, Message const& message, CompletionFn completionCallback) { QMutexLocker ml(&mutex); - assert(!sentMessages.contains(receipt)); - sentMessages.insert(receipt, {message, std::chrono::steady_clock::now(), completionCallback}); - checkForCompleteMessages(receipt); + receiptResolver.notifyMessageSent(receipt, {message, std::chrono::steady_clock::now(), completionCallback}); } -/** -* @brief Deliver all messages, used when a friend comes online. -*/ -void OfflineMsgEngine::deliverOfflineMsgs() +void OfflineMsgEngine::addSentExtendedMessage(ExtendedReceiptNum receipt, Message const& message, + CompletionFn completionCallback) { QMutexLocker ml(&mutex); - - if (!Status::isOnline(f->getStatus())) { - return; - } - - if (sentMessages.empty() && unsentMessages.empty()) { - return; - } - - QVector messages = sentMessages.values().toVector() + unsentMessages; - // order messages by authorship time to resend in same order as they were written - std::sort(messages.begin(), messages.end(), [](const OfflineMessage& lhs, const OfflineMessage& rhs) { - return lhs.authorshipTime < rhs.authorshipTime; - }); - removeAllMessages(); - - for (const auto& message : messages) { - QString messageText = message.message.content; - ReceiptNum receipt; - bool messageSent{false}; - if (message.message.isAction) { - messageSent = messageSender->sendAction(f->getId(), messageText, receipt); - } else { - messageSent = messageSender->sendMessage(f->getId(), messageText, receipt); - } - if (messageSent) { - addSentMessage(receipt, message.message, message.completionFn); - } else { - qCritical() << "deliverOfflineMsgs failed to send message"; - addUnsentMessage(message.message, message.completionFn); - } - } + extendedReceiptResolver.notifyMessageSent(receipt, {message, std::chrono::steady_clock::now(), completionCallback}); } /** * @brief Removes all messages which are being tracked. */ -void OfflineMsgEngine::removeAllMessages() +std::vector OfflineMsgEngine::removeAllMessages() { QMutexLocker ml(&mutex); - receivedReceipts.clear(); - sentMessages.clear(); + auto messages = receiptResolver.clear(); + auto extendedMessages = extendedReceiptResolver.clear(); + + messages.insert( + messages.end(), + std::make_move_iterator(extendedMessages.begin()), + std::make_move_iterator(extendedMessages.end())); + + messages.insert( + messages.end(), + std::make_move_iterator(unsentMessages.begin()), + std::make_move_iterator(unsentMessages.end())); + unsentMessages.clear(); -} -void OfflineMsgEngine::completeMessage(QMap::iterator msgIt) -{ - msgIt->completionFn(); - receivedReceipts.removeOne(msgIt.key()); - sentMessages.erase(msgIt); -} + std::sort(messages.begin(), messages.end(), [] (const OfflineMessage& a, const OfflineMessage& b) { + return a.authorshipTime < b.authorshipTime; + }); -void OfflineMsgEngine::checkForCompleteMessages(ReceiptNum receipt) -{ - auto msgIt = sentMessages.find(receipt); - const bool receiptReceived = receivedReceipts.contains(receipt); - if (!receiptReceived || msgIt == sentMessages.end()) { - return; - } - completeMessage(msgIt); + auto ret = std::vector(); + ret.reserve(messages.size()); + + std::transform(messages.begin(), messages.end(), std::back_inserter(ret), [](const OfflineMessage& msg) { + return RemovedMessage{msg.message, msg.completionFn}; + }); + + return ret; } diff --git a/src/persistence/offlinemsgengine.h b/src/persistence/offlinemsgengine.h index ed4ceb307..7e65bd94a 100644 --- a/src/persistence/offlinemsgengine.h +++ b/src/persistence/offlinemsgengine.h @@ -30,23 +30,27 @@ #include #include -class Friend; -class ICoreFriendMessageSender; class OfflineMsgEngine : public QObject { Q_OBJECT public: - explicit OfflineMsgEngine(Friend* f, ICoreFriendMessageSender* messageSender); - - using CompletionFn = std::function; + using CompletionFn = std::function; + OfflineMsgEngine(); void addUnsentMessage(Message const& message, CompletionFn completionCallback); - void addSentMessage(ReceiptNum receipt, Message const& message, CompletionFn completionCallback); - void deliverOfflineMsgs(); + void addSentCoreMessage(ReceiptNum receipt, Message const& message, CompletionFn completionCallback); + void addSentExtendedMessage(ExtendedReceiptNum receipt, Message const& message, CompletionFn completionCallback); + + struct RemovedMessage + { + Message message; + CompletionFn callback; + }; + std::vector removeAllMessages(); public slots: - void removeAllMessages(); void onReceiptReceived(ReceiptNum receipt); + void onExtendedReceiptReceived(ExtendedReceiptNum receipt); private: struct OfflineMessage @@ -56,16 +60,58 @@ private: CompletionFn completionFn; }; -private slots: - void completeMessage(QMap::iterator msgIt); - -private: - void checkForCompleteMessages(ReceiptNum receipt); - QMutex mutex; - const Friend* f; - ICoreFriendMessageSender* messageSender; - QVector receivedReceipts; - QMap sentMessages; - QVector unsentMessages; + + template + class ReceiptResolver + { + public: + void notifyMessageSent(ReceiptT receipt, OfflineMessage const& message) + { + auto receivedReceiptIt = std::find( + receivedReceipts.begin(), receivedReceipts.end(), receipt); + + if (receivedReceiptIt != receivedReceipts.end()) { + receivedReceipts.erase(receivedReceiptIt); + message.completionFn(true); + return; + } + + unAckedMessages[receipt] = message; + } + + void notifyReceiptReceived(ReceiptT receipt) + { + auto unackedMessageIt = unAckedMessages.find(receipt); + if (unackedMessageIt != unAckedMessages.end()) { + unackedMessageIt->second.completionFn(true); + unAckedMessages.erase(unackedMessageIt); + return; + } + + receivedReceipts.push_back(receipt); + } + + std::vector clear() + { + auto ret = std::vector(); + std::transform( + std::make_move_iterator(unAckedMessages.begin()), std::make_move_iterator(unAckedMessages.end()), + std::back_inserter(ret), + [] (const std::pair& pair) { + return std::move(pair.second); + }); + + receivedReceipts.clear(); + unAckedMessages.clear(); + return ret; + } + + std::vector receivedReceipts; + std::map unAckedMessages; + }; + + ReceiptResolver receiptResolver; + ReceiptResolver extendedReceiptResolver; + std::vector unsentMessages; }; diff --git a/src/persistence/profile.cpp b/src/persistence/profile.cpp index 62ea34cab..b7484aebe 100644 --- a/src/persistence/profile.cpp +++ b/src/persistence/profile.cpp @@ -712,7 +712,7 @@ void Profile::onRequestSent(const ToxPk& friendPk, const QString& message) const ToxPk selfPk = core->getSelfPublicKey(); const QDateTime datetime = QDateTime::currentDateTime(); const QString name = core->getUsername(); - history->addNewMessage(friendPk, inviteStr, selfPk, datetime, true, name); + history->addNewMessage(friendPk, inviteStr, selfPk, datetime, true, ExtensionSet(), name); } /** diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index b2357406a..8c05b33bb 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -142,9 +142,10 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::updateFriendActivityForFile); connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::updateFriendActivityForFile); connect(&core, &Core::friendTypingChanged, this, &ChatForm::onFriendTypingChanged); - connect(&core, &Core::friendStatusChanged, this, &ChatForm::onFriendStatusChanged); connect(coreFile, &CoreFile::fileNameChanged, this, &ChatForm::onFileNameChanged); + connect(chatFriend, &Friend::statusChanged, this, &ChatForm::onFriendStatusChanged); + const CoreAV* av = core.getAv(); connect(av, &CoreAV::avInvite, this, &ChatForm::onAvInvite); connect(av, &CoreAV::avStart, this, &ChatForm::onAvStart); @@ -423,12 +424,10 @@ void ChatForm::onVolMuteToggle() updateMuteVolButton(); } -void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status) +void ChatForm::onFriendStatusChanged(const ToxPk& friendPk, Status::Status status) { // Disable call buttons if friend is offline - if (friendId != f->getId()) { - return; - } + assert(friendPk == f->getPublicKey()); if (!Status::isOnline(f->getStatus())) { // Hide the "is typing" message when a friend goes offline diff --git a/src/widget/form/chatform.h b/src/widget/form/chatform.h index 318980393..f7cc09135 100644 --- a/src/widget/form/chatform.h +++ b/src/widget/form/chatform.h @@ -90,7 +90,7 @@ private slots: void onMicMuteToggle(); void onVolMuteToggle(); - void onFriendStatusChanged(quint32 friendId, Status::Status status); + void onFriendStatusChanged(const ToxPk& friendPk, Status::Status status); void onFriendTypingChanged(quint32 friendId, bool isTyping); void onFriendNameChanged(const QString& name); void onStatusMessage(const QString& message); diff --git a/src/widget/form/genericchatform.cpp b/src/widget/form/genericchatform.cpp index 10d974c1d..a7630d45b 100644 --- a/src/widget/form/genericchatform.cpp +++ b/src/widget/form/genericchatform.cpp @@ -190,6 +190,8 @@ void renderMessageRaw(const QString& displayName, bool isSelf, bool colorizeName if (chatMessage) { if (chatLogMessage.state == MessageState::complete) { chatMessage->markAsDelivered(chatLogMessage.message.timestamp); + } else if (chatLogMessage.state == MessageState::broken) { + chatMessage->markAsBroken(); } } else { chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage); diff --git a/src/widget/friendwidget.cpp b/src/widget/friendwidget.cpp index 6f65289c5..a462d584b 100644 --- a/src/widget/friendwidget.cpp +++ b/src/widget/friendwidget.cpp @@ -343,6 +343,8 @@ QString FriendWidget::getStatusString() const tr("Away"), tr("Busy"), tr("Offline"), + tr("Blocked"), + tr("Negotiating") }; return event ? tr("New message") : names.value(status); diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index cf36af646..a4ac71764 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -679,7 +679,7 @@ void Widget::onCoreChanged(Core& core) connect(&core, &Core::friendAdded, this, &Widget::addFriend); connect(&core, &Core::failedToAddFriend, this, &Widget::addFriendFailed); connect(&core, &Core::friendUsernameChanged, this, &Widget::onFriendUsernameChanged); - connect(&core, &Core::friendStatusChanged, this, &Widget::onFriendStatusChanged); + connect(&core, &Core::friendStatusChanged, this, &Widget::onCoreFriendStatusChanged); connect(&core, &Core::friendStatusMessageChanged, this, &Widget::onFriendStatusMessageChanged); connect(&core, &Core::friendRequestReceived, this, &Widget::onFriendRequestReceived); connect(&core, &Core::friendMessageReceived, this, &Widget::onFriendMessageReceived); @@ -1190,6 +1190,7 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk) friendAlertConnections.insert(friendPk, notifyReceivedConnection); connect(newfriend, &Friend::aliasChanged, this, &Widget::onFriendAliasChanged); connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged); + connect(newfriend, &Friend::statusChanged, this, &Widget::onFriendStatusChanged); connect(friendForm, &ChatForm::incomingNotification, this, &Widget::incomingNotification); connect(friendForm, &ChatForm::outgoingNotification, this, &Widget::outgoingNotification); @@ -1229,7 +1230,7 @@ void Widget::addFriendFailed(const ToxPk&, const QString& errorInfo) QMessageBox::critical(nullptr, "Error", info); } -void Widget::onFriendStatusChanged(int friendId, Status::Status status) +void Widget::onCoreFriendStatusChanged(int friendId, Status::Status status) { const auto& friendPk = FriendList::id2Key(friendId); Friend* f = FriendList::findFriend(friendPk); @@ -1237,18 +1238,35 @@ void Widget::onFriendStatusChanged(int friendId, Status::Status status) return; } - bool isActualChange = f->getStatus() != status; + auto const oldStatus = f->getStatus(); + f->setStatus(status); + auto const newStatus = f->getStatus(); - FriendWidget* widget = friendWidgets[f->getPublicKey()]; - if (isActualChange) { - if (!Status::isOnline(f->getStatus())) { - contactListWidget->moveWidget(widget, Status::Status::Online); - } else if (status == Status::Status::Offline) { - contactListWidget->moveWidget(widget, Status::Status::Offline); - } + auto const startedNegotiating = (newStatus == Status::Status::Negotiating && oldStatus != newStatus); + if (startedNegotiating) { + constexpr auto negotiationTimeoutMs = 1000; + auto timer = std::unique_ptr(new QTimer); + timer->setSingleShot(true); + timer->setInterval(negotiationTimeoutMs); + connect(timer.get(), &QTimer::timeout, f, &Friend::onNegotiationComplete); + timer->start(); + negotiateTimers[friendPk] = std::move(timer); + } + + // Any widget behavior will be triggered based off of the status + // transformations done by the Friend class +} + +void Widget::onFriendStatusChanged(const ToxPk& friendPk, Status::Status status) +{ + FriendWidget* widget = friendWidgets[friendPk]; + + if (Status::isOnline(status)) { + contactListWidget->moveWidget(widget, Status::Status::Online); + } else { + contactListWidget->moveWidget(widget, Status::Status::Offline); } - f->setStatus(status); widget->updateStatusLight(); if (widget->isActive()) { setWindowTitle(widget->getTitle()); @@ -1422,23 +1440,13 @@ void Widget::onExtendedMessageSupport(uint32_t friendNumber, bool compatible) void Widget::onFriendExtMessageReceived(uint32_t friendNumber, const QString& message) { const auto& friendKey = FriendList::id2Key(friendNumber); - Friend* f = FriendList::findFriend(friendKey); - if (!f) { - return; - } - - friendMessageDispatchers[f->getPublicKey()]->onExtMessageReceived(message); + friendMessageDispatchers[friendKey]->onExtMessageReceived(message); } void Widget::onExtReceiptReceived(uint32_t friendNumber, uint64_t receiptId) { const auto& friendKey = FriendList::id2Key(friendNumber); - Friend* f = FriendList::findFriend(friendKey); - if (!f) { - return; - } - - friendMessageDispatchers[f->getPublicKey()]->onExtReceiptReceived(receiptId); + friendMessageDispatchers[friendKey]->onExtReceiptReceived(receiptId); } void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog) @@ -2131,6 +2139,8 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId) &SessionChatLog::onMessageSent); connect(messageDispatcher.get(), &IMessageDispatcher::messageComplete, groupChatLog.get(), &SessionChatLog::onMessageComplete); + connect(messageDispatcher.get(), &IMessageDispatcher::messageBroken, groupChatLog.get(), + &SessionChatLog::onMessageBroken); auto notifyReceivedCallback = [this, groupId](const ToxPk& author, const Message& message) { auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(), diff --git a/src/widget/widget.h b/src/widget/widget.h index d000e5625..e9cfefbe3 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -165,7 +165,8 @@ public slots: void setStatusMessage(const QString& statusMessage); void addFriend(uint32_t friendId, const ToxPk& friendPk); void addFriendFailed(const ToxPk& userId, const QString& errorInfo = QString()); - void onFriendStatusChanged(int friendId, Status::Status status); + void onCoreFriendStatusChanged(int friendId, Status::Status status); + void onFriendStatusChanged(const ToxPk& friendPk, Status::Status status); void onFriendStatusMessageChanged(int friendId, const QString& message); void onFriendDisplayedNameChanged(const QString& displayed); void onFriendUsernameChanged(int friendId, const QString& username); diff --git a/test/model/friendmessagedispatcher_test.cpp b/test/model/friendmessagedispatcher_test.cpp index 665375944..a3956ee5b 100644 --- a/test/model/friendmessagedispatcher_test.cpp +++ b/test/model/friendmessagedispatcher_test.cpp @@ -25,6 +25,7 @@ #include #include +#include #include @@ -110,6 +111,9 @@ private slots: void testFailedMessage(); void testNegotiationFailure(); void testNegotiationSuccess(); + void testOfflineExtensionMessages(); + void testSentMessageExtensionSetReduced(); + void testActionMessagesSplitWithExtensions(); void onMessageSent(DispatchedMessageId id, Message message) { @@ -130,6 +134,11 @@ private slots: receivedMessages.push_back(std::move(message)); } + void onMessageBroken(DispatchedMessageId id, BrokenMessageReason) + { + brokenMessages.insert(id); + } + private: // All unique_ptrs to make construction/init() easier to manage std::unique_ptr f; @@ -139,6 +148,7 @@ private: std::unique_ptr messageProcessor; std::unique_ptr friendMessageDispatcher; std::map outgoingMessages; + std::set brokenMessages; std::deque receivedMessages; }; @@ -151,6 +161,7 @@ void TestFriendMessageDispatcher::init() { f = std::unique_ptr(new Friend(0, ToxPk())); f->setStatus(Status::Status::Online); + f->onNegotiationComplete(); messageSender = std::unique_ptr(new MockFriendMessageSender()); coreExtPacketAllocator = std::unique_ptr(new MockCoreExtPacketAllocator()); sharedProcessorParams = @@ -165,9 +176,12 @@ void TestFriendMessageDispatcher::init() &TestFriendMessageDispatcher::onMessageComplete); connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageReceived, this, &TestFriendMessageDispatcher::onMessageReceived); + connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageBroken, this, + &TestFriendMessageDispatcher::onMessageBroken); outgoingMessages = std::map(); receivedMessages = std::deque(); + brokenMessages = std::set(); } /** @@ -237,6 +251,7 @@ void TestFriendMessageDispatcher::testOfflineMessages() QVERIFY(outgoingMessages.size() == 3); f->setStatus(Status::Status::Online); + f->onNegotiationComplete(); QVERIFY(messageSender->numSentActions == 1); QVERIFY(messageSender->numSentMessages == 2); @@ -266,21 +281,34 @@ void TestFriendMessageDispatcher::testFailedMessage() messageSender->canSend = true; f->setStatus(Status::Status::Offline); f->setStatus(Status::Status::Online); + f->onNegotiationComplete(); QVERIFY(messageSender->numSentMessages == 1); } void TestFriendMessageDispatcher::testNegotiationFailure() { + f->setStatus(Status::Status::Offline); + f->setStatus(Status::Status::Online); + + QVERIFY(f->getStatus() == Status::Status::Negotiating); + friendMessageDispatcher->sendMessage(false, "test"); + QVERIFY(messageSender->numSentMessages == 0); + + f->onNegotiationComplete(); + QVERIFY(messageSender->numSentMessages == 1); - QVERIFY(coreExtPacketAllocator->numSentMessages == 0); } void TestFriendMessageDispatcher::testNegotiationSuccess() { + f->setStatus(Status::Status::Offline); + f->setStatus(Status::Status::Online); + f->setExtendedMessageSupport(true); + f->onNegotiationComplete(); friendMessageDispatcher->sendMessage(false, "test"); @@ -291,5 +319,74 @@ void TestFriendMessageDispatcher::testNegotiationSuccess() QVERIFY(messageSender->numSentMessages == 0); } +void TestFriendMessageDispatcher::testOfflineExtensionMessages() +{ + f->setStatus(Status::Status::Offline); + + auto requiredExtensions = ExtensionSet(); + requiredExtensions[ExtensionType::messages] = true; + + friendMessageDispatcher->sendExtendedMessage("Test", requiredExtensions); + + f->setStatus(Status::Status::Online); + f->setExtendedMessageSupport(true); + f->onNegotiationComplete(); + + // Ensure that when our friend came online with the desired extensions we + // were able to send them our message over the extended message path + QVERIFY(coreExtPacketAllocator->numSentMessages == 1); + + f->setStatus(Status::Status::Offline); + + friendMessageDispatcher->sendExtendedMessage("Test", requiredExtensions); + + f->setStatus(Status::Status::Online); + f->setExtendedMessageSupport(false); + f->onNegotiationComplete(); + + // Here we want to make sure that when they do _not_ support extensions + // we discard the message instead of attempting to send it over either + // channel + QVERIFY(coreExtPacketAllocator->numSentMessages == 1); + QVERIFY(messageSender->numSentMessages == 0); +} + +void TestFriendMessageDispatcher::testSentMessageExtensionSetReduced() +{ + f->setStatus(Status::Status::Online); + f->setExtendedMessageSupport(true); + f->onNegotiationComplete(); + + friendMessageDispatcher->sendMessage(false, "Test"); + + f->setStatus(Status::Status::Offline); + f->setStatus(Status::Status::Online); + f->setExtendedMessageSupport(false); + f->onNegotiationComplete(); + + // Ensure that when we reduce our extension set we correctly emit the + // "messageBroken" signal + QVERIFY(brokenMessages.size() == 1); +} + +void TestFriendMessageDispatcher::testActionMessagesSplitWithExtensions() +{ + f->setStatus(Status::Status::Online); + f->setExtendedMessageSupport(true); + f->onNegotiationComplete(); + + auto reallyLongMessage = QString("a"); + + for (int i = 0; i < 9999; ++i) { + reallyLongMessage += i; + } + + friendMessageDispatcher->sendMessage(true, reallyLongMessage); + + QVERIFY(coreExtPacketAllocator->numSentMessages == 0); + QVERIFY(messageSender->numSentMessages == 0); + QVERIFY(messageSender->numSentActions > 1); +} + QTEST_GUILESS_MAIN(TestFriendMessageDispatcher) #include "friendmessagedispatcher_test.moc" diff --git a/test/model/messageprocessor_test.cpp b/test/model/messageprocessor_test.cpp index 9041e7503..62a9fd9d2 100644 --- a/test/model/messageprocessor_test.cpp +++ b/test/model/messageprocessor_test.cpp @@ -133,10 +133,17 @@ void TestMessageProcessor::testOutgoingMessage() testStr += "a"; } - auto messages = messageProcessor.processOutgoingMessage(false, testStr, true /*needsSplit*/); + auto messages = messageProcessor.processOutgoingMessage(false, testStr, ExtensionSet()); // The message processor should split our messages QVERIFY(messages.size() == 2); + + auto extensionSet = ExtensionSet(); + extensionSet[ExtensionType::messages] = true; + messages = messageProcessor.processOutgoingMessage(false, testStr, extensionSet); + + // If we have multipart messages we shouldn't split our messages + QVERIFY(messages.size() == 1); } /** diff --git a/test/persistence/dbschema_test.cpp b/test/persistence/dbschema_test.cpp index be8745a43..428a7f159 100644 --- a/test/persistence/dbschema_test.cpp +++ b/test/persistence/dbschema_test.cpp @@ -51,6 +51,7 @@ private slots: void test2to3(); void test3to4(); void test4to5(); + void test5to6(); void cleanupTestCase(); private: bool initSucess{false}; @@ -66,7 +67,8 @@ const QString testFileList[] = { "test1to2.db", "test2to3.db", "test3to4.db", - "test4to5.db" + "test4to5.db", + "test5to6.db" }; // db schemas can be select with "SELECT name, sql FROM sqlite_master;" on the database. @@ -122,6 +124,17 @@ const std::vector schema5 { {"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"} }; +// added toxext extensions +const std::vector schema6 { + {"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name), FOREIGN KEY (owner) REFERENCES peers(id))"}, + {"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, required_extensions INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (id) REFERENCES history(id))"}, + {"file_transfers", "CREATE TABLE file_transfers (id INTEGER PRIMARY KEY, chat_id INTEGER NOT NULL, file_restart_id BLOB NOT NULL, file_name BLOB NOT NULL, file_path BLOB NOT NULL, file_hash BLOB NOT NULL, file_size INTEGER NOT NULL, direction INTEGER NOT NULL, file_state INTEGER NOT NULL)"}, + {"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER, FOREIGN KEY (file_id) REFERENCES file_transfers(id), FOREIGN KEY (chat_id) REFERENCES peers(id), FOREIGN KEY (sender_alias) REFERENCES aliases(id))"}, + {"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}, + {"broken_messages", "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, reason INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (id) REFERENCES history(id))"}, + {"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"} +}; + void TestDbSchema::initTestCase() { for (const auto& path : testFileList) { @@ -176,7 +189,7 @@ void TestDbSchema::testCreation() QVector queries; auto db = std::shared_ptr{new RawDatabase{"testCreation.db", {}, {}}}; QVERIFY(createCurrentSchema(*db)); - verifyDb(db, schema5); + verifyDb(db, schema6); } void TestDbSchema::testIsNewDb() @@ -374,5 +387,13 @@ void TestDbSchema::test4to5() verifyDb(db, schema5); } +void TestDbSchema::test5to6() +{ + auto db = std::shared_ptr{new RawDatabase{"test5to6.db", {}, {}}}; + createSchemaAtVersion(db, schema5); + QVERIFY(dbSchema5to6(*db)); + verifyDb(db, schema6); +} + QTEST_GUILESS_MAIN(TestDbSchema) #include "dbschema_test.moc" diff --git a/test/persistence/offlinemsgengine_test.cpp b/test/persistence/offlinemsgengine_test.cpp index cf4e634d3..63ed8905d 100644 --- a/test/persistence/offlinemsgengine_test.cpp +++ b/test/persistence/offlinemsgengine_test.cpp @@ -24,171 +24,164 @@ #include -struct MockFriendMessageSender : public QObject, public ICoreFriendMessageSender -{ - Q_OBJECT -public: - MockFriendMessageSender(Friend* f) - : f(f){} - bool sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt) override - { - return false; - } - bool sendMessage(uint32_t friendId, const QString& message, ReceiptNum& receipt) override - { - if (Status::isOnline(f->getStatus())) { - receipt.get() = receiptNum++; - if (!dropReceipts) { - msgs.push_back(message); - emit receiptReceived(receipt); - } - numMessagesSent++; - } else { - numMessagesFailed++; - } - return Status::isOnline(f->getStatus()); - } - -signals: - void receiptReceived(ReceiptNum receipt); - -public: - Friend* f; - bool dropReceipts = false; - size_t numMessagesSent = 0; - size_t numMessagesFailed = 0; - int receiptNum = 0; - std::vector msgs; -}; - class TestOfflineMsgEngine : public QObject { Q_OBJECT private slots: - void testReceiptResolution(); - void testOfflineFriend(); - void testSentUnsentCoordination(); + void testReceiptBeforeMessage(); + void testReceiptAfterMessage(); + void testResendWorkflow(); + void testTypeCoordination(); void testCallback(); + void testExtendedMessageCoordination(); }; -class OfflineMsgEngineFixture -{ -public: - OfflineMsgEngineFixture() - : f(0, ToxPk(QByteArray(32, 0))) - , friendMessageSender(&f) - , offlineMsgEngine(&f, &friendMessageSender) - { - f.setStatus(Status::Status::Online); - QObject::connect(&friendMessageSender, &MockFriendMessageSender::receiptReceived, - &offlineMsgEngine, &OfflineMsgEngine::onReceiptReceived); - } +void completionFn(bool) {} - Friend f; - MockFriendMessageSender friendMessageSender; +void TestOfflineMsgEngine::testReceiptBeforeMessage() +{ OfflineMsgEngine offlineMsgEngine; -}; - -void completionFn() {} - -void TestOfflineMsgEngine::testReceiptResolution() -{ - OfflineMsgEngineFixture fixture; Message msg{false, QString(), QDateTime()}; - ReceiptNum receipt; - fixture.friendMessageSender.sendMessage(0, msg.content, receipt); - fixture.offlineMsgEngine.addSentMessage(receipt, msg, completionFn); + auto const receipt = ReceiptNum(0); + offlineMsgEngine.onReceiptReceived(receipt); + offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn); - // We should have no offline messages to deliver if we resolved our receipt - // correctly - fixture.offlineMsgEngine.deliverOfflineMsgs(); - fixture.offlineMsgEngine.deliverOfflineMsgs(); - fixture.offlineMsgEngine.deliverOfflineMsgs(); + auto const removedMessages = offlineMsgEngine.removeAllMessages(); - QVERIFY(fixture.friendMessageSender.numMessagesSent == 1); - - // If we drop receipts we should keep trying to send messages every time we - // "deliverOfflineMsgs" - fixture.friendMessageSender.dropReceipts = true; - fixture.friendMessageSender.sendMessage(0, msg.content, receipt); - fixture.offlineMsgEngine.addSentMessage(receipt, msg, completionFn); - fixture.offlineMsgEngine.deliverOfflineMsgs(); - fixture.offlineMsgEngine.deliverOfflineMsgs(); - fixture.offlineMsgEngine.deliverOfflineMsgs(); - - QVERIFY(fixture.friendMessageSender.numMessagesSent == 5); - - // And once we stop dropping and try one more time we should run out of - // messages to send again - fixture.friendMessageSender.dropReceipts = false; - fixture.offlineMsgEngine.deliverOfflineMsgs(); - fixture.offlineMsgEngine.deliverOfflineMsgs(); - fixture.offlineMsgEngine.deliverOfflineMsgs(); - QVERIFY(fixture.friendMessageSender.numMessagesSent == 6); + QVERIFY(removedMessages.empty()); } -void TestOfflineMsgEngine::testOfflineFriend() +void TestOfflineMsgEngine::testReceiptAfterMessage() { - OfflineMsgEngineFixture fixture; + OfflineMsgEngine offlineMsgEngine; - Message msg{false, QString(), QDateTime()}; + auto const receipt = ReceiptNum(0); + offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn); + offlineMsgEngine.onReceiptReceived(receipt); - fixture.f.setStatus(Status::Status::Offline); + auto const removedMessages = offlineMsgEngine.removeAllMessages(); - fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn); - fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn); - fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn); - fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn); - fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn); - - fixture.f.setStatus(Status::Status::Online); - fixture.offlineMsgEngine.deliverOfflineMsgs(); - - - QVERIFY(fixture.friendMessageSender.numMessagesFailed == 0); - QVERIFY(fixture.friendMessageSender.numMessagesSent == 5); + QVERIFY(removedMessages.empty()); } -void TestOfflineMsgEngine::testSentUnsentCoordination() +void TestOfflineMsgEngine::testResendWorkflow() { - OfflineMsgEngineFixture fixture; - Message msg{false, QString("a"), QDateTime()}; - ReceiptNum receipt; + OfflineMsgEngine offlineMsgEngine; - fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn); - msg.content = "b"; - fixture.friendMessageSender.dropReceipts = true; - fixture.friendMessageSender.sendMessage(0, msg.content, receipt); - fixture.friendMessageSender.dropReceipts = false; - fixture.offlineMsgEngine.addSentMessage(receipt, msg, completionFn); - msg.content = "c"; - fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn); + auto const receipt = ReceiptNum(0); + offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn); + auto messagesToResend = offlineMsgEngine.removeAllMessages(); - fixture.offlineMsgEngine.deliverOfflineMsgs(); + QVERIFY(messagesToResend.size() == 1); - auto expectedResponseOrder = std::vector{"a", "b", "c"}; - QVERIFY(fixture.friendMessageSender.msgs == expectedResponseOrder); + offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn); + offlineMsgEngine.onReceiptReceived(receipt); + + messagesToResend = offlineMsgEngine.removeAllMessages(); + QVERIFY(messagesToResend.size() == 0); + + auto const nullMsg = Message(); + auto msg2 = Message(); + auto msg3 = Message(); + msg2.content = "msg2"; + msg3.content = "msg3"; + offlineMsgEngine.addSentCoreMessage(ReceiptNum(0), nullMsg, completionFn); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), nullMsg, completionFn); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), msg2, completionFn); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(3), msg3, completionFn); + + offlineMsgEngine.onReceiptReceived(ReceiptNum(0)); + offlineMsgEngine.onReceiptReceived(ReceiptNum(1)); + offlineMsgEngine.onReceiptReceived(ReceiptNum(3)); + + messagesToResend = offlineMsgEngine.removeAllMessages(); + QVERIFY(messagesToResend.size() == 1); + QVERIFY(messagesToResend[0].message.content == "msg2"); +} + + +void TestOfflineMsgEngine::testTypeCoordination() +{ + OfflineMsgEngine offlineMsgEngine; + + auto msg1 = Message(); + auto msg2 = Message(); + auto msg3 = Message(); + auto msg4 = Message(); + auto msg5 = Message(); + + msg1.content = "msg1"; + msg2.content = "msg2"; + msg3.content = "msg3"; + msg4.content = "msg4"; + msg5.content = "msg5"; + + offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), msg1, completionFn); + offlineMsgEngine.addUnsentMessage(msg2, completionFn); + offlineMsgEngine.addSentExtendedMessage(ExtendedReceiptNum(1), msg3, completionFn); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), msg4, completionFn); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(3), msg5, completionFn); + + const auto messagesToResend = offlineMsgEngine.removeAllMessages(); + + QVERIFY(messagesToResend[0].message.content == "msg1"); + QVERIFY(messagesToResend[1].message.content == "msg2"); + QVERIFY(messagesToResend[2].message.content == "msg3"); + QVERIFY(messagesToResend[3].message.content == "msg4"); + QVERIFY(messagesToResend[4].message.content == "msg5"); } void TestOfflineMsgEngine::testCallback() { - OfflineMsgEngineFixture fixture; + OfflineMsgEngine offlineMsgEngine; size_t numCallbacks = 0; - auto callback = [&numCallbacks] { numCallbacks++; }; + auto callback = [&numCallbacks] (bool) { numCallbacks++; }; Message msg{false, QString(), QDateTime()}; ReceiptNum receipt; - fixture.friendMessageSender.sendMessage(0, msg.content, receipt); - fixture.offlineMsgEngine.addSentMessage(receipt, msg, callback); - fixture.offlineMsgEngine.addUnsentMessage(msg, callback); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), Message(), callback); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), Message(), callback); + + offlineMsgEngine.onReceiptReceived(ReceiptNum(1)); + offlineMsgEngine.onReceiptReceived(ReceiptNum(2)); - fixture.offlineMsgEngine.deliverOfflineMsgs(); QVERIFY(numCallbacks == 2); } +void TestOfflineMsgEngine::testExtendedMessageCoordination() +{ + OfflineMsgEngine offlineMsgEngine; + + size_t numCallbacks = 0; + auto callback = [&numCallbacks] (bool) { numCallbacks++; }; + + auto msg1 = Message(); + auto msg2 = Message(); + auto msg3 = Message(); + + offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), msg1, callback); + offlineMsgEngine.addSentExtendedMessage(ExtendedReceiptNum(1), msg1, callback); + offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), msg1, callback); + + offlineMsgEngine.onExtendedReceiptReceived(ExtendedReceiptNum(2)); + QVERIFY(numCallbacks == 0); + + offlineMsgEngine.onReceiptReceived(ReceiptNum(1)); + QVERIFY(numCallbacks == 1); + + offlineMsgEngine.onReceiptReceived(ReceiptNum(1)); + QVERIFY(numCallbacks == 1); + + offlineMsgEngine.onExtendedReceiptReceived(ExtendedReceiptNum(1)); + QVERIFY(numCallbacks == 2); + + offlineMsgEngine.onReceiptReceived(ReceiptNum(2)); + QVERIFY(numCallbacks == 3); +} + QTEST_GUILESS_MAIN(TestOfflineMsgEngine) #include "offlinemsgengine_test.moc" From b71581501165ccb48c29799ddb857489c28a2736 Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Thu, 14 Nov 2019 19:08:40 -0800 Subject: [PATCH 3/6] feat(extensions): UI updates for extension support Added a UI element to indicate extension support of the chatroom. For all groups it will always be red since we do not support extensions in groups. In a 1-1 chat the indicator will either be green/yellow/red depending on if the friend has support for all/some/none of qTox's desired extension set. --- CMakeLists.txt | 2 + img/status/extensions_available.svg | 1 + img/status/extensions_partial.svg | 1 + img/status/extensions_unavailable.svg | 1 + res.qrc | 3 ++ src/widget/chatformheader.cpp | 18 ++++++++- src/widget/chatformheader.h | 8 ++++ src/widget/extensionstatus.cpp | 54 +++++++++++++++++++++++++++ src/widget/extensionstatus.h | 34 +++++++++++++++++ src/widget/form/chatform.cpp | 7 ++++ src/widget/form/chatform.h | 1 + 11 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 img/status/extensions_available.svg create mode 100644 img/status/extensions_partial.svg create mode 100644 img/status/extensions_unavailable.svg create mode 100644 src/widget/extensionstatus.cpp create mode 100644 src/widget/extensionstatus.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 152a3e36a..7f5434c1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -362,6 +362,8 @@ set(${PROJECT_NAME}_SOURCES src/widget/contentlayout.h src/widget/emoticonswidget.cpp src/widget/emoticonswidget.h + src/widget/extensionstatus.cpp + src/widget/extensionstatus.h src/widget/flowlayout.cpp src/widget/flowlayout.h src/widget/searchform.cpp diff --git a/img/status/extensions_available.svg b/img/status/extensions_available.svg new file mode 100644 index 000000000..e5025ae68 --- /dev/null +++ b/img/status/extensions_available.svg @@ -0,0 +1 @@ + diff --git a/img/status/extensions_partial.svg b/img/status/extensions_partial.svg new file mode 100644 index 000000000..14f86fdae --- /dev/null +++ b/img/status/extensions_partial.svg @@ -0,0 +1 @@ + diff --git a/img/status/extensions_unavailable.svg b/img/status/extensions_unavailable.svg new file mode 100644 index 000000000..7734fdecd --- /dev/null +++ b/img/status/extensions_unavailable.svg @@ -0,0 +1 @@ + diff --git a/res.qrc b/res.qrc index 9d200bef1..bb20bf79f 100644 --- a/res.qrc +++ b/res.qrc @@ -28,6 +28,9 @@ img/status/offline_notification.svg img/status/online.svg img/status/online_notification.svg + img/status/extensions_available.svg + img/status/extensions_partial.svg + img/status/extensions_unavailable.svg img/taskbar/dark/taskbar_online.svg img/taskbar/dark/taskbar_online_event.svg img/taskbar/dark/taskbar_away.svg diff --git a/src/widget/chatformheader.cpp b/src/widget/chatformheader.cpp index 6cd708054..b0f770e59 100644 --- a/src/widget/chatformheader.cpp +++ b/src/widget/chatformheader.cpp @@ -18,6 +18,9 @@ */ #include "chatformheader.h" +#include "extensionstatus.h" + +#include "src/model/status.h" #include "src/widget/gui.h" #include "src/widget/maskablepixmapwidget.h" @@ -117,6 +120,11 @@ ChatFormHeader::ChatFormHeader(QWidget* parent) avatar = new MaskablePixmapWidget(this, AVATAR_SIZE, ":/img/avatar_mask.svg"); avatar->setObjectName("avatar"); + nameLine = new QHBoxLayout(); + nameLine->setSpacing(3); + + extensionStatus = new ExtensionStatus(); + nameLabel = new CroppingLabel(); nameLabel->setObjectName("nameLabel"); nameLabel->setMinimumHeight(Style::getFont(Style::Medium).pixelSize()); @@ -124,9 +132,12 @@ ChatFormHeader::ChatFormHeader(QWidget* parent) nameLabel->setTextFormat(Qt::PlainText); connect(nameLabel, &CroppingLabel::editFinished, this, &ChatFormHeader::nameChanged); + nameLine->addWidget(extensionStatus); + nameLine->addWidget(nameLabel); + headTextLayout = new QVBoxLayout(); headTextLayout->addStretch(); - headTextLayout->addWidget(nameLabel); + headTextLayout->addLayout(nameLine); headTextLayout->addStretch(); micButton = createButton("micButton", this, &ChatFormHeader::micMuteToggle); @@ -223,6 +234,11 @@ void ChatFormHeader::removeCallConfirm() callConfirm.reset(nullptr); } +void ChatFormHeader::updateExtensionSupport(ExtensionSet extensions) +{ + extensionStatus->onExtensionSetUpdate(extensions); +} + void ChatFormHeader::updateCallButtons(bool online, bool audio, bool video) { const bool audioAvaliable = online && (mode & Mode::Audio); diff --git a/src/widget/chatformheader.h b/src/widget/chatformheader.h index 7e39a8f23..aac2477d9 100644 --- a/src/widget/chatformheader.h +++ b/src/widget/chatformheader.h @@ -21,14 +21,19 @@ #include +#include "src/core/extension.h" + #include class MaskablePixmapWidget; class QVBoxLayout; +class QHBoxLayout; class CroppingLabel; class QPushButton; class QToolButton; class CallConfirmWidget; +class QLabel; +class ExtensionStatus; class ChatFormHeader : public QWidget { @@ -64,6 +69,7 @@ public: void showCallConfirm(); void removeCallConfirm(); + void updateExtensionSupport(ExtensionSet extensions); void updateCallButtons(bool online, bool audio, bool video = false); void updateMuteMicButton(bool active, bool inputMuted); void updateMuteVolButton(bool active, bool outputMuted); @@ -98,6 +104,8 @@ private: Mode mode; MaskablePixmapWidget* avatar; QVBoxLayout* headTextLayout; + QHBoxLayout* nameLine; + ExtensionStatus* extensionStatus; CroppingLabel* nameLabel; QPushButton* callButton; diff --git a/src/widget/extensionstatus.cpp b/src/widget/extensionstatus.cpp new file mode 100644 index 000000000..d81784623 --- /dev/null +++ b/src/widget/extensionstatus.cpp @@ -0,0 +1,54 @@ +/* + Copyright © 2019-2020 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 "extensionstatus.h" + +#include + +ExtensionStatus::ExtensionStatus(QWidget* parent) + : QLabel(parent) +{ + // Initialize with 0 extensions + onExtensionSetUpdate(ExtensionSet()); +} + +void ExtensionStatus::onExtensionSetUpdate(ExtensionSet extensionSet) +{ + QString iconName; + QString hoverText; + if (extensionSet.all()) { + iconName = ":/img/status/extensions_available.svg"; + hoverText = tr("All extensions supported"); + } else if (extensionSet.none()) { + iconName = ":/img/status/extensions_unavailable.svg"; + hoverText = tr("No extensions supported"); + } else { + iconName = ":/img/status/extensions_partial.svg"; + hoverText = tr("Not all extensions supported"); + } + + hoverText += "\n"; + hoverText += tr("Multipart Messages: "); + hoverText += extensionSet[ExtensionType::messages] ? "✔" : "❌"; + + auto pixmap = QIcon(iconName).pixmap(QSize(16, 16)); + + setPixmap(pixmap); + setToolTip(hoverText); +} diff --git a/src/widget/extensionstatus.h b/src/widget/extensionstatus.h new file mode 100644 index 000000000..759f14c3c --- /dev/null +++ b/src/widget/extensionstatus.h @@ -0,0 +1,34 @@ +/* + Copyright © 2019-2020 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 . +*/ + +#pragma once + +#include "src/core/extension.h" + +#include + +class ExtensionStatus : public QLabel +{ + Q_OBJECT +public: + ExtensionStatus(QWidget* parent = nullptr); + +public slots: + void onExtensionSetUpdate(ExtensionSet extensionSet); +}; diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index 8c05b33bb..fb9a83048 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -181,6 +181,8 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes connect(bodySplitter, &QSplitter::splitterMoved, this, &ChatForm::onSplitterMoved); + connect(f, &Friend::extensionSupportChanged, this, &ChatForm::onExtensionSupportChanged); + updateCallButtons(); setAcceptDrops(true); @@ -224,6 +226,11 @@ void ChatForm::onFileNameChanged(const ToxPk& friendPk) "so you can save the file on Windows.")); } +void ChatForm::onExtensionSupportChanged(ExtensionSet extensions) +{ + headWidget->updateExtensionSupport(extensions); +} + void ChatForm::onTextEditChanged() { if (!Settings::getInstance().getTypingNotification()) { diff --git a/src/widget/form/chatform.h b/src/widget/form/chatform.h index f7cc09135..bb940e5ad 100644 --- a/src/widget/form/chatform.h +++ b/src/widget/form/chatform.h @@ -72,6 +72,7 @@ public slots: void onAvEnd(uint32_t friendId, bool error); void onAvatarChanged(const ToxPk& friendPk, const QPixmap& pic); void onFileNameChanged(const ToxPk& friendPk); + void onExtensionSupportChanged(ExtensionSet extensions); void clearChatArea(); void onShowMessagesClicked(); void onSplitterMoved(int pos, int index); From 670457a77bcb42f22153f0a08f8ecb351cf230ff Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Sun, 17 Nov 2019 14:00:25 -0800 Subject: [PATCH 4/6] fix(extensions): Add toxext to CI scripts --- .travis/build-ubuntu-16-04.sh | 34 ++++++++-- appimage/build-appimage.sh | 4 +- bootstrap-osx.sh | 2 +- bootstrap.sh | 75 ++++++++++++++++++++++ flatpak/io.github.qtox.qTox.json | 24 +++++++ osx/qTox-Mac-Deployer-ULTIMATE.sh | 102 ++++++++++++++++++++++++++++- windows/cross-compile/build.sh | 103 ++++++++++++++++++++++++++++++ 7 files changed, 332 insertions(+), 12 deletions(-) diff --git a/.travis/build-ubuntu-16-04.sh b/.travis/build-ubuntu-16-04.sh index 985b7c620..d1387140f 100755 --- a/.travis/build-ubuntu-16-04.sh +++ b/.travis/build-ubuntu-16-04.sh @@ -126,16 +126,18 @@ CC="ccache $CC" CXX="ccache $CXX" make -j$(nproc) sudo checkinstall --install --pkgname libsodium --pkgversion 1.0.8 --nodoc -y sudo ldconfig cd .. + # toxcore git clone --branch v0.2.12 --depth=1 https://github.com/toktok/c-toxcore.git toxcore cd toxcore -autoreconf -if -CC="ccache $CC" CXX="ccache $CXX" ./configure -CC="ccache $CC" CXX="ccache $CXX" make -j$(nproc) > /dev/null +mkdir build-cmake +cd build-cmake +CC="ccache $CC" CXX="ccache $CXX" cmake .. +make -j$(nproc) > /dev/null sudo make install echo '/usr/local/lib/' | sudo tee -a /etc/ld.so.conf.d/locallib.conf sudo ldconfig -cd .. +cd ../.. # filteraudio git clone --branch v0.0.1 --depth=1 https://github.com/irungentoo/filter_audio filteraudio @@ -144,12 +146,32 @@ CC="ccache $CC" CXX="ccache $CXX" sudo make install -j$(nproc) sudo ldconfig cd .. -$CC --version -$CXX --version +# toxext +git clone --branch v0.0.2 --depth=1 https://github.com/toxext/toxext toxext +cd toxext +mkdir build +cd build +cmake .. +make -j$(nproc) +sudo make install +cd ../../ + +# toxext_messages +git clone --branch v0.0.2 --depth=1 https://github.com/toxext/tox_extension_messages tox_extension_messages +cd tox_extension_messages +mkdir build +cd build +cmake .. +make -j$(nproc) +sudo make install +cd ../../ # needed, otherwise ffmpeg doesn't get detected export PKG_CONFIG_PATH="$PWD/libs/lib/pkgconfig" +$CC --version +$CXX --version + build_qtox() { bdir() { cd $BUILDDIR diff --git a/appimage/build-appimage.sh b/appimage/build-appimage.sh index 26c30217c..05b0028ba 100755 --- a/appimage/build-appimage.sh +++ b/appimage/build-appimage.sh @@ -29,7 +29,7 @@ readonly DEBUG="$1" -# Set this to True to upload the PR version of the +# Set this to True to upload the PR version of the # AppImage to transfer.sh for testing. readonly UPLOAD_PR_APPIMAGE="False" @@ -80,7 +80,7 @@ then # the .zsync meta file as the given name below with .zsync # extension. readonly OUTFILE=./output/qTox-"$TRAVIS_TAG".x86_64.AppImage - + # just check if the files are in the right place eval "ls $OUTFILE" eval "ls $OUTFILE.zsync" diff --git a/bootstrap-osx.sh b/bootstrap-osx.sh index 057a2626a..ddbfc20b8 100755 --- a/bootstrap-osx.sh +++ b/bootstrap-osx.sh @@ -52,7 +52,7 @@ copy_libs() { echo Copying libraries… for lib in "${libs[@]}" do - cp -v "$lib" "$dest" + cp -v -r "$lib" "$dest" done } diff --git a/bootstrap.sh b/bootstrap.sh index 7c3a378dc..145cfdfa4 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -49,14 +49,20 @@ readonly BASE_DIR="${SCRIPT_DIR}/${INSTALL_DIR}" # versions of libs to checkout readonly TOXCORE_VERSION="v0.2.12" +readonly TOXEXT_VERSION="v0.0.2" +readonly TOX_EXT_MESSAGES_VERSION="v0.0.2" readonly SQLCIPHER_VERSION="v4.3.0" # directory names of cloned repositories readonly TOXCORE_DIR="libtoxcore-$TOXCORE_VERSION" +readonly TOXEXT_DIR="toxext-$TOXEXT_VERSION" +readonly TOX_EXT_MESSAGES_DIR="tox_ext_messages-$TOXEXT_VERSION" readonly SQLCIPHER_DIR="sqlcipher-$SQLCIPHER_VERSION" # default values for user given parameters INSTALL_TOX=true +INSTALL_TOXEXT=true +INSTALL_TOX_EXT_MESSAGES=true INSTALL_SQLCIPHER=false SYSTEM_WIDE=true KEEP_BUILD_FILES=false @@ -128,6 +134,57 @@ install_toxcore() { fi } +install_toxext() { + if [[ $INSTALL_TOXEXT = "true" ]] + then + git clone https://github.com/toxext/toxext.git \ + --branch $TOXEXT_VERSION \ + "${BASE_DIR}/${TOXEXT_DIR}" + + pushd ${BASE_DIR}/${TOXEXT_DIR} + + # compile and install + if [[ $SYSTEM_WIDE = "false" ]] + then + cmake . -DCMAKE_INSTALL_PREFIX=${BASE_DIR} + make -j $(nproc) + make install + else + cmake . + make -j $(nproc) + sudo make install + sudo ldconfig + fi + + popd + fi +} + +install_tox_ext_messages() { + if [[ $INSTALL_TOX_EXT_MESSAGES = "true" ]] + then + git clone https://github.com/toxext/tox_extension_messages.git \ + --branch $TOX_EXT_MESSAGES_VERSION \ + "${BASE_DIR}/${TOX_EXT_MESSAGES_DIR}" + + pushd ${BASE_DIR}/${TOX_EXT_MESSAGES_DIR} + + # compile and install + if [[ $SYSTEM_WIDE = "false" ]] + then + cmake . -DCMAKE_INSTALL_PREFIX=${BASE_DIR} + make -j $(nproc) + make install + else + cmake . + make -j $(nproc) + sudo make install + sudo ldconfig + fi + + popd + fi +} install_sqlcipher() { if [[ $INSTALL_SQLCIPHER = "true" ]] @@ -178,6 +235,22 @@ main() { then INSTALL_TOX=false shift + elif [ ${1} = "--with-toxext" ] + then + INSTALL_TOXEXT=true + shift + elif [ ${1} = "--without-toxext" ] + then + INSTALL_TOXEXT=false + shift + elif [ ${1} = "--with-toxext-messages" ] + then + INSTALL_TOX_EXT_MESSAGES=true + shift + elif [ ${1} = "--without-toxext-messages" ] + then + INSTALL_TOX_EXT_MESSAGES=false + shift elif [ ${1} = "--with-sqlcipher" ] then INSTALL_SQLCIPHER=true @@ -221,6 +294,8 @@ main() { ############### install step ############### install_toxcore + install_toxext + install_tox_ext_messages install_sqlcipher ############### cleanup step ############### diff --git a/flatpak/io.github.qtox.qTox.json b/flatpak/io.github.qtox.qTox.json index 2d05791b8..47e18f84f 100644 --- a/flatpak/io.github.qtox.qTox.json +++ b/flatpak/io.github.qtox.qTox.json @@ -117,6 +117,30 @@ } ] }, + { + "name": "toxext", + "buildsystem": "cmake-ninja", + "sources": [ + { + "type": "git", + "url": "https://github.com/toxext/toxext", + "tag": "v0.0.2", + "commit": "0280357a0dded4dd46d0ff29f52875687136472d" + } + ] + }, + { + "name": "tox_extension_messages", + "buildsystem": "cmake-ninja", + "sources": [ + { + "type": "git", + "url": "https://github.com/toxext/tox_extension_messages", + "tag": "v0.0.2", + "commit": "f1f4539cf1aeed0bcc0ad476fbae74cb5bd0cf66" + } + ] + }, { "name": "qTox", "buildsystem": "cmake-ninja", diff --git a/osx/qTox-Mac-Deployer-ULTIMATE.sh b/osx/qTox-Mac-Deployer-ULTIMATE.sh index 6fb6aef2e..d2121ebc0 100755 --- a/osx/qTox-Mac-Deployer-ULTIMATE.sh +++ b/osx/qTox-Mac-Deployer-ULTIMATE.sh @@ -41,6 +41,8 @@ QT_VER=($(ls ${QT_DIR} | sed -n -e 's/^\([0-9]*\.([0-9]*\.([0-9]*\).*/\1/' -e '1 QT_DIR_VER="${QT_DIR}/${QT_VER[1]}" TOXCORE_DIR="${MAIN_DIR}/toxcore" # Change to Git location +TOX_EXT_DIR="${MAIN_DIR}/toxext" +TOX_EXT_MESSAGES_DIR="${MAIN_DIR}/tox_extension_messages" LIB_INSTALL_PREFIX="${QTOX_DIR}/libs" @@ -80,6 +82,43 @@ build_toxcore() { make install > /dev/null || exit 1 } +build_toxext() { + echo "Starting Toxext build and install" + cd $TOX_EXT_DIR + echo "Now working in: ${PWD}" + + [[ $TRAVIS != true ]] \ + && sleep 3 + + mkdir _build && cd _build + fcho "Starting cmake ..." + PKG_CONFIG_PATH="${LIB_INSTALL_PREFIX}"/lib/pkgconfig cmake -DCMAKE_INSTALL_PREFIX="${LIB_INSTALL_PREFIX}" .. + make clean &> /dev/null + fcho "Compiling toxext." + make > /dev/null || exit 1 + fcho "Installing toxext." + make install > /dev/null || exit 1 +} + +build_tox_extension_messages() { + echo "Starting tox_extension_messages build and install" + cd $TOX_EXT_MESSAGES_DIR + echo "Now working in: ${PWD}" + + [[ $TRAVIS != true ]] \ + && sleep 3 + + mkdir _build && cd _build + fcho "Starting cmake ..." + PKG_CONFIG_PATH="${LIB_INSTALL_PREFIX}"/lib/pkgconfig cmake -DCMAKE_INSTALL_PREFIX="${LIB_INSTALL_PREFIX}" .. + make clean &> /dev/null + fcho "Compiling tox_extension_messages." + make > /dev/null || exit 1 + fcho "Installing tox_extension_messages." + make install > /dev/null || exit 1 +} + + install() { fcho "==============================" fcho "This script will install the necessary applications and libraries needed to compile qTox properly." @@ -145,6 +184,26 @@ install() { fcho "Cloning Toxcore git ... " git clone --branch v0.2.12 --depth=1 https://github.com/toktok/c-toxcore "$TOXCORE_DIR" fi + # toxext + if [[ -e $TOX_EXT_DIR/.git/index ]] + then + fcho "ToxExt git repo already in place !" + cd $TOX_EXT_DIR + git pull + else + fcho "Cloning ToxExt git ... " + git clone --branch v0.0.2 --depth=1 https://github.com/toxext/toxext "$TOX_EXT_DIR" + fi + # tox_extension_messages + if [[ -e $TOX_EXT_MESSAGES_DIR/.git/index ]] + then + fcho "ToxExt git repo already in place !" + cd $TOX_EXT_MESSAGES_DIR + git pul + else + fcho "Cloning tox_extension_messages git ... " + git clone --branch v0.0.2 --depth=1 https://github.com/toxext/tox_extension_messages "$TOX_EXT_MESSAGES_DIR" + fi # qTox if [[ $TRAVIS = true ]] then @@ -161,12 +220,16 @@ install() { fi fi + if [[ $TRAVIS != true ]] + then + fcho "If all went well you should now have all the tools needed to compile qTox!" + fi + # toxcore build if [[ $TRAVIS = true ]] then build_toxcore else - fcho "If all went well you should now have all the tools needed to compile qTox!" read -r -p "Would you like to install toxcore now? [y/N] " response if [[ $response =~ ^([yY][eE][sS]|[yY])$ ]] then @@ -176,6 +239,34 @@ install() { fi fi + # toxext build + if [[ $TRAVIS = true ]] + then + build_toxext + else + read -r -p "Would you like to install toxext now? [y/N] " response + if [[ $response =~ ^([yY][eE][sS]|[yY])$ ]] + then + build_toxext + else + fcho "You can simply use the -u command and say [Yes/n] when prompted" + fi + fi + + # tox_extension_messages build + if [[ $TRAVIS = true ]] + then + build_tox_extension_messages + else + read -r -p "Would you like to install tox_extension_messages now? [y/N] " response + if [[ $response =~ ^([yY][eE][sS]|[yY])$ ]] + then + build_tox_extension_messages + else + fcho "You can simply use the -u command and say [Yes/n] when prompted" + fi + fi + QT_VER=($(ls ${QT_DIR} | sed -n -e 's/^\([0-9]*\.([0-9]*\.([0-9]*\).*/\1/' -e '1p;$p')) QT_DIR_VER="${QT_DIR}/${QT_VER[1]}" @@ -230,7 +321,6 @@ build() { cd $BUILD_DIR fcho "Now working in ${PWD}" fcho "Starting cmake ..." - export CMAKE_PREFIX_PATH=$(brew --prefix qt5) if [[ $TRAVIS = true ]] then @@ -238,7 +328,11 @@ build() { else STRICT_OPTIONS="OFF" fi - cmake -H$QTOX_DIR -B. -DUPDATE_CHECK=ON -DSPELL_CHECK=OFF -DSTRICT_OPTIONS="${STRICT_OPTIONS}" + cmake -H$QTOX_DIR -B. \ + -DUPDATE_CHECK=ON \ + -DSPELL_CHECK=OFF \ + -DSTRICT_OPTIONS="${STRICT_OPTIONS}" \ + -DCMAKE_PREFIX_PATH="$(brew --prefix qt5);${LIB_INSTALL_PREFIX}" make -j$(sysctl -n hw.ncpu) } @@ -262,6 +356,8 @@ bootstrap() { #Toxcore build_toxcore + build_toxext + build_tox_extension_messages #Boot Strap fcho "Running: sudo ${QTOX_DIR}/bootstrap-osx.sh" diff --git a/windows/cross-compile/build.sh b/windows/cross-compile/build.sh index 4a7aa4669..d4f291e60 100644 --- a/windows/cross-compile/build.sh +++ b/windows/cross-compile/build.sh @@ -1030,6 +1030,109 @@ else echo "Using cached build of Toxcore `cat $TOXCORE_PREFIX_DIR/done`" fi +# toxext +TOXEXT_PREFIX_DIR="$DEP_DIR/toxext" +TOXEXT_VERSION=0.0.2 +TOXEXT_HASH="047093eeed396ea9b4a3f0cd0a6bc4e0e09b339e2b03ba4b676e30888fe6acde" +TOXEXT_FILENAME="toxext-$TOXEXT_VERSION.tar.gz" +if [ ! -f "$TOXEXT_PREFIX_DIR/done" ] +then + rm -rf "$TOXEXT_PREFIX_DIR" + mkdir -p "$TOXEXT_PREFIX_DIR" + + curl $CURL_OPTIONS https://github.com/toxext/toxext/archive/v$TOXEXT_VERSION.tar.gz -o $TOXEXT_FILENAME + check_sha256 "$TOXEXT_HASH" "$TOXEXT_FILENAME" + bsdtar --no-same-owner --no-same-permissions -xf "$TOXEXT_FILENAME" + rm "$TOXEXT_FILENAME" + cd toxext* + + mkdir -p build + cd build + + export PKG_CONFIG_PATH="$OPUS_PREFIX_DIR/lib/pkgconfig:$SODIUM_PREFIX_DIR/lib/pkgconfig:$VPX_PREFIX_DIR/lib/pkgconfig:$TOXCORE_PREFIX_DIR/lib/pkgconfig" + export PKG_CONFIG_LIBDIR="/usr/$ARCH-w64-mingw32" + + echo " + SET(CMAKE_SYSTEM_NAME Windows) + + SET(CMAKE_C_COMPILER $ARCH-w64-mingw32-gcc) + SET(CMAKE_CXX_COMPILER $ARCH-w64-mingw32-g++) + SET(CMAKE_RC_COMPILER $ARCH-w64-mingw32-windres) + + SET(CMAKE_FIND_ROOT_PATH /usr/$ARCH-w64-mingw32 $OPUS_PREFIX_DIR $SODIUM_PREFIX_DIR $VPX_PREFIX_DIR $TOXCORE_PREFIX_DIR) + " > toolchain.cmake + + cmake -DCMAKE_INSTALL_PREFIX=$TOXEXT_PREFIX_DIR \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ + .. + + make + make install + echo -n $TOXEXT_VERSION > $TOXEXT_PREFIX_DIR/done + + unset PKG_CONFIG_PATH + unset PKG_CONFIG_LIBDIR + + cd .. + + cd .. + rm -rf ./toxext* +else + echo "Using cached build of ToxExt `cat $TOXEXT_PREFIX_DIR/done`" +fi + +# tox_extension_messages +TOX_EXTENSION_MESSAGES_PREFIX_DIR="$DEP_DIR/tox_extension_messages" +TOX_EXTENSION_MESSAGES_VERSION=0.0.2 +TOX_EXTENSION_MESSAGES_HASH="95e8cdd1de6cc7ba561620716f340e9606a06b3c2ff9c9020af4784c22fd0d7f" +TOX_EXTENSION_MESSAGES_FILENAME="tox_extension_messages-$TOX_EXTENSION_MESSAGES_VERSION.tar.gz" +if [ ! -f "$TOX_EXTENSION_MESSAGES_PREFIX_DIR/done" ] +then + rm -rf "$TOX_EXTENSION_MESSAGES_PREFIX_DIR" + mkdir -p "$TOX_EXTENSION_MESSAGES_PREFIX_DIR" + + curl $CURL_OPTIONS https://github.com/toxext/tox_extension_messages/archive/v$TOX_EXTENSION_MESSAGES_VERSION.tar.gz -o $TOX_EXTENSION_MESSAGES_FILENAME + check_sha256 "$TOX_EXTENSION_MESSAGES_HASH" "$TOX_EXTENSION_MESSAGES_FILENAME" + bsdtar --no-same-owner --no-same-permissions -xf "$TOX_EXTENSION_MESSAGES_FILENAME" + rm "$TOX_EXTENSION_MESSAGES_FILENAME" + cd tox_extension_messages* + + mkdir -p build + cd build + + export PKG_CONFIG_PATH="$OPUS_PREFIX_DIR/lib/pkgconfig:$SODIUM_PREFIX_DIR/lib/pkgconfig:$VPX_PREFIX_DIR/lib/pkgconfig:$TOXCORE_PREFIX_DIR/lib/pkgconfig" + export PKG_CONFIG_LIBDIR="/usr/$ARCH-w64-mingw32" + + echo " + SET(CMAKE_SYSTEM_NAME Windows) + + SET(CMAKE_C_COMPILER $ARCH-w64-mingw32-gcc) + SET(CMAKE_CXX_COMPILER $ARCH-w64-mingw32-g++) + SET(CMAKE_RC_COMPILER $ARCH-w64-mingw32-windres) + + SET(CMAKE_FIND_ROOT_PATH /usr/$ARCH-w64-mingw32 $OPUS_PREFIX_DIR $SODIUM_PREFIX_DIR $VPX_PREFIX_DIR $TOXCORE_PREFIX_DIR $TOXEXT_PREFIX_DIR) + " > toolchain.cmake + + cmake -DCMAKE_INSTALL_PREFIX=$TOX_EXTENSION_MESSAGES_PREFIX_DIR \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ + .. + + make + make install + echo -n $TOX_EXTENSION_MESSAGES_VERSION > $TOX_EXTENSION_MESSAGES_PREFIX_DIR/done + + unset PKG_CONFIG_PATH + unset PKG_CONFIG_LIBDIR + + cd .. + + cd .. + rm -rf ./tox_extension_messages* +else + echo "Using cached build of tox_extension_messages `cat $TOX_EXTENSION_MESSAGES_PREFIX_DIR/done`" +fi set +u if [[ -n "$TRAVIS_CI_STAGE" ]] || [[ "$BUILD_TYPE" == "debug" ]] From a11a65af2a73be7920a6dc6dfa3ac28e52968cb6 Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Mon, 18 Nov 2019 20:05:10 -0800 Subject: [PATCH 5/6] feat(extensions): Update documentation --- INSTALL.md | 52 +++++++++++++++++++++++++++++++++---------- doc/user_manual_en.md | 8 +++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 16104b679..006467a4c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -34,18 +34,20 @@ ## Dependencies -| Name | Version | Modules | -|---------------|-------------|----------------------------------------------------------| -| [Qt] | >= 5.5.0 | concurrent, core, gui, network, opengl, svg, widget, xml | -| [GCC]/[MinGW] | >= 4.8 | C++11 enabled | -| [toxcore] | >= 0.2.10 | core, av | -| [FFmpeg] | >= 2.6.0 | avformat, avdevice, avcodec, avutil, swscale | -| [CMake] | >= 2.8.11 | | -| [OpenAL Soft] | >= 1.16.0 | | -| [qrencode] | >= 3.0.3 | | -| [sqlcipher] | >= 3.2.0 | | -| [pkg-config] | >= 0.28 | | -| [snorenotify] | >= 0.7.0 | optional dependency | +| Name | Version | Modules | +|--------------------------|-------------|----------------------------------------------------------| +| [Qt] | >= 5.5.0 | concurrent, core, gui, network, opengl, svg, widget, xml | +| [GCC]/[MinGW] | >= 4.8 | C++11 enabled | +| [toxcore] | >= 0.2.10 | core, av | +| [FFmpeg] | >= 2.6.0 | avformat, avdevice, avcodec, avutil, swscale | +| [CMake] | >= 2.8.11 | | +| [OpenAL Soft] | >= 1.16.0 | | +| [qrencode] | >= 3.0.3 | | +| [sqlcipher] | >= 3.2.0 | | +| [pkg-config] | >= 0.28 | | +| [snorenotify] | >= 0.7.0 | optional dependency | +| [toxext] | >= 0.0.1 | | +| [tox_extension_messages] | >= 0.0.1 | | ## Optional dependencies @@ -424,6 +426,30 @@ echo '/usr/local/lib/' | sudo tee -a /etc/ld.so.conf.d/locallib.conf sudo ldconfig ``` +### Compile extensions + +qTox uses the toxext library and some of the extensions that go with it. + +You will likely have to compile these yourself + +```bash +git clone https://github.com/toxext/toxext.git toxext +cd toxext +git checkout v0.0.2 +cmake . +make -j$(nproc) +sudo make install +``` + +```bash +git clone https://github.com/toxext/tox_extension_messages.git tox_extension_messages +cd tox_extension_messages +git checkout v0.0.2 +cmake . +make -j$(nproc) +sudo make install +``` + ### Compile qTox **Make sure that all the dependencies are installed.** If you experience @@ -756,3 +782,5 @@ Switches: [toxcore]: https://github.com/TokTok/c-toxcore/ [sonnet]: https://github.com/KDE/sonnet [snorenotify]: https://techbase.kde.org/Projects/Snorenotify +[toxext]: https://github.com/toxext/toxext +[tox_extension_messages]: https://github.com/toxext/tox_extension_messages diff --git a/doc/user_manual_en.md b/doc/user_manual_en.md index dc6c863c2..46a115dc3 100644 --- a/doc/user_manual_en.md +++ b/doc/user_manual_en.md @@ -489,3 +489,11 @@ public key, and which changes on every start of a client, so it's best to use a [ToxMe service]: #register-on-toxme [user profile]: #user-profile [profile corner]: #profile-corner + +# Extensions + +qTox supports extra features through the use of extensions to the tox protocol. Not all contacts are going to support these extensions. + +For most cases you won't have to do anything, but you may wonder why behavior of chats is different for some friends. There is a puzzle piece icon to the left of your contact's name in the top of a chat. If it's green that means that they support all the features qTox cares about. If it's yellow it means some of the features are supported. If it's red it means that they don't support any extensions. + +You can hover over the icon to see which extensions they support. qTox should dynamically enable/disable features based on the extension set of your friend. From 26701283cd96aa2b0094035f6b6090351ec297f4 Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Sun, 24 Jan 2021 14:49:24 -0800 Subject: [PATCH 6/6] feat(extensions): Split messages on extended messages v0.0.2 of toxext_extended_messages brought in a user configurable max message size. This changeset implements the minimum work required for qTox to work sanely under the new API. * Hardcode a max message size for all friends * If a friend negotiates a max message size below the hardcoded value pretend they do not have the extension * Move splitMessage out of Core to MessageProcessor * Updates to allow for extended messages to be split --- src/core/core.cpp | 37 ++-------------- src/core/core.h | 2 +- src/core/coreext.cpp | 38 +++++++++++++++-- src/core/coreext.h | 2 + src/model/chathistory.cpp | 2 +- src/model/friendmessagedispatcher.cpp | 24 ++++++----- src/model/friendmessagedispatcher.h | 2 +- src/model/groupmessagedispatcher.cpp | 5 ++- src/model/groupmessagedispatcher.h | 3 +- src/model/imessagedispatcher.h | 2 +- src/model/message.cpp | 47 ++++++++++++++++++--- src/model/message.h | 23 ++++++++-- src/widget/widget.cpp | 11 +++-- src/widget/widget.h | 2 +- test/model/friendmessagedispatcher_test.cpp | 7 ++- test/model/groupmessagedispatcher_test.cpp | 2 +- test/model/messageprocessor_test.cpp | 6 +-- 17 files changed, 141 insertions(+), 74 deletions(-) diff --git a/src/core/core.cpp b/src/core/core.cpp index 78f0a77ad..317103aa1 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -1094,7 +1094,7 @@ bool Core::sendMessageWithType(uint32_t friendId, const QString& message, Tox_Me ReceiptNum& receipt) { int size = message.toUtf8().size(); - auto maxSize = static_cast(tox_max_message_length()); + auto maxSize = static_cast(getMaxMessageSize()); if (size > maxSize) { assert(false); qCritical() << "Core::sendMessageWithType called with message of size:" << size @@ -1140,7 +1140,7 @@ void Core::sendGroupMessageWithType(int groupId, const QString& message, Tox_Mes QMutexLocker ml{&coreLoopLock}; int size = message.toUtf8().size(); - auto maxSize = static_cast(tox_max_message_length()); + auto maxSize = static_cast(getMaxMessageSize()); if (size > maxSize) { qCritical() << "Core::sendMessageWithType called with message of size:" << size << "when max is:" << maxSize << ". Ignoring."; @@ -1762,11 +1762,8 @@ QString Core::getFriendUsername(uint32_t friendnumber) const return ToxString(nameBuf.data(), nameSize).getQString(); } -QStringList Core::splitMessage(const QString& message) +uint64_t Core::getMaxMessageSize() const { - QStringList splittedMsgs; - QByteArray ba_message{message.toUtf8()}; - /* * TODO: Remove this hack; the reported max message length we receive from c-toxcore * as of 08-02-2019 is inaccurate, causing us to generate too large messages when splitting @@ -1777,33 +1774,7 @@ QStringList Core::splitMessage(const QString& message) * * (uint32_t tox_max_message_length(void); declared in tox.h, unable to see explicit definition) */ - const auto maxLen = static_cast(tox_max_message_length()) - 50; - - while (ba_message.size() > maxLen) { - int splitPos = ba_message.lastIndexOf('\n', maxLen - 1); - - if (splitPos <= 0) { - splitPos = ba_message.lastIndexOf(' ', maxLen - 1); - } - - if (splitPos <= 0) { - constexpr uint8_t firstOfMultiByteMask = 0xC0; - constexpr uint8_t multiByteMask = 0x80; - splitPos = maxLen; - // don't split a utf8 character - if ((ba_message[splitPos] & multiByteMask) == multiByteMask) { - while ((ba_message[splitPos] & firstOfMultiByteMask) != firstOfMultiByteMask) { - --splitPos; - } - } - --splitPos; - } - splittedMsgs.append(QString{ba_message.left(splitPos + 1)}); - ba_message = ba_message.mid(splitPos + 1); - } - - splittedMsgs.append(QString{ba_message}); - return splittedMsgs; + return tox_max_message_length() - 50; } QString Core::getPeerName(const ToxPk& id) const diff --git a/src/core/core.h b/src/core/core.h index fd622cb27..c13379419 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -85,7 +85,7 @@ public: ~Core(); static const QString TOX_EXT; - static QStringList splitMessage(const QString& message); + uint64_t getMaxMessageSize() const; QString getPeerName(const ToxPk& id) const; QVector getFriendList() const; GroupId getGroupPersistentId(uint32_t groupNumber) const override; diff --git a/src/core/coreext.cpp b/src/core/coreext.cpp index 0d9c7fe5d..375093002 100644 --- a/src/core/coreext.cpp +++ b/src/core/coreext.cpp @@ -98,16 +98,34 @@ uint64_t CoreExt::Packet::addExtendedMessage(QString message) return UINT64_MAX; } - ToxString toxString(message); - Tox_Extension_Messages_Error err; + int size = message.toUtf8().size(); + enum Tox_Extension_Messages_Error err; + auto maxSize = static_cast(tox_extension_messages_get_max_sending_size( + toxExtMessages, + friendId, + &err)); - return tox_extension_messages_append( + if (size > maxSize) { + assert(false); + qCritical() << "addExtendedMessage called with message of size:" << size + << "when max is:" << maxSize << ". Ignoring."; + return false; + } + + ToxString toxString(message); + const auto receipt = tox_extension_messages_append( toxExtMessages, packetList, toxString.data(), toxString.size(), friendId, &err); + + if (err != TOX_EXTENSION_MESSAGES_SUCCESS) { + qWarning() << "Error sending extension message"; + } + + return receipt; } bool CoreExt::Packet::send() @@ -122,6 +140,11 @@ bool CoreExt::Packet::send() return ret == TOXEXT_SUCCESS; } +uint64_t CoreExt::getMaxExtendedMessageSize() +{ + return TOX_EXTENSION_MESSAGES_DEFAULT_MAX_RECEIVING_MESSAGE_SIZE; +} + void CoreExt::onFriendStatusChanged(uint32_t friendId, Status::Status status) { const auto prevStatusIt = currentStatuses.find(friendId); @@ -154,6 +177,15 @@ void CoreExt::onExtendedMessageReceipt(uint32_t friendId, uint64_t receiptId, vo void CoreExt::onExtendedMessageNegotiation(uint32_t friendId, bool compatible, uint64_t maxMessageSize, void* userData) { auto coreExt = static_cast(userData); + + // HACK: handling configurable max message size per-friend is not trivial. + // For now the upper layers just assume that the max size for extended + // messages is the same for all friends. If a friend has a max message size + // lower than this value we just pretend they do not have the extension since + // we will not split correctly for this friend. + if (maxMessageSize < coreExt->getMaxExtendedMessageSize()) + compatible = false; + emit coreExt->extendedMessageSupport(friendId, compatible); } diff --git a/src/core/coreext.h b/src/core/coreext.h index 41a937cb8..f47553887 100644 --- a/src/core/coreext.h +++ b/src/core/coreext.h @@ -117,6 +117,8 @@ public: std::unique_ptr getPacket(uint32_t friendId) override; + uint64_t getMaxExtendedMessageSize(); + signals: void extendedMessageReceived(uint32_t friendId, const QString& message); void extendedReceiptReceived(uint32_t friendId, uint64_t receiptId); diff --git a/src/model/chathistory.cpp b/src/model/chathistory.cpp index 8e8bb6fa1..497ccda8f 100644 --- a/src/model/chathistory.cpp +++ b/src/model/chathistory.cpp @@ -441,7 +441,7 @@ void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher) // up having to split more than when we added the message to history we'll // just associate the last dispatched id with the history message ? messageDispatcher.sendMessage(isAction, messageContent).second - : messageDispatcher.sendExtendedMessage(messageContent, requiredExtensions); + : messageDispatcher.sendExtendedMessage(messageContent, requiredExtensions).second; handleDispatchedMessage(dispatchId, message.id); diff --git a/src/model/friendmessagedispatcher.cpp b/src/model/friendmessagedispatcher.cpp index 8a9f4eadc..75a0b0655 100644 --- a/src/model/friendmessagedispatcher.cpp +++ b/src/model/friendmessagedispatcher.cpp @@ -55,19 +55,22 @@ FriendMessageDispatcher::sendMessage(bool isAction, const QString& content) /** * @see IMessageDispatcher::sendExtendedMessage */ -DispatchedMessageId FriendMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions) +std::pair +FriendMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions) { - auto messageId = nextMessageId++; + const auto firstId = nextMessageId; + auto lastId = nextMessageId; - auto messages = processor.processOutgoingMessage(false, content, extensions); - assert(messages.size() == 1); + for (const auto& message : processor.processOutgoingMessage(false, content, extensions)) { + auto messageId = nextMessageId++; + lastId = messageId; - auto onOfflineMsgComplete = getCompletionFn(messageId); - sendProcessedMessage(messages[0], onOfflineMsgComplete); + auto onOfflineMsgComplete = getCompletionFn(messageId); + sendProcessedMessage(message, onOfflineMsgComplete); - emit this->messageSent(messageId, messages[0]); - - return messageId; + emit this->messageSent(messageId, message); + } + return std::make_pair(firstId, lastId); } /** @@ -150,7 +153,8 @@ void FriendMessageDispatcher::sendExtendedProcessedMessage(Message const& messag auto receipt = ExtendedReceiptNum(); - auto packet = coreExtPacketAllocator.getPacket(f.getId()); + const auto friendId = f.getId(); + auto packet = coreExtPacketAllocator.getPacket(friendId); if (message.extensionSet[ExtensionType::messages]) { receipt.get() = packet->addExtendedMessage(message.content); diff --git a/src/model/friendmessagedispatcher.h b/src/model/friendmessagedispatcher.h index e0688f112..37a61dfbe 100644 --- a/src/model/friendmessagedispatcher.h +++ b/src/model/friendmessagedispatcher.h @@ -41,7 +41,7 @@ public: std::pair sendMessage(bool isAction, const QString& content) override; - DispatchedMessageId sendExtendedMessage(const QString& content, ExtensionSet extensions) override; + std::pair sendExtendedMessage(const QString& content, ExtensionSet extensions) override; void onMessageReceived(bool isAction, const QString& content); void onReceiptReceived(ReceiptNum receipt); void onExtMessageReceived(const QString& message); diff --git a/src/model/groupmessagedispatcher.cpp b/src/model/groupmessagedispatcher.cpp index e4ca5b426..2b5786278 100644 --- a/src/model/groupmessagedispatcher.cpp +++ b/src/model/groupmessagedispatcher.cpp @@ -65,14 +65,15 @@ GroupMessageDispatcher::sendMessage(bool isAction, QString const& content) return std::make_pair(firstMessageId, lastMessageId); } -DispatchedMessageId GroupMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions) +std::pair +GroupMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions) { // Stub this api to immediately fail auto messageId = nextMessageId++; auto messages = processor.processOutgoingMessage(false, content, ExtensionSet()); emit this->messageSent(messageId, messages[0]); emit this->messageBroken(messageId, BrokenMessageReason::unsupportedExtensions); - return messageId; + return {messageId, messageId}; } /** diff --git a/src/model/groupmessagedispatcher.h b/src/model/groupmessagedispatcher.h index 807bd4f2a..4ed0d646b 100644 --- a/src/model/groupmessagedispatcher.h +++ b/src/model/groupmessagedispatcher.h @@ -43,7 +43,8 @@ public: std::pair sendMessage(bool isAction, QString const& content) override; - DispatchedMessageId sendExtendedMessage(const QString& content, ExtensionSet extensions) override; + std::pair sendExtendedMessage(const QString& content, + ExtensionSet extensions) override; void onMessageReceived(ToxPk const& sender, bool isAction, QString const& content); private: diff --git a/src/model/imessagedispatcher.h b/src/model/imessagedispatcher.h index abbdd7c1c..4c4007bf2 100644 --- a/src/model/imessagedispatcher.h +++ b/src/model/imessagedispatcher.h @@ -54,7 +54,7 @@ public: * @note If the provided extensions are not supported the message will be flagged * as broken */ - virtual DispatchedMessageId + virtual std::pair sendExtendedMessage(const QString& content, ExtensionSet extensions) = 0; signals: diff --git a/src/model/message.cpp b/src/model/message.cpp index e7e018f3a..7ec5fc93a 100644 --- a/src/model/message.cpp +++ b/src/model/message.cpp @@ -23,6 +23,38 @@ #include +namespace { + QStringList splitMessage(const QString& message, uint64_t maxLength) + { + QStringList splittedMsgs; + QByteArray ba_message{message.toUtf8()}; + while (static_cast(ba_message.size()) > maxLength) { + int splitPos = ba_message.lastIndexOf('\n', maxLength - 1); + + if (splitPos <= 0) { + splitPos = ba_message.lastIndexOf(' ', maxLength - 1); + } + + if (splitPos <= 0) { + constexpr uint8_t firstOfMultiByteMask = 0xC0; + constexpr uint8_t multiByteMask = 0x80; + splitPos = maxLength; + // don't split a utf8 character + if ((ba_message[splitPos] & multiByteMask) == multiByteMask) { + while ((ba_message[splitPos] & firstOfMultiByteMask) != firstOfMultiByteMask) { + --splitPos; + } + } + --splitPos; + } + splittedMsgs.append(QString{ba_message.left(splitPos + 1)}); + ba_message = ba_message.mid(splitPos + 1); + } + + splittedMsgs.append(QString{ba_message}); + return splittedMsgs; + } +} void MessageProcessor::SharedParams::onUserNameSet(const QString& username) { QString sanename = username; @@ -55,10 +87,11 @@ std::vector MessageProcessor::processOutgoingMessage(bool isAction, QSt { std::vector ret; - const auto needsSplit = !extensions[ExtensionType::messages] || isAction; - const auto splitMsgs = needsSplit - ? Core::splitMessage(content) - : QStringList({content}); + const auto maxSendingSize = extensions[ExtensionType::messages] + ? sharedParams.getMaxExtendedMessageSize() + : sharedParams.getMaxCoreMessageSize(); + + const auto splitMsgs = splitMessage(content, maxSendingSize); ret.reserve(splitMsgs.size()); @@ -91,9 +124,9 @@ Message MessageProcessor::processIncomingCoreMessage(bool isAction, QString cons ret.timestamp = timestamp; if (detectingMentions) { - auto nameMention = sharedParams.GetNameMention(); - auto sanitizedNameMention = sharedParams.GetSanitizedNameMention(); - auto pubKeyMention = sharedParams.GetPublicKeyMention(); + auto nameMention = sharedParams.getNameMention(); + auto sanitizedNameMention = sharedParams.getSanitizedNameMention(); + auto pubKeyMention = sharedParams.getPublicKeyMention(); for (auto const& mention : {nameMention, sanitizedNameMention, pubKeyMention}) { auto matchIt = mention.globalMatch(ret.content); diff --git a/src/model/message.h b/src/model/message.h index ac31183d3..11b3b0d66 100644 --- a/src/model/message.h +++ b/src/model/message.h @@ -71,22 +71,39 @@ public: { public: - QRegularExpression GetNameMention() const + SharedParams(uint64_t maxCoreMessageSize_, uint64_t maxExtendedMessageSize_) + : maxCoreMessageSize(maxCoreMessageSize_) + , maxExtendedMessageSize(maxExtendedMessageSize_) + {} + + QRegularExpression getNameMention() const { return nameMention; } - QRegularExpression GetSanitizedNameMention() const + QRegularExpression getSanitizedNameMention() const { return sanitizedNameMention; } - QRegularExpression GetPublicKeyMention() const + QRegularExpression getPublicKeyMention() const { return pubKeyMention; } void onUserNameSet(const QString& username); void setPublicKey(const QString& pk); + uint64_t getMaxCoreMessageSize() const + { + return maxCoreMessageSize; + } + + uint64_t getMaxExtendedMessageSize() const + { + return maxExtendedMessageSize; + } + private: + uint64_t maxCoreMessageSize; + uint64_t maxExtendedMessageSize; QRegularExpression nameMention; QRegularExpression sanitizedNameMention; QRegularExpression pubKeyMention; diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index a4ac71764..8baa35c77 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -248,6 +248,9 @@ void Widget::init() ui->searchContactFilterBox->setMenu(filterMenu); core = &profile.getCore(); + auto coreExt = core->getExt(); + + sharedMessageProcessorParams.reset(new MessageProcessor::SharedParams(core->getMaxMessageSize(), coreExt->getMaxExtendedMessageSize())); contactListWidget = new FriendListWidget(*core, this, settings.getGroupchatPosition()); connect(contactListWidget, &FriendListWidget::searchCircle, this, &Widget::searchCircle); @@ -707,7 +710,7 @@ void Widget::onCoreChanged(Core& core) connect(this, &Widget::friendRequestAccepted, &core, &Core::acceptFriendRequest); connect(this, &Widget::changeGroupTitle, &core, &Core::changeGroupTitle); - sharedMessageProcessorParams.setPublicKey(core.getSelfPublicKey().toString()); + sharedMessageProcessorParams->setPublicKey(core.getSelfPublicKey().toString()); } void Widget::onConnected() @@ -999,7 +1002,7 @@ void Widget::setUsername(const QString& username) Qt::convertFromPlainText(username, Qt::WhiteSpaceNormal)); // for overlength names } - sharedMessageProcessorParams.onUserNameSet(username); + sharedMessageProcessorParams->onUserNameSet(username); } void Widget::onStatusMessageChanged(const QString& newStatusMessage) @@ -1151,7 +1154,7 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk) connectFriendWidget(*widget); auto history = profile.getHistory(); - auto messageProcessor = MessageProcessor(sharedMessageProcessorParams); + auto messageProcessor = MessageProcessor(*sharedMessageProcessorParams); auto friendMessageDispatcher = std::make_shared(*newfriend, std::move(messageProcessor), *core, *core->getExt()); @@ -2127,7 +2130,7 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId) const auto compact = settings.getCompactLayout(); auto widget = new GroupWidget(chatroom, compact); - auto messageProcessor = MessageProcessor(sharedMessageProcessorParams); + auto messageProcessor = MessageProcessor(*sharedMessageProcessorParams); auto messageDispatcher = std::make_shared(*newgroup, std::move(messageProcessor), *core, *core, settings); diff --git a/src/widget/widget.h b/src/widget/widget.h index e9cfefbe3..2581c6fa0 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -364,7 +364,7 @@ private: Core* core = nullptr; - MessageProcessor::SharedParams sharedMessageProcessorParams; + std::unique_ptr sharedMessageProcessorParams; #if DESKTOP_NOTIFICATIONS std::unique_ptr notificationGenerator; DesktopNotify notifier; diff --git a/test/model/friendmessagedispatcher_test.cpp b/test/model/friendmessagedispatcher_test.cpp index a3956ee5b..0b2216f0f 100644 --- a/test/model/friendmessagedispatcher_test.cpp +++ b/test/model/friendmessagedispatcher_test.cpp @@ -28,6 +28,8 @@ #include #include +static constexpr uint64_t testMaxExtendedMessageSize = 10 * 1024 * 1024; + class MockCoreExtPacket : public ICoreExtPacket { @@ -165,7 +167,8 @@ void TestFriendMessageDispatcher::init() messageSender = std::unique_ptr(new MockFriendMessageSender()); coreExtPacketAllocator = std::unique_ptr(new MockCoreExtPacketAllocator()); sharedProcessorParams = - std::unique_ptr(new MessageProcessor::SharedParams()); + std::unique_ptr(new MessageProcessor::SharedParams(tox_max_message_length(), testMaxExtendedMessageSize)); + messageProcessor = std::unique_ptr(new MessageProcessor(*sharedProcessorParams)); friendMessageDispatcher = std::unique_ptr( new FriendMessageDispatcher(*f, *messageProcessor, *messageSender, *coreExtPacketAllocator)); @@ -377,7 +380,7 @@ void TestFriendMessageDispatcher::testActionMessagesSplitWithExtensions() auto reallyLongMessage = QString("a"); - for (int i = 0; i < 9999; ++i) { + for (uint64_t i = 0; i < testMaxExtendedMessageSize + 50; ++i) { reallyLongMessage += i; } diff --git a/test/model/groupmessagedispatcher_test.cpp b/test/model/groupmessagedispatcher_test.cpp index cdebc64b1..94c2bd756 100644 --- a/test/model/groupmessagedispatcher_test.cpp +++ b/test/model/groupmessagedispatcher_test.cpp @@ -131,7 +131,7 @@ void TestGroupMessageDispatcher::init() new Group(0, GroupId(), "TestGroup", false, "me", *groupQuery, *coreIdHandler)); messageSender = std::unique_ptr(new MockGroupMessageSender()); sharedProcessorParams = - std::unique_ptr(new MessageProcessor::SharedParams()); + std::unique_ptr(new MessageProcessor::SharedParams(tox_max_message_length(), 10 * 1024 * 1024)); messageProcessor = std::unique_ptr(new MessageProcessor(*sharedProcessorParams)); groupMessageDispatcher = std::unique_ptr( new GroupMessageDispatcher(*g, *messageProcessor, *coreIdHandler, *messageSender, diff --git a/test/model/messageprocessor_test.cpp b/test/model/messageprocessor_test.cpp index 62a9fd9d2..332b4557b 100644 --- a/test/model/messageprocessor_test.cpp +++ b/test/model/messageprocessor_test.cpp @@ -53,7 +53,7 @@ private slots: */ void TestMessageProcessor::testSelfMention() { - MessageProcessor::SharedParams sharedParams; + MessageProcessor::SharedParams sharedParams(tox_max_message_length(), 10 * 1024 * 1024);; const QLatin1String testUserName{"MyUserName"}; const QLatin1String testToxPk{ "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"}; @@ -124,7 +124,7 @@ void TestMessageProcessor::testSelfMention() */ void TestMessageProcessor::testOutgoingMessage() { - auto sharedParams = MessageProcessor::SharedParams(); + auto sharedParams = MessageProcessor::SharedParams(tox_max_message_length(), 10 * 1024 * 1024); auto messageProcessor = MessageProcessor(sharedParams); QString testStr; @@ -152,7 +152,7 @@ void TestMessageProcessor::testOutgoingMessage() void TestMessageProcessor::testIncomingMessage() { // Nothing too special happening on the incoming side if we aren't looking for self mentions - auto sharedParams = MessageProcessor::SharedParams(); + auto sharedParams = MessageProcessor::SharedParams(tox_max_message_length(), 10 * 1024 * 1024); auto messageProcessor = MessageProcessor(sharedParams); auto message = messageProcessor.processIncomingCoreMessage(false, "test");