Automatically load recent messages on reconnect (#3878)

* Add working reconnect recent messages

* Rename method to messagesUpdated

* Use audo declarations

* Add docs to new LimitedQueue methods

* Add more documentation, try atomic loading flag

* Update CHANGELOG.md

* Remove unused include

* Rename 'reconnected' signal to 'connected'

* Reserve before filtering on arbitrary update

* Extract recent messages fetching to own class

* Use std::atomic_flag instead of std::atomic_bool

* Add PostToThread include

* Add chatterino.recentmessages logging

* Remove unneeded parameters, lambda move capture

* Remove TwitchChannel::buildRecentMessages

* Add documentation, use more clear method name

* Reword changelog entry

I think it sounds better like this :)

* Rework how filling in missing messages is handled

This should hopefully prevent issues with filtered channels with old messages
that no longer exist in the underlying channel

* Check existing messages when looking for reply

* Clean up string distribution in file

* Try to improve documentation

* Use std::function for RecentMessagesApi

* Only trigger filledInMessages if we inserted

* Remove old unused lines

* Use make_shared<MessageLayout> instead of new MessageLayout

* Alphabetize QLogging categories

* Reorder CHANGELOG.md
This commit is contained in:
Daniel Sage
2022-08-06 12:18:34 -04:00
committed by GitHub
parent 2dd37ca210
commit 46f43f3ce8
15 changed files with 580 additions and 174 deletions
+78 -153
View File
@@ -7,6 +7,7 @@
#include "controllers/accounts/AccountController.hpp"
#include "controllers/notifications/NotificationController.hpp"
#include "messages/Message.hpp"
#include "providers/RecentMessagesApi.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/bttv/LoadBttvChannelEmote.hpp"
#include "providers/twitch/IrcMessageHandler.hpp"
@@ -19,7 +20,6 @@
#include "singletons/Settings.hpp"
#include "singletons/Toasts.hpp"
#include "singletons/WindowManager.hpp"
#include "util/FormatTime.hpp"
#include "util/PostToThread.hpp"
#include "util/QStringHash.hpp"
#include "widgets/Window.hpp"
@@ -48,79 +48,6 @@ namespace {
const QString LOGIN_PROMPT_TEXT("Click here to add your account again.");
const Link ACCOUNTS_LINK(Link::OpenAccountsPage, QString());
// convertClearchatToNotice takes a Communi::IrcMessage that is a CLEARCHAT command and converts it to a readable NOTICE message
// This has historically been done in the Recent Messages API, but this functionality is being moved to Chatterino instead
auto convertClearchatToNotice(Communi::IrcMessage *message)
{
auto channelName = message->parameter(0);
QString noticeMessage{};
if (message->tags().contains("target-user-id"))
{
auto target = message->parameter(1);
if (message->tags().contains("ban-duration"))
{
// User was timed out
noticeMessage =
QString("%1 has been timed out for %2.")
.arg(target)
.arg(formatTime(
message->tag("ban-duration").toString()));
}
else
{
// User was permanently banned
noticeMessage =
QString("%1 has been permanently banned.").arg(target);
}
}
else
{
// Chat was cleared
noticeMessage = "Chat has been cleared by a moderator.";
}
// rebuild the raw IRC message so we can convert it back to an ircmessage again!
// this could probably be done in a smarter way
auto s = QString(":tmi.twitch.tv NOTICE %1 :%2")
.arg(channelName)
.arg(noticeMessage);
auto newMessage = Communi::IrcMessage::fromData(s.toUtf8(), nullptr);
newMessage->setTags(message->tags());
return newMessage;
}
// parseRecentMessages takes a json object and returns a vector of
// Communi IrcMessages
auto parseRecentMessages(const QJsonObject &jsonRoot, ChannelPtr channel)
{
QJsonArray jsonMessages = jsonRoot.value("messages").toArray();
std::vector<Communi::IrcMessage *> messages;
if (jsonMessages.empty())
return messages;
for (const auto jsonMessage : jsonMessages)
{
auto content = jsonMessage.toString();
content.replace(COMBINED_FIXER, ZERO_WIDTH_JOINER);
auto message =
Communi::IrcMessage::fromData(content.toUtf8(), nullptr);
if (message->command() == "CLEARCHAT")
{
message = convertClearchatToNotice(message);
}
messages.emplace_back(std::move(message));
}
return messages;
}
std::pair<Outcome, std::unordered_set<QString>> parseChatters(
const QJsonObject &jsonRoot)
{
@@ -143,6 +70,7 @@ namespace {
return {Success, std::move(usernames)};
}
} // namespace
TwitchChannel::TwitchChannel(const QString &name)
@@ -181,6 +109,19 @@ TwitchChannel::TwitchChannel(const QString &name)
this->refreshBTTVChannelEmotes(false);
});
this->connected.connect([this]() {
if (this->roomId().isEmpty())
{
// If we get a reconnected event when the room id is not set, we
// just connected for the first time. After receiving the first
// message from a channel, setRoomId is called and further
// invocations of this event will load recent messages.
return;
}
this->loadRecentMessagesReconnect();
});
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
if (msg->replyThread)
{
@@ -819,93 +760,77 @@ void TwitchChannel::loadRecentMessages()
return;
}
QUrl url(Env::get().recentMessagesApiUrl.arg(this->getName()));
QUrlQuery urlQuery(url);
if (!urlQuery.hasQueryItem("limit"))
if (this->loadingRecentMessages_.test_and_set())
{
urlQuery.addQueryItem(
"limit", QString::number(getSettings()->twitchMessageHistoryLimit));
return; // already loading
}
url.setQuery(urlQuery);
auto weak = weakOf<Channel>(this);
NetworkRequest(url)
.onSuccess([this, weak](NetworkResult result) -> Outcome {
auto shared = weak.lock();
if (!shared)
return Failure;
auto root = result.parseJson();
auto messages = parseRecentMessages(root, shared);
auto &handler = IrcMessageHandler::instance();
std::vector<MessagePtr> allBuiltMessages;
for (auto message : messages)
{
if (message->tags().contains("rm-received-ts"))
{
QDate msgDate = QDateTime::fromMSecsSinceEpoch(
message->tags()
.value("rm-received-ts")
.toLongLong())
.date();
if (msgDate != shared.get()->lastDate_)
{
shared.get()->lastDate_ = msgDate;
auto msg = makeSystemMessage(
QLocale().toString(msgDate, QLocale::LongFormat),
QTime(0, 0));
msg->flags.set(MessageFlag::RecentMessage);
allBuiltMessages.emplace_back(msg);
}
}
auto builtMessages = handler.parseMessageWithReply(
shared.get(), message, allBuiltMessages);
for (auto builtMessage : builtMessages)
{
builtMessage->flags.set(MessageFlag::RecentMessage);
allBuiltMessages.emplace_back(builtMessage);
}
}
postToThread([this, shared, root,
messages = std::move(allBuiltMessages)]() mutable {
shared->addMessagesAtStart(messages);
// Notify user about a possible gap in logs if it returned some messages
// but isn't currently joined to a channel
if (QString errorCode = root.value("error_code").toString();
!errorCode.isEmpty())
{
qCDebug(chatterinoTwitch)
<< QString("rm error_code=%1, channel=%2")
.arg(errorCode, this->getName());
if (errorCode == "channel_not_joined" && !messages.empty())
{
shared->addMessage(makeSystemMessage(
"Message history service recovering, there may be "
"gaps in the message history."));
}
}
});
return Success;
})
.onError([weak](NetworkResult result) {
RecentMessagesApi::loadRecentMessages(
this->getName(), weak,
[weak](const auto &messages) {
auto shared = weak.lock();
if (!shared)
return;
shared->addMessage(makeSystemMessage(
QString("Message history service unavailable (Error %1)")
.arg(result.status())));
})
.execute();
auto tc = dynamic_cast<TwitchChannel *>(shared.get());
if (!tc)
return;
tc->addMessagesAtStart(messages);
tc->loadingRecentMessages_.clear();
},
[weak]() {
auto shared = weak.lock();
if (!shared)
return;
auto tc = dynamic_cast<TwitchChannel *>(shared.get());
if (!tc)
return;
tc->loadingRecentMessages_.clear();
});
}
void TwitchChannel::loadRecentMessagesReconnect()
{
if (!getSettings()->loadTwitchMessageHistoryOnConnect)
{
return;
}
if (this->loadingRecentMessages_.test_and_set())
{
return; // already loading
}
auto weak = weakOf<Channel>(this);
RecentMessagesApi::loadRecentMessages(
this->getName(), weak,
[weak](const auto &messages) {
auto shared = weak.lock();
if (!shared)
return;
auto tc = dynamic_cast<TwitchChannel *>(shared.get());
if (!tc)
return;
tc->fillInMissingMessages(messages);
tc->loadingRecentMessages_.clear();
},
[weak]() {
auto shared = weak.lock();
if (!shared)
return;
auto tc = dynamic_cast<TwitchChannel *>(shared.get());
if (!tc)
return;
tc->loadingRecentMessages_.clear();
});
}
void TwitchChannel::refreshPubSub()