Ensure live status requests are always batched (#4713)

This commit is contained in:
pajlada
2023-07-02 15:52:15 +02:00
committed by GitHub
parent f915eab1a2
commit 76527073cf
20 changed files with 582 additions and 282 deletions
+171 -203
View File
@@ -8,6 +8,7 @@
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/twitch/LiveController.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "messages/Link.hpp"
@@ -98,14 +99,14 @@ TwitchChannel::TwitchChannel(const QString &name)
// room id loaded -> refresh live status
this->roomIdChanged.connect([this]() {
this->refreshPubSub();
this->refreshTitle();
this->refreshLiveStatus();
this->refreshBadges();
this->refreshCheerEmotes();
this->refreshFFZChannelEmotes(false);
this->refreshBTTVChannelEmotes(false);
this->refreshSevenTVChannelEmotes(false);
this->joinBttvChannel();
getIApp()->getTwitchLiveController()->add(
std::dynamic_pointer_cast<TwitchChannel>(shared_from_this()));
});
this->connected.connect([this]() {
@@ -151,6 +152,85 @@ TwitchChannel::TwitchChannel(const QString &name)
});
this->threadClearTimer_.start(5 * 60 * 1000);
auto onLiveStatusChanged = [this](auto isLive) {
if (isLive)
{
qCDebug(chatterinoTwitch)
<< "[TwitchChannel" << this->getName() << "] Online";
if (getApp()->notifications->isChannelNotified(this->getName(),
Platform::Twitch))
{
if (Toasts::isEnabled())
{
getApp()->toasts->sendChannelNotification(
this->getName(), this->accessStreamStatus()->title,
Platform::Twitch);
}
if (getSettings()->notificationPlaySound)
{
getApp()->notifications->playSound();
}
if (getSettings()->notificationFlashTaskbar)
{
getApp()->windows->sendAlert();
}
}
// Channel live message
MessageBuilder builder;
TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// Message in /live channel
MessageBuilder builder2;
TwitchMessageBuilder::liveMessage(this->getDisplayName(),
&builder2);
getApp()->twitch->liveChannel->addMessage(builder2.release());
// Notify on all channels with a ping sound
if (getSettings()->notificationOnAnyChannel &&
!(isInStreamerMode() &&
getSettings()->streamerModeSuppressLiveNotifications))
{
getApp()->notifications->playSound();
}
}
else
{
qCDebug(chatterinoTwitch)
<< "[TwitchChannel" << this->getName() << "] Offline";
// Channel offline message
MessageBuilder builder;
TwitchMessageBuilder::offlineSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// "delete" old 'CHANNEL is live' message
LimitedQueueSnapshot<MessagePtr> snapshot =
getApp()->twitch->liveChannel->getMessageSnapshot();
int snapshotLength = snapshot.size();
// MSVC hates this code if the parens are not there
int end = (std::max)(0, snapshotLength - 200);
auto liveMessageSearchText =
QString("%1 is live!").arg(this->getDisplayName());
for (int i = snapshotLength - 1; i >= end; --i)
{
const auto &s = snapshot[i];
if (s->messageText == liveMessageSearchText)
{
s->flags.set(MessageFlag::Disabled);
break;
}
}
}
};
this->signalHolder_.managedConnect(this->liveStatusChanged,
onLiveStatusChanged);
// debugging
#if 0
for (int i = 0; i < 1000; i++) {
@@ -172,7 +252,6 @@ TwitchChannel::~TwitchChannel()
void TwitchChannel::initialize()
{
this->fetchDisplayName();
this->refreshChatters();
this->refreshBadges();
}
@@ -340,6 +419,89 @@ boost::optional<ChannelPointReward> TwitchChannel::channelPointReward(
return it->second;
}
void TwitchChannel::updateStreamStatus(
const std::optional<HelixStream> &helixStream)
{
if (helixStream)
{
auto stream = *helixStream;
{
auto status = this->streamStatus_.access();
status->viewerCount = stream.viewerCount;
status->gameId = stream.gameId;
status->game = stream.gameName;
status->title = stream.title;
QDateTime since =
QDateTime::fromString(stream.startedAt, Qt::ISODate);
auto diff = since.secsTo(QDateTime::currentDateTime());
status->uptime = QString::number(diff / 3600) + "h " +
QString::number(diff % 3600 / 60) + "m";
status->rerun = false;
status->streamType = stream.type;
}
if (this->setLive(true))
{
this->liveStatusChanged.invoke(true);
}
this->streamStatusChanged.invoke();
}
else
{
if (this->setLive(false))
{
this->liveStatusChanged.invoke(false);
this->streamStatusChanged.invoke();
}
}
}
void TwitchChannel::updateStreamTitle(const QString &title)
{
{
auto status = this->streamStatus_.access();
if (status->title == title)
{
// Title has not changed
return;
}
status->title = title;
}
this->streamStatusChanged.invoke();
}
void TwitchChannel::updateDisplayName(const QString &displayName)
{
if (displayName == this->getDisplayName())
{
// Display name has not changed
return;
}
// Display name has changed
if (QString::compare(displayName, this->getName(), Qt::CaseInsensitive) ==
0)
{
// Display name is only a case variation of the login name
this->setDisplayName(displayName);
this->setLocalizedName(displayName);
}
else
{
// Display name contains Chinese, Japanese, or Korean characters
this->setDisplayName(this->getName());
this->setLocalizedName(
QString("%1(%2)").arg(this->getName()).arg(displayName));
}
this->addRecentChatter(this->getDisplayName());
this->displayNameChanged.invoke();
}
void TwitchChannel::showLoginMessage()
{
const auto linkColor = MessageColor(MessageColor::Link);
@@ -909,183 +1071,16 @@ int TwitchChannel::chatterCount()
return this->chatterCount_;
}
void TwitchChannel::setLive(bool newLiveStatus)
bool TwitchChannel::setLive(bool newLiveStatus)
{
auto guard = this->streamStatus_.access();
if (guard->live == newLiveStatus)
{
auto guard = this->streamStatus_.access();
if (guard->live == newLiveStatus)
{
return;
}
guard->live = newLiveStatus;
return false;
}
guard->live = newLiveStatus;
if (newLiveStatus)
{
if (getApp()->notifications->isChannelNotified(this->getName(),
Platform::Twitch))
{
if (Toasts::isEnabled())
{
getApp()->toasts->sendChannelNotification(
this->getName(), this->accessStreamStatus()->title,
Platform::Twitch);
}
if (getSettings()->notificationPlaySound)
{
getApp()->notifications->playSound();
}
if (getSettings()->notificationFlashTaskbar)
{
getApp()->windows->sendAlert();
}
}
// Channel live message
MessageBuilder builder;
TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// Message in /live channel
MessageBuilder builder2;
TwitchMessageBuilder::liveMessage(this->getDisplayName(), &builder2);
getApp()->twitch->liveChannel->addMessage(builder2.release());
// Notify on all channels with a ping sound
if (getSettings()->notificationOnAnyChannel &&
!(isInStreamerMode() &&
getSettings()->streamerModeSuppressLiveNotifications))
{
getApp()->notifications->playSound();
}
}
else
{
// Channel offline message
MessageBuilder builder;
TwitchMessageBuilder::offlineSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// "delete" old 'CHANNEL is live' message
LimitedQueueSnapshot<MessagePtr> snapshot =
getApp()->twitch->liveChannel->getMessageSnapshot();
int snapshotLength = snapshot.size();
// MSVC hates this code if the parens are not there
int end = (std::max)(0, snapshotLength - 200);
auto liveMessageSearchText =
QString("%1 is live!").arg(this->getDisplayName());
for (int i = snapshotLength - 1; i >= end; --i)
{
auto &s = snapshot[i];
if (s->messageText == liveMessageSearchText)
{
s->flags.set(MessageFlag::Disabled);
break;
}
}
}
this->liveStatusChanged.invoke();
}
void TwitchChannel::refreshTitle()
{
// timer has never started, proceed and start it
if (!this->titleRefreshedTimer_.isValid())
{
this->titleRefreshedTimer_.start();
}
else if (this->roomId().isEmpty() ||
this->titleRefreshedTimer_.elapsed() < TITLE_REFRESH_PERIOD)
{
return;
}
this->titleRefreshedTimer_.restart();
getHelix()->getChannel(
this->roomId(),
[this, weak = weakOf<Channel>(this)](HelixChannel channel) {
ChannelPtr shared = weak.lock();
if (!shared)
{
return;
}
{
auto status = this->streamStatus_.access();
status->title = channel.title;
}
this->liveStatusChanged.invoke();
},
[] {
// failure
});
}
void TwitchChannel::refreshLiveStatus()
{
auto roomID = this->roomId();
if (roomID.isEmpty())
{
qCDebug(chatterinoTwitch) << "[TwitchChannel" << this->getName()
<< "] Refreshing live status (Missing ID)";
this->setLive(false);
return;
}
getHelix()->getStreamById(
roomID,
[this, weak = weakOf<Channel>(this)](bool live, const auto &stream) {
ChannelPtr shared = weak.lock();
if (!shared)
{
return;
}
this->parseLiveStatus(live, stream);
},
[] {
// failure
},
[] {
// finally
});
}
void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream)
{
if (!live)
{
this->setLive(false);
return;
}
{
auto status = this->streamStatus_.access();
status->viewerCount = stream.viewerCount;
status->gameId = stream.gameId;
status->game = stream.gameName;
status->title = stream.title;
QDateTime since = QDateTime::fromString(stream.startedAt, Qt::ISODate);
auto diff = since.secsTo(QDateTime::currentDateTime());
status->uptime = QString::number(diff / 3600) + "h " +
QString::number(diff % 3600 / 60) + "m";
status->rerun = false;
status->streamType = stream.type;
}
this->setLive(true);
// Signal all listeners that the stream status has been updated
this->liveStatusChanged.invoke();
return true;
}
void TwitchChannel::loadRecentMessages()
@@ -1220,33 +1215,6 @@ void TwitchChannel::refreshChatters()
[](auto error, auto message) {});
}
void TwitchChannel::fetchDisplayName()
{
getHelix()->getUserByName(
this->getName(),
[weak = weakOf<Channel>(this)](const auto &user) {
auto shared = weak.lock();
if (!shared)
return;
auto channel = static_cast<TwitchChannel *>(shared.get());
if (QString::compare(user.displayName, channel->getName(),
Qt::CaseInsensitive) == 0)
{
channel->setDisplayName(user.displayName);
channel->setLocalizedName(user.displayName);
}
else
{
channel->setLocalizedName(QString("%1(%2)")
.arg(channel->getName())
.arg(user.displayName));
}
channel->addRecentChatter(channel->getDisplayName());
channel->displayNameChanged.invoke();
},
[] {});
}
void TwitchChannel::addReplyThread(const std::shared_ptr<MessageThread> &thread)
{
this->threads_[thread->rootId()] = thread;
+39 -6
View File
@@ -18,6 +18,7 @@
#include <atomic>
#include <mutex>
#include <optional>
#include <unordered_map>
namespace chatterino {
@@ -120,7 +121,6 @@ public:
virtual bool hasHighRateLimit() const override;
virtual bool canReconnect() const override;
virtual void reconnect() override;
void refreshTitle();
void createClip();
// Data
@@ -194,7 +194,23 @@ public:
// Signals
pajlada::Signals::NoArgSignal roomIdChanged;
pajlada::Signals::NoArgSignal userStateChanged;
pajlada::Signals::NoArgSignal liveStatusChanged;
/**
* This signals fires whenever the live status is changed
*
* Streams are counted as offline by default, so if a stream does not go online
* this signal will never fire
**/
pajlada::Signals::Signal<bool> liveStatusChanged;
/**
* This signal fires whenever the stream status is changed
*
* This includes when the stream goes from offline to online,
* or the viewer count changes, or the title has been updated
**/
pajlada::Signals::NoArgSignal streamStatusChanged;
pajlada::Signals::NoArgSignal roomModesChanged;
// Channel point rewards
@@ -205,6 +221,12 @@ public:
boost::optional<ChannelPointReward> channelPointReward(
const QString &rewardId) const;
// Live status
void updateStreamStatus(const std::optional<HelixStream> &helixStream);
void updateStreamTitle(const QString &title);
void updateDisplayName(const QString &displayName);
private:
struct NameOptions {
QString displayName;
@@ -213,21 +235,23 @@ private:
private:
// Methods
void refreshLiveStatus();
void parseLiveStatus(bool live, const HelixStream &stream);
void refreshPubSub();
void refreshChatters();
void refreshBadges();
void refreshCheerEmotes();
void loadRecentMessages();
void loadRecentMessagesReconnect();
void fetchDisplayName();
void cleanUpReplyThreads();
void showLoginMessage();
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
void joinBttvChannel() const;
void setLive(bool newLiveStatus);
/**
* @brief Sets the live status of this Twitch channel
*
* Returns true if the live status changed with this call
**/
bool setLive(bool newLiveStatus);
void setMod(bool value);
void setVIP(bool value);
void setStaff(bool value);
@@ -236,7 +260,16 @@ private:
void setDisplayName(const QString &name);
void setLocalizedName(const QString &name);
/**
* Returns the display name of the user
*
* If the display name contained chinese, japenese, or korean characters, the user's login name is returned instead
**/
const QString &getDisplayName() const override;
/**
* Returns the localized name of the user
**/
const QString &getLocalizedName() const override;
QString prepareMessage(const QString &message) const;
-59
View File
@@ -80,12 +80,6 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
this->reloadBTTVGlobalEmotes();
this->reloadFFZGlobalEmotes();
this->reloadSevenTVGlobalEmotes();
/* Refresh all twitch channel's live status in bulk every 30 seconds after starting chatterino */
QObject::connect(&this->bulkLiveStatusTimer_, &QTimer::timeout, [this] {
this->bulkRefreshLiveStatus();
});
this->bulkLiveStatusTimer_.start(30 * 1000);
}
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
@@ -412,59 +406,6 @@ std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
return Channel::getEmpty();
}
void TwitchIrcServer::bulkRefreshLiveStatus()
{
auto twitchChans = std::make_shared<QHash<QString, TwitchChannel *>>();
this->forEachChannel([twitchChans](ChannelPtr chan) {
auto tc = dynamic_cast<TwitchChannel *>(chan.get());
if (tc && !tc->roomId().isEmpty())
{
twitchChans->insert(tc->roomId(), tc);
}
});
// iterate over batches of channel IDs
for (const auto &batch : splitListIntoBatches(twitchChans->keys()))
{
getHelix()->fetchStreams(
batch, {},
[twitchChans](std::vector<HelixStream> streams) {
for (const auto &stream : streams)
{
// remaining channels will be used later to set their stream status as offline
// so we use take(id) to remove it
auto tc = twitchChans->take(stream.userId);
if (tc == nullptr)
{
continue;
}
tc->parseLiveStatus(true, stream);
}
},
[]() {
// failure
},
[batch, twitchChans] {
// All the channels that were not present in fetchStreams response should be assumed to be offline
// It is necessary to update their stream status in case they've gone live -> offline
// Otherwise some of them will be marked as live forever
for (const auto &chID : batch)
{
auto tc = twitchChans->value(chID);
// early out in case channel does not exist anymore
if (tc == nullptr)
{
continue;
}
tc->parseLiveStatus(false, {});
}
});
}
}
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
if (dirtyChannelName.startsWith('#'))
-3
View File
@@ -49,8 +49,6 @@ public:
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
void bulkRefreshLiveStatus();
void reloadBTTVGlobalEmotes();
void reloadAllBTTVChannelEmotes();
void reloadFFZGlobalEmotes();
@@ -124,7 +122,6 @@ private:
BttvEmotes bttv;
FfzEmotes ffz;
SeventvEmotes seventv_;
QTimer bulkLiveStatusTimer_;
pajlada::Signals::SignalHolder signalHolder_;
};
+39
View File
@@ -417,6 +417,45 @@ void Helix::createClip(QString channelId,
.execute();
}
void Helix::fetchChannels(
QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
for (const auto &userID : userIDs)
{
urlQuery.addQueryItem("broadcaster_id", userID);
}
this->makeGet("channels", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixChannel> channels;
for (const auto &unparsedChannel : data.toArray())
{
channels.emplace_back(unparsedChannel.toObject());
}
successCallback(channels);
return Success;
})
.onError([failureCallback](auto /*result*/) {
failureCallback();
})
.execute();
}
void Helix::getChannel(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,
HelixFailureCallback failureCallback)
+12
View File
@@ -788,6 +788,12 @@ public:
std::function<void(HelixClipError)> failureCallback,
std::function<void()> finallyCallback) = 0;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
virtual void fetchChannels(
QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback) = 0;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
virtual void getChannel(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,
@@ -1101,6 +1107,12 @@ public:
std::function<void(HelixClipError)> failureCallback,
std::function<void()> finallyCallback) final;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
void fetchChannels(
QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback) final;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
void getChannel(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,
+2 -2
View File
@@ -43,7 +43,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams
Used in:
- `TwitchChannel` to get live status, game, title, and viewer count of a channel
- `LiveController` to get live status, game, title, and viewer count of a channel
- `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
### Create Clip
@@ -61,7 +61,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information
Used in:
- `TwitchChannel` to refresh stream title
- `LiveController` to refresh stream title & display name
### Update Channel