Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate /chatters commands to use Helix api #4088

Merged
merged 37 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ade2972
Migrate /chatters to helix api
cbclemmer Oct 9, 2022
b230adc
Text clean up
cbclemmer Oct 9, 2022
8ecb341
Only refresh chatters for mods
cbclemmer Oct 11, 2022
efe0414
Update from master
cbclemmer Oct 11, 2022
abe1c6e
Recursively call next page until end of list or page 50
cbclemmer Oct 11, 2022
c7a7e2a
Use helix api for viewer list and /chatters command
cbclemmer Oct 11, 2022
bd080b1
Add todo note
cbclemmer Oct 11, 2022
bca041d
Fetch chatter count from cache when user does not have mod rights
cbclemmer Oct 13, 2022
90dacda
Add list of vips to viewer list
cbclemmer Oct 13, 2022
6ba0a2e
Moderator list WIP
cbclemmer Oct 28, 2022
59b2e38
Fix compilation error
cbclemmer Oct 28, 2022
b1c8a0d
Override /mod command
cbclemmer Oct 28, 2022
5a82905
format error depending upon user type
cbclemmer Oct 29, 2022
af55ed5
Update from master
cbclemmer Oct 29, 2022
e934078
Add warning for extra args in /chatters
cbclemmer Oct 29, 2022
6c28042
Clean up
cbclemmer Oct 29, 2022
61707aa
Update change log
cbclemmer Oct 29, 2022
03e503b
Roll back to only change /chatters
cbclemmer Oct 30, 2022
fc268df
Periodically refresh chatter list for mods
cbclemmer Oct 30, 2022
cb74b28
Move refreshChatters back to private function
cbclemmer Oct 30, 2022
f3059b4
Add chatter count helix api
cbclemmer Oct 31, 2022
9085b29
Fix refresh chatters
cbclemmer Oct 31, 2022
7e16257
Remove unused get num chatters
cbclemmer Oct 31, 2022
e271460
Clean up
cbclemmer Oct 31, 2022
c0618c8
Move general error to get chatters error
cbclemmer Nov 1, 2022
2cbde99
Reformat code
pajlada Nov 1, 2022
559da26
Merge remote-tracking branch 'origin/master' into cc/#4019
pajlada Nov 1, 2022
f6cce29
Fix changelog entry
pajlada Nov 1, 2022
58c18fe
Use auto for the chatterList in the success lambda
pajlada Nov 1, 2022
7e03bdc
Undo some old unneccesary changes
pajlada Nov 1, 2022
817187e
Remove unused variable `paginationCursor` from Helix::getChatters
pajlada Nov 1, 2022
d2069c4
No longer care what arguments are provided for `/chatters`
pajlada Nov 1, 2022
9d44cb2
Don't early out for mod rights check - rely on the API error
pajlada Nov 1, 2022
a3aed80
Refactor pagination logic
pajlada Nov 1, 2022
0f61ea8
Add mock test function
pajlada Nov 1, 2022
4e868a0
Merge branch 'master' into cc/#4019
pajlada Nov 1, 2022
8c710be
Add include to make std::unordered_set<QString> work for Qt5.12
pajlada Nov 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
- Minor: Migrated /uniquechatoff and /r9kbetaoff to Helix API. (#4057)
- Minor: Added stream titles to windows live toast notifications. (#1297)
- Minor: Make menus and placeholders display appropriate custom key combos. (#4045)
- Minor: Migrated /chatters to Helix API. (#4088)
- Minor: Add settings tooltips. (#3437)
- Bugfix: Connection to Twitch PubSub now recovers more reliably. (#3643, #3716)
- Bugfix: Fixed `Smooth scrolling on new messages` setting sometimes hiding messages. (#4028)
Expand Down
64 changes: 53 additions & 11 deletions src/controllers/commands/CommandController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -878,23 +878,65 @@ void CommandController::initialize(Settings &, Paths &paths)
return "";
});

this->registerCommand(
"/chatters", [](const auto & /*words*/, auto channel) {
auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
this->registerCommand("/chatters", [](const auto &words, auto channel) {
pajlada marked this conversation as resolved.
Show resolved Hide resolved
auto formatError = [](HelixGetChattersError error, QString message) {
pajlada marked this conversation as resolved.
Show resolved Hide resolved
using Error = HelixGetChattersError;

if (twitchChannel == nullptr)
QString errorMessage = QString("Failed to get chatter count: ");

switch (error)
{
channel->addMessage(makeSystemMessage(
"The /chatters command only works in Twitch Channels"));
return "";
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 += "You must have moderator permissions to "
"use this command.";
}
break;

case Error::Unknown: {
errorMessage += "An unknown error has occurred.";
}
break;
}
return errorMessage;
};

channel->addMessage(makeSystemMessage(
QString("Chatter count: %1")
.arg(localizeNumbers(twitchChannel->chatterCount()))));
auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
pajlada marked this conversation as resolved.
Show resolved Hide resolved

if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /chatters command only works in Twitch Channels"));
return "";
});
}

// Refresh chatter list via helix api for mods
getHelix()->getChatters(
twitchChannel->roomId(),
getApp()->accounts->twitch.getCurrent()->getUserId(), 1,
[channel](auto result) {
channel->addMessage(
makeSystemMessage(QString("Chatter count: %1")
.arg(localizeNumbers(result.total))));
},
[channel, formatError](auto error, auto message) {
auto errorMessage = formatError(error, message);
channel->addMessage(makeSystemMessage(errorMessage));
});

return "";
});

this->registerCommand("/clip", [](const auto & /*words*/, auto channel) {
if (const auto type = channel->getType();
Expand Down
71 changes: 23 additions & 48 deletions src/providers/twitch/TwitchChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,8 @@ namespace {
const QString LOGIN_PROMPT_TEXT("Click here to add your account again.");
const Link ACCOUNTS_LINK(Link::OpenAccountsPage, QString());

std::pair<Outcome, std::unordered_set<QString>> parseChatters(
const QJsonObject &jsonRoot)
{
static QStringList categories = {"broadcaster", "vips", "moderators",
"staff", "admins", "global_mods",
"viewers"};

auto usernames = std::unordered_set<QString>();

// parse json
QJsonObject jsonCategories = jsonRoot.value("chatters").toObject();

for (const auto &category : categories)
{
for (auto jsonCategory : jsonCategories.value(category).toArray())
{
usernames.insert(jsonCategory.toString());
}
}

return {Success, std::move(usernames)};
}

// Maximum number of chatters to fetch when refreshing chatters
constexpr auto MAX_CHATTERS_TO_FETCH = 5000;
} // namespace

TwitchChannel::TwitchChannel(const QString &name)
Expand Down Expand Up @@ -136,9 +115,11 @@ TwitchChannel::TwitchChannel(const QString &name)
});

// timers

QObject::connect(&this->chattersListTimer_, &QTimer::timeout, [=] {
this->refreshChatters();
});

this->chattersListTimer_.start(5 * 60 * 1000);

QObject::connect(&this->threadClearTimer_, &QTimer::timeout, [=] {
Expand Down Expand Up @@ -905,6 +886,12 @@ void TwitchChannel::refreshPubSub()

void TwitchChannel::refreshChatters()
{
// helix endpoint only works for mods
if (!this->hasModRights())
{
return;
}

// setting?
const auto streamStatus = this->accessStreamStatus();
const auto viewerCount = static_cast<int>(streamStatus->viewerCount);
Expand All @@ -917,31 +904,19 @@ void TwitchChannel::refreshChatters()
}
}

// get viewer list
NetworkRequest("https://tmi.twitch.tv/group/user/" + this->getName() +
"/chatters")

.onSuccess(
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
// channel still exists?
auto shared = weak.lock();
if (!shared)
{
return Failure;
}

auto data = result.parseJson();
this->chatterCount_ = data.value("chatter_count").toInt();

auto pair = parseChatters(std::move(data));
if (pair.first)
{
this->updateOnlineChatters(pair.second);
}

return pair.first;
})
.execute();
// Get chatter list via helix api
getHelix()->getChatters(
this->roomId(), getApp()->accounts->twitch.getCurrent()->getUserId(),
MAX_CHATTERS_TO_FETCH,
[this, weak = weakOf<Channel>(this)](auto result) {
if (auto shared = weak.lock())
{
this->updateOnlineChatters(result.chatters);
this->chatterCount_ = result.total;
}
},
// Refresh chatters should only be used when failing silently is an option
[](auto error, auto message) {});
}

void TwitchChannel::fetchDisplayName()
Expand Down
114 changes: 114 additions & 0 deletions src/providers/twitch/api/Helix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,83 @@ void Helix::updateChatSettings(
.execute();
}

// https://dev.twitch.tv/docs/api/reference#get-chatters
void Helix::fetchChatters(
QString broadcasterID, QString moderatorID, int first, QString after,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
pajlada marked this conversation as resolved.
Show resolved Hide resolved
pajlada marked this conversation as resolved.
Show resolved Hide resolved
ResultCallback<HelixChatters> successCallback,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
FailureCallback<HelixGetChattersError, QString> failureCallback)
pajlada marked this conversation as resolved.
Show resolved Hide resolved
{
using Error = HelixGetChattersError;

QUrlQuery urlQuery;

urlQuery.addQueryItem("broadcaster_id", broadcasterID);
urlQuery.addQueryItem("moderator_id", moderatorID);
urlQuery.addQueryItem("first", QString::number(first));

if (!after.isEmpty())
{
urlQuery.addQueryItem("after", after);
}

this->makeRequest("chat/chatters", urlQuery)
.onSuccess([successCallback](auto result) -> Outcome {
if (result.status() != 200)
{
qCWarning(chatterinoTwitch)
<< "Success result for getting chatters was "
<< result.status() << "but we expected it to be 200";
}

auto response = result.parseJson();
successCallback(HelixChatters(response));
return Success;
})
.onError([failureCallback](auto result) {
auto obj = result.parseJson();
auto message = obj.value("message").toString();

switch (result.status())
{
case 400: {
failureCallback(Error::Forwarded, message);
}
break;

case 401: {
if (message.startsWith("Missing scope",
Qt::CaseInsensitive))
{
pajlada marked this conversation as resolved.
Show resolved Hide resolved
failureCallback(Error::UserMissingScope, message);
}
else if (message.contains("OAuth token"))
{
failureCallback(Error::UserNotAuthorized, message);
}
else
{
failureCallback(Error::Forwarded, message);
}
}
break;

case 403: {
failureCallback(Error::UserNotAuthorized, message);
}
break;

default: {
qCDebug(chatterinoTwitch)
<< "Unhandled error data:" << result.status()
<< result.getData() << obj;
failureCallback(Error::Unknown, message);
}
break;
}
})
.execute();
}

// Ban/timeout a user
// https://dev.twitch.tv/docs/api/reference#ban-user
void Helix::banUser(QString broadcasterID, QString moderatorID, QString userID,
Expand Down Expand Up @@ -1991,6 +2068,43 @@ void Helix::sendWhisper(
.execute();
}

// https://dev.twitch.tv/docs/api/reference#get-chatters
void Helix::getChatters(
pajlada marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: method 'getChatters' can be made static [readability-convert-member-functions-to-static]

Suggested change
void Helix::getChatters(
static void Helix::getChatters(

QString broadcasterID, QString moderatorID, int maxChattersToFetch,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
pajlada marked this conversation as resolved.
Show resolved Hide resolved
ResultCallback<HelixChatters> successCallback,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
FailureCallback<HelixGetChattersError, QString> failureCallback)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: parameter 'failureCallback' is unused [misc-unused-parameters]

Suggested change
FailureCallback<HelixGetChattersError, QString> failureCallback)
FailureCallback<HelixGetChattersError, QString> /*failureCallback*/)

{
static const auto NUM_CHATTERS_TO_FETCH = 1000;
pajlada marked this conversation as resolved.
Show resolved Hide resolved

auto finalChatters = std::make_shared<HelixChatters>();

ResultCallback<HelixChatters> fetchSuccess;
pajlada marked this conversation as resolved.
Show resolved Hide resolved

fetchSuccess = [this, broadcasterID, moderatorID, maxChattersToFetch,
finalChatters, &fetchSuccess, successCallback,
failureCallback](auto chatters) {
qCDebug(chatterinoTwitch)
<< "Fetched" << chatters.chatters.size() << "chatters";
finalChatters->chatters.merge(chatters.chatters);
finalChatters->total = chatters.total;

if (chatters.cursor.isEmpty() ||
finalChatters->chatters.size() >= maxChattersToFetch)
{
// Done paginating
successCallback(*finalChatters);
return;
}

this->fetchChatters(broadcasterID, moderatorID, NUM_CHATTERS_TO_FETCH,
chatters.cursor, fetchSuccess, failureCallback);
};

// Initiate the recursive calls
this->fetchChatters(broadcasterID, moderatorID, NUM_CHATTERS_TO_FETCH, "",
fetchSuccess, failureCallback);
}

// List the VIPs of a channel
// https://dev.twitch.tv/docs/api/reference#get-vips
void Helix::getChannelVIPs(
Expand Down
Loading