Migrate viewer list to Helix (#4117)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Colton Clemmer 2023-03-27 11:26:08 -05:00 committed by GitHub
parent 1797b04329
commit 3d4985c88f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 292 additions and 42 deletions

View file

@ -2,6 +2,7 @@
## Unversioned
- Minor: Migrated viewer list to Helix API. (#4117)
- Minor: Include normally-stripped mention in replies in logs. (#4420)
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)

View file

@ -10,6 +10,7 @@
#include "controllers/hotkeys/HotkeyController.hpp"
#include "controllers/notifications/NotificationController.hpp"
#include "messages/MessageThread.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
@ -52,11 +53,140 @@
#include <QMimeData>
#include <QMovie>
#include <QPainter>
#include <QSet>
#include <QVBoxLayout>
#include <functional>
#include <random>
namespace {
using namespace chatterino;
QString formatVIPListError(HelixListVIPsError error, const QString &message)
{
using Error = HelixListVIPsError;
QString errorMessage = QString("Failed to list VIPs - ");
switch (error)
{
case Error::Forwarded: {
errorMessage += message;
}
break;
case Error::Ratelimited: {
errorMessage += "You are being ratelimited by Twitch. Try "
"again in a few seconds.";
}
break;
case Error::UserMissingScope: {
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
errorMessage += "Missing required scope. "
"Re-login with your "
"account and try again.";
}
break;
case Error::UserNotAuthorized: {
// TODO(pajlada): Phrase MISSING_PERMISSION
errorMessage += "You don't have permission to "
"perform that action.";
}
break;
case Error::UserNotBroadcaster: {
errorMessage +=
"Due to Twitch restrictions, "
"this command can only be used by the broadcaster. "
"To see the list of VIPs you must use the Twitch website.";
}
break;
case Error::Unknown: {
errorMessage += "An unknown error has occurred.";
}
break;
}
return errorMessage;
}
QString formatModsError(HelixGetModeratorsError error, QString message)
{
using Error = HelixGetModeratorsError;
QString errorMessage = QString("Failed to get moderators: ");
switch (error)
{
case Error::Forwarded: {
errorMessage += message;
}
break;
case Error::UserMissingScope: {
errorMessage += "Missing required scope. "
"Re-login with your "
"account and try again.";
}
break;
case Error::UserNotAuthorized: {
errorMessage +=
"Due to Twitch restrictions, "
"this command can only be used by the broadcaster. "
"To see the list of mods you must use the Twitch website.";
}
break;
case Error::Unknown: {
errorMessage += "An unknown error has occurred.";
}
break;
}
return errorMessage;
}
QString formatChattersError(HelixGetChattersError error, QString message)
{
using Error = HelixGetChattersError;
QString errorMessage = QString("Failed to get chatters: ");
switch (error)
{
case Error::Forwarded: {
errorMessage += message;
}
break;
case Error::UserMissingScope: {
errorMessage += "Missing required scope. "
"Re-login with your "
"account and try again.";
}
break;
case Error::UserNotAuthorized: {
errorMessage +=
"Due to Twitch restrictions, "
"this command can only be used by moderators. "
"To see the list of chatters you must use the Twitch website.";
}
break;
case Error::Unknown: {
errorMessage += "An unknown error has occurred.";
}
break;
}
return errorMessage;
}
} // namespace
namespace chatterino {
namespace {
void showTutorialVideo(QWidget *parent, const QString &source,
@ -1000,6 +1130,26 @@ void Split::showViewerList()
auto chattersList = new QListWidget();
auto resultList = new QListWidget();
auto channel = this->getChannel();
if (!channel)
{
qCWarning(chatterinoWidget)
<< "Viewer list opened when no channel was defined";
return;
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
qCWarning(chatterinoWidget)
<< "Viewer list opened in a non-Twitch channel";
return;
}
auto *loadingLabel = new QLabel("Loading...");
searchBar->setPlaceholderText("Search User...");
auto formatListItemText = [](QString text) {
auto item = new QListWidgetItem();
item->setText(text);
@ -1007,15 +1157,26 @@ void Split::showViewerList()
return item;
};
static QStringList labels = {
"Broadcaster", "Moderators", "VIPs", "Staff",
"Admins", "Global Moderators", "Viewers"};
static QStringList jsonLabels = {"broadcaster", "moderators", "vips",
"staff", "admins", "global_mods",
"viewers"};
auto loadingLabel = new QLabel("Loading...");
auto addLabel = [this, formatListItemText, chattersList](QString label) {
auto formattedLabel = formatListItemText(label);
formattedLabel->setForeground(this->theme->accent);
chattersList->addItem(formattedLabel);
};
searchBar->setPlaceholderText("Search User...");
auto addUserList = [=](QStringList users, QString label) {
if (users.isEmpty())
{
return;
}
addLabel(QString("%1 (%2)").arg(label, localizeNumbers(users.size())));
for (const auto &user : users)
{
chattersList->addItem(formatListItemText(user));
}
chattersList->addItem(new QListWidgetItem());
};
auto performListSearch = [=]() {
auto query = searchBar->text();
@ -1039,46 +1200,134 @@ void Split::showViewerList()
resultList->show();
};
QObject::connect(searchBar, &QLineEdit::textEdited, this,
performListSearch);
auto loadChatters = [=](auto modList, auto vipList, bool isBroadcaster) {
getHelix()->getChatters(
twitchChannel->roomId(),
getApp()->accounts->twitch.getCurrent()->getUserId(), 50000,
[=](auto chatters) {
auto broadcaster = channel->getName().toLower();
QStringList chatterList;
QStringList modChatters;
QStringList vipChatters;
NetworkRequest::twitchRequest("https://tmi.twitch.tv/group/user/" +
this->getChannel()->getName() + "/chatters")
.caller(this)
.onSuccess([=, this](auto result) -> Outcome {
auto obj = result.parseJson();
QJsonObject chattersObj = obj.value("chatters").toObject();
viewerDock->setWindowTitle(
QString("Viewer List - %1 (%2 chatters)")
.arg(this->getChannel()->getName())
.arg(localizeNumbers(obj.value("chatter_count").toInt())));
loadingLabel->hide();
for (int i = 0; i < jsonLabels.size(); i++)
bool addedBroadcaster = false;
for (auto chatter : chatters.chatters)
{
auto currentCategory =
chattersObj.value(jsonLabels.at(i)).toArray();
// If current category of chatters is empty, dont show this
// category.
if (currentCategory.empty())
chatter = chatter.toLower();
if (!addedBroadcaster && chatter == broadcaster)
{
addedBroadcaster = true;
addLabel("Broadcaster");
chattersList->addItem(broadcaster);
chattersList->addItem(new QListWidgetItem());
continue;
auto label = formatListItemText(QString("%1 (%2)").arg(
labels.at(i), localizeNumbers(currentCategory.size())));
label->setForeground(this->theme->accent);
chattersList->addItem(label);
foreach (const QJsonValue &v, currentCategory)
{
chattersList->addItem(formatListItemText(v.toString()));
}
if (modList.contains(chatter))
{
modChatters.append(chatter);
continue;
}
if (vipList.contains(chatter))
{
vipChatters.append(chatter);
continue;
}
chatterList.append(chatter);
}
modChatters.sort();
vipChatters.sort();
chatterList.sort();
if (isBroadcaster)
{
addUserList(modChatters, QString("Moderators"));
addUserList(vipChatters, QString("VIPs"));
}
else
{
addLabel("Moderators");
chattersList->addItem(
"Moderators cannot check who is a moderator");
chattersList->addItem(new QListWidgetItem());
addLabel("VIPs");
chattersList->addItem(
"Moderators cannot check who is a VIP");
chattersList->addItem(new QListWidgetItem());
}
addUserList(chatterList, QString("Chatters"));
loadingLabel->hide();
performListSearch();
return Success;
})
.execute();
},
[chattersList, formatListItemText](auto error, auto message) {
auto errorMessage = formatChattersError(error, message);
chattersList->addItem(formatListItemText(errorMessage));
});
};
QObject::connect(searchBar, &QLineEdit::textEdited, this,
performListSearch);
// Only broadcaster can get vips, mods can get chatters
if (channel->isBroadcaster())
{
// Add moderators
getHelix()->getModerators(
twitchChannel->roomId(), 1000,
[=](auto mods) {
QSet<QString> modList;
for (const auto &mod : mods)
{
modList.insert(mod.userName.toLower());
}
// Add vips
getHelix()->getChannelVIPs(
twitchChannel->roomId(),
[=](auto vips) {
QSet<QString> vipList;
for (const auto &vip : vips)
{
vipList.insert(vip.userName.toLower());
}
// Add chatters
loadChatters(modList, vipList, true);
},
[chattersList, formatListItemText](auto error,
auto message) {
auto errorMessage = formatVIPListError(error, message);
chattersList->addItem(formatListItemText(errorMessage));
});
},
[chattersList, formatListItemText](auto error, auto message) {
auto errorMessage = formatModsError(error, message);
chattersList->addItem(formatListItemText(errorMessage));
});
}
else if (channel->hasModRights())
{
QSet<QString> modList;
QSet<QString> vipList;
loadChatters(modList, vipList, false);
}
else
{
chattersList->addItem(
formatListItemText("Due to Twitch restrictions, this feature is "
"only \navailable for moderators."));
chattersList->addItem(
formatListItemText("If you would like to see the Viewer list, you "
"must \nuse the Twitch website."));
loadingLabel->hide();
}
QObject::connect(viewerDock, &QDockWidget::topLevelChanged, this, [=]() {
viewerDock->setMinimumWidth(300);