mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Migrate viewer list to Helix (#4117)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
1797b04329
commit
3d4985c88f
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
|
- Minor: Migrated viewer list to Helix API. (#4117)
|
||||||
- Minor: Include normally-stripped mention in replies in logs. (#4420)
|
- Minor: Include normally-stripped mention in replies in logs. (#4420)
|
||||||
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
|
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
|
||||||
- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)
|
- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include "controllers/hotkeys/HotkeyController.hpp"
|
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||||
#include "controllers/notifications/NotificationController.hpp"
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
#include "messages/MessageThread.hpp"
|
#include "messages/MessageThread.hpp"
|
||||||
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
@ -52,11 +53,140 @@
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QSet>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <random>
|
#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 chatterino {
|
||||||
namespace {
|
namespace {
|
||||||
void showTutorialVideo(QWidget *parent, const QString &source,
|
void showTutorialVideo(QWidget *parent, const QString &source,
|
||||||
|
@ -1000,6 +1130,26 @@ void Split::showViewerList()
|
||||||
auto chattersList = new QListWidget();
|
auto chattersList = new QListWidget();
|
||||||
auto resultList = 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 formatListItemText = [](QString text) {
|
||||||
auto item = new QListWidgetItem();
|
auto item = new QListWidgetItem();
|
||||||
item->setText(text);
|
item->setText(text);
|
||||||
|
@ -1007,15 +1157,26 @@ void Split::showViewerList()
|
||||||
return item;
|
return item;
|
||||||
};
|
};
|
||||||
|
|
||||||
static QStringList labels = {
|
auto addLabel = [this, formatListItemText, chattersList](QString label) {
|
||||||
"Broadcaster", "Moderators", "VIPs", "Staff",
|
auto formattedLabel = formatListItemText(label);
|
||||||
"Admins", "Global Moderators", "Viewers"};
|
formattedLabel->setForeground(this->theme->accent);
|
||||||
static QStringList jsonLabels = {"broadcaster", "moderators", "vips",
|
chattersList->addItem(formattedLabel);
|
||||||
"staff", "admins", "global_mods",
|
};
|
||||||
"viewers"};
|
|
||||||
auto loadingLabel = new QLabel("Loading...");
|
|
||||||
|
|
||||||
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 performListSearch = [=]() {
|
||||||
auto query = searchBar->text();
|
auto query = searchBar->text();
|
||||||
|
@ -1039,46 +1200,134 @@ void Split::showViewerList()
|
||||||
resultList->show();
|
resultList->show();
|
||||||
};
|
};
|
||||||
|
|
||||||
QObject::connect(searchBar, &QLineEdit::textEdited, this,
|
auto loadChatters = [=](auto modList, auto vipList, bool isBroadcaster) {
|
||||||
performListSearch);
|
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/" +
|
bool addedBroadcaster = false;
|
||||||
this->getChannel()->getName() + "/chatters")
|
for (auto chatter : chatters.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++)
|
|
||||||
{
|
{
|
||||||
auto currentCategory =
|
chatter = chatter.toLower();
|
||||||
chattersObj.value(jsonLabels.at(i)).toArray();
|
|
||||||
// If current category of chatters is empty, dont show this
|
if (!addedBroadcaster && chatter == broadcaster)
|
||||||
// category.
|
{
|
||||||
if (currentCategory.empty())
|
addedBroadcaster = true;
|
||||||
|
addLabel("Broadcaster");
|
||||||
|
chattersList->addItem(broadcaster);
|
||||||
|
chattersList->addItem(new QListWidgetItem());
|
||||||
continue;
|
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());
|
chattersList->addItem(new QListWidgetItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addUserList(chatterList, QString("Chatters"));
|
||||||
|
|
||||||
|
loadingLabel->hide();
|
||||||
performListSearch();
|
performListSearch();
|
||||||
return Success;
|
},
|
||||||
})
|
[chattersList, formatListItemText](auto error, auto message) {
|
||||||
.execute();
|
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, [=]() {
|
QObject::connect(viewerDock, &QDockWidget::topLevelChanged, this, [=]() {
|
||||||
viewerDock->setMinimumWidth(300);
|
viewerDock->setMinimumWidth(300);
|
||||||
|
|
Loading…
Reference in a new issue