mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
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
This commit is contained in:
parent
7474c6d8ac
commit
5f5f612841
10
img/status/negotiating.svg
Normal file
10
img/status/negotiating.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 10 10" enable-background="new 0 0 10 10" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#C94F50" d="M5,1.5c1.9,0,3.5,1.6,3.5,3.5S6.9,8.5,5,8.5C3.1,8.5,1.5,6.9,1.5,5S3.1,1.5,5,1.5 M5,0C2.2,0,0,2.2,0,5
|
||||
s2.2,5,5,5c2.8,0,5-2.2,5-5S7.8,0,5,0z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 621 B |
32
img/status/negotiating_notification.svg
Normal file
32
img/status/negotiating_notification.svg
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="14"
|
||||
width="14"
|
||||
xml:space="preserve"
|
||||
enable-background="new 0 0 10 10"
|
||||
viewBox="0 0 14 14"
|
||||
y="0px"
|
||||
x="0px"
|
||||
id="Layer_1"
|
||||
version="1.1"><metadata
|
||||
id="metadata9"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs7" /><g
|
||||
style="stroke-width:0.36135802;stroke-miterlimit:4;stroke-dasharray:none;stroke:#c94f50"
|
||||
id="layer1"
|
||||
transform="matrix(3.0289233,0,0,3.0289233,0.18380903,-887.48895)"><path
|
||||
style="fill:#000000;stroke-width:0.36135802;stroke-miterlimit:4;stroke-dasharray:none;stroke:#c94f50"
|
||||
d=""
|
||||
id="path4500" /><path
|
||||
style="fill:#000000;stroke-width:0.36135802;stroke-miterlimit:4;stroke-dasharray:none;stroke:#c94f50"
|
||||
d=""
|
||||
id="path4498" /><path
|
||||
id="path4528"
|
||||
style="fill:none;stroke:#c94f50;stroke-width:0.36135802;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 0.10133195,293.90792 1.88406635,1.94794 H 2.5570778 L 4.3764717,293.9374 M 0.13990242,293.77165 H 4.375461 v 2.99779 H 0.13310726 l 9.0535e-4,-3.17789" /></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
2
res.qrc
2
res.qrc
|
@ -22,6 +22,8 @@
|
|||
<file>img/status/away_notification.svg</file>
|
||||
<file>img/status/busy.svg</file>
|
||||
<file>img/status/busy_notification.svg</file>
|
||||
<file>img/status/negotiating.svg</file>
|
||||
<file>img/status/negotiating_notification.svg</file>
|
||||
<file>img/status/offline.svg</file>
|
||||
<file>img/status/offline_notification.svg</file>
|
||||
<file>img/status/online.svg</file>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -60,6 +60,7 @@ public:
|
|||
static ChatMessage::Ptr createBusyNotification();
|
||||
|
||||
void markAsDelivered(const QDateTime& time);
|
||||
void markAsBroken();
|
||||
QString toString() const;
|
||||
bool isAction() const;
|
||||
void setAsAction();
|
||||
|
|
|
@ -1096,6 +1096,7 @@ bool Core::sendMessageWithType(uint32_t friendId, const QString& message, Tox_Me
|
|||
int size = message.toUtf8().size();
|
||||
auto maxSize = static_cast<int>(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;
|
||||
|
|
|
@ -26,3 +26,6 @@
|
|||
|
||||
using ReceiptNum = NamedType<uint32_t, struct ReceiptNumTag, Orderable>;
|
||||
Q_DECLARE_METATYPE(ReceiptNum)
|
||||
|
||||
using ExtendedReceiptNum = NamedType<uint32_t, struct ExtendedReceiptNumTag, Orderable>;
|
||||
Q_DECLARE_METATYPE(ExtendedReceiptNum);
|
||||
|
|
27
src/model/brokenmessagereason.h
Normal file
27
src/model/brokenmessagereason.h
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
#include "ichatlog.h"
|
||||
#include "sessionchatlog.h"
|
||||
#include "src/model/brokenmessagereason.h"
|
||||
#include "src/persistence/history.h"
|
||||
|
||||
#include <QSet>
|
||||
|
@ -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<DispatchedMessageId> completedMessages;
|
||||
// If a message breaks before it's inserted into history it will end up
|
||||
// in this set
|
||||
QMap<DispatchedMessageId, BrokenMessageReason> brokenMessages;
|
||||
|
||||
// If a message is inserted into history before it gets a completion
|
||||
// callback it will end up in this map
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
#include "src/persistence/profile.h"
|
||||
#include "src/widget/form/chatform.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <memory>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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<DispatchedMessageId, DispatchedMessageId>
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ public:
|
|||
|
||||
std::pair<DispatchedMessageId, DispatchedMessageId> 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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -42,6 +42,8 @@ public:
|
|||
|
||||
std::pair<DispatchedMessageId, DispatchedMessageId> 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:
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
#include "src/model/friend.h"
|
||||
#include "src/model/message.h"
|
||||
#include "src/model/brokenmessagereason.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
@ -44,6 +45,18 @@ public:
|
|||
*/
|
||||
virtual std::pair<DispatchedMessageId, DispatchedMessageId>
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -51,10 +51,11 @@ MessageProcessor::MessageProcessor(const MessageProcessor::SharedParams& sharedP
|
|||
/**
|
||||
* @brief Converts an outgoing message into one (or many) sanitized Message(s)
|
||||
*/
|
||||
std::vector<Message> MessageProcessor::processOutgoingMessage(bool isAction, QString const& content, bool needsSplit)
|
||||
std::vector<Message> MessageProcessor::processOutgoingMessage(bool isAction, QString const& content, ExtensionSet extensions)
|
||||
{
|
||||
std::vector<Message> ret;
|
||||
|
||||
const auto needsSplit = !extensions[ExtensionType::messages] || isAction;
|
||||
const auto splitMsgs = needsSplit
|
||||
? Core::splitMessage(content)
|
||||
: QStringList({content});
|
||||
|
@ -68,6 +69,10 @@ std::vector<Message> 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;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "src/core/coreext.h"
|
||||
#include "src/core/extension.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QRegularExpression>
|
||||
|
@ -53,6 +54,7 @@ struct Message
|
|||
bool isAction;
|
||||
QString content;
|
||||
QDateTime timestamp;
|
||||
ExtensionSet extensionSet;
|
||||
std::vector<MessageMetadata> metadata;
|
||||
};
|
||||
|
||||
|
@ -92,7 +94,7 @@ public:
|
|||
|
||||
MessageProcessor(const SharedParams& sharedParams);
|
||||
|
||||
std::vector<Message> processOutgoingMessage(bool isAction, const QString& content, bool needsSplit);
|
||||
std::vector<Message> processOutgoingMessage(bool isAction, const QString& content, ExtensionSet extensions);
|
||||
Message processIncomingCoreMessage(bool isAction, const QString& content);
|
||||
Message processIncomingExtMessage(const QString& content);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,7 +31,8 @@ namespace Status
|
|||
Away,
|
||||
Busy,
|
||||
Offline,
|
||||
Blocked
|
||||
Blocked,
|
||||
Negotiating,
|
||||
};
|
||||
|
||||
QString getIconPath(Status status, bool event = false);
|
||||
|
|
|
@ -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<RawDatabase>& db, bool& success)
|
|||
bool dbSchema0to1(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> 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<RawDatabase::Query> 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<QVariant>& 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<RawDatabase::Query> 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<RawDatabase::Query> 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<RawDatabase>& 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<QVariant>& row) {
|
||||
|
@ -295,8 +326,9 @@ bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& 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<RawDatabase>& 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<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
|
||||
dbSchema3to4, dbSchema4to5, dbSchema5to6};
|
||||
|
||||
assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
|
||||
assert(upgradeFns.size() == SCHEMA_VERSION);
|
||||
|
||||
for (int64_t i = databaseSchemaVersion; i < static_cast<int>(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<RawDatabase::Query>
|
||||
History::generateNewMessageQueries(const ToxPk& friendPk, const QString& message,
|
||||
const ToxPk& sender, const QDateTime& time, bool isDelivered,
|
||||
QString dispName, std::function<void(RowId)> insertIdCallback)
|
||||
ExtensionSet extensionSet, QString dispName,
|
||||
std::function<void(RowId)> insertIdCallback)
|
||||
{
|
||||
QVector<RawDatabase::Query> 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<int>(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<void(RowId)>& insertIdCallback)
|
||||
const QDateTime& time, bool isDelivered, ExtensionSet extensionSet,
|
||||
QString dispName, const std::function<void(RowId)>& 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::HistMessage> 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::HistMessage> 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::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
|
|||
file.filesize = row[10].toLongLong();
|
||||
file.direction = static_cast<ToxFile::FileDirection>(row[11].toLongLong());
|
||||
file.status = static_cast<ToxFile::FileStatus>(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::HistMessage> 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::HistMessage> 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<RawDatabase::Query> 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<int64_t>(reason)));
|
||||
|
||||
db->execLater(queries);
|
||||
}
|
||||
|
|
|
@ -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<History>
|
|||
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<void(RowId)>& insertIdCallback = {});
|
||||
const QDateTime& time, bool isDelivered, ExtensionSet extensions,
|
||||
QString dispName, const std::function<void(RowId)>& 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<RawDatabase::Query>
|
||||
generateNewMessageQueries(const ToxPk& friendPk, const QString& message,
|
||||
const ToxPk& sender, const QDateTime& time, bool isDelivered,
|
||||
QString dispName, std::function<void(RowId)> insertIdCallback = {});
|
||||
ExtensionSet extensionSet, QString dispName, std::function<void(RowId)> insertIdCallback = {});
|
||||
|
||||
signals:
|
||||
void fileInsertionReady(FileDbInsertionData data);
|
||||
|
|
|
@ -29,10 +29,8 @@
|
|||
#include <QCoreApplication>
|
||||
#include <chrono>
|
||||
|
||||
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<OfflineMessage> 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::RemovedMessage> 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<ReceiptNum, OfflineMessage>::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<RemovedMessage>();
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -30,23 +30,27 @@
|
|||
#include <QSet>
|
||||
#include <chrono>
|
||||
|
||||
class Friend;
|
||||
class ICoreFriendMessageSender;
|
||||
|
||||
class OfflineMsgEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OfflineMsgEngine(Friend* f, ICoreFriendMessageSender* messageSender);
|
||||
|
||||
using CompletionFn = std::function<void()>;
|
||||
using CompletionFn = std::function<void(bool)>;
|
||||
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<RemovedMessage> 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<ReceiptNum, OfflineMessage>::iterator msgIt);
|
||||
|
||||
private:
|
||||
void checkForCompleteMessages(ReceiptNum receipt);
|
||||
|
||||
QMutex mutex;
|
||||
const Friend* f;
|
||||
ICoreFriendMessageSender* messageSender;
|
||||
QVector<ReceiptNum> receivedReceipts;
|
||||
QMap<ReceiptNum, OfflineMessage> sentMessages;
|
||||
QVector<OfflineMessage> unsentMessages;
|
||||
|
||||
template <class ReceiptT>
|
||||
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<OfflineMessage> clear()
|
||||
{
|
||||
auto ret = std::vector<OfflineMessage>();
|
||||
std::transform(
|
||||
std::make_move_iterator(unAckedMessages.begin()), std::make_move_iterator(unAckedMessages.end()),
|
||||
std::back_inserter(ret),
|
||||
[] (const std::pair<ReceiptT, OfflineMessage>& pair) {
|
||||
return std::move(pair.second);
|
||||
});
|
||||
|
||||
receivedReceipts.clear();
|
||||
unAckedMessages.clear();
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<ReceiptT> receivedReceipts;
|
||||
std::map<ReceiptT, OfflineMessage> unAckedMessages;
|
||||
};
|
||||
|
||||
ReceiptResolver<ReceiptNum> receiptResolver;
|
||||
ReceiptResolver<ExtendedReceiptNum> extendedReceiptResolver;
|
||||
std::vector<OfflineMessage> unsentMessages;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<QTimer>(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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include <QObject>
|
||||
#include <QtTest/QtTest>
|
||||
|
||||
#include <set>
|
||||
#include <deque>
|
||||
|
||||
|
||||
|
@ -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<Friend> f;
|
||||
|
@ -139,6 +148,7 @@ private:
|
|||
std::unique_ptr<MessageProcessor> messageProcessor;
|
||||
std::unique_ptr<FriendMessageDispatcher> friendMessageDispatcher;
|
||||
std::map<DispatchedMessageId, Message> outgoingMessages;
|
||||
std::set<DispatchedMessageId> brokenMessages;
|
||||
std::deque<Message> receivedMessages;
|
||||
};
|
||||
|
||||
|
@ -151,6 +161,7 @@ void TestFriendMessageDispatcher::init()
|
|||
{
|
||||
f = std::unique_ptr<Friend>(new Friend(0, ToxPk()));
|
||||
f->setStatus(Status::Status::Online);
|
||||
f->onNegotiationComplete();
|
||||
messageSender = std::unique_ptr<MockFriendMessageSender>(new MockFriendMessageSender());
|
||||
coreExtPacketAllocator = std::unique_ptr<MockCoreExtPacketAllocator>(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<DispatchedMessageId, Message>();
|
||||
receivedMessages = std::deque<Message>();
|
||||
brokenMessages = std::set<DispatchedMessageId>();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<SqliteMasterEntry> schema5 {
|
|||
{"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"}
|
||||
};
|
||||
|
||||
// added toxext extensions
|
||||
const std::vector<SqliteMasterEntry> 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<RawDatabase::Query> queries;
|
||||
auto db = std::shared_ptr<RawDatabase>{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<RawDatabase>{new RawDatabase{"test5to6.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema5);
|
||||
QVERIFY(dbSchema5to6(*db));
|
||||
verifyDb(db, schema6);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestDbSchema)
|
||||
#include "dbschema_test.moc"
|
||||
|
|
|
@ -24,171 +24,164 @@
|
|||
|
||||
#include <QtTest/QtTest>
|
||||
|
||||
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<QString> 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<QString>{"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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user