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 20 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 @@ -68,6 +68,7 @@
- Minor: Migrated /uniquechat and /r9kbeta to Helix API. (#4057)
- Minor: Migrated /uniquechatoff and /r9kbetaoff to Helix API. (#4057)
- Minor: Make menus and placeholders display appropriate custom key combos. (#4045)
- Minor: Migrated /chatters to Helix API. (#4019)
- Bugfix: Connection to Twitch PubSub now recovers more reliably. (#3643, #3716)
- Bugfix: Fixed `Smooth scrolling on new messages` setting sometimes hiding messages. (#4028)
- Bugfix: Fixed a crash that can occur when closing and quickly reopening a split, then running a command. (#3852)
Expand Down
5 changes: 5 additions & 0 deletions src/common/ChatterSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ std::vector<QString> ChatterSet::filterByPrefix(const QString &prefix) const
return result;
}

int ChatterSet::getNumChatters() const
{
return this->items.size();
pajlada marked this conversation as resolved.
Show resolved Hide resolved
}

} // namespace chatterino
2 changes: 2 additions & 0 deletions src/common/ChatterSet.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class ChatterSet
/// are in mixed case if available.
std::vector<QString> filterByPrefix(const QString &prefix) const;

int getNumChatters() const;

private:
// user name in lower case -> user name in normal case
cache::lru_cache<QString, QString> items;
Expand Down
36 changes: 32 additions & 4 deletions src/controllers/commands/CommandController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,13 @@ void CommandController::initialize(Settings &, Paths &paths)
});

this->registerCommand(
"/chatters", [](const auto & /*words*/, auto channel) {
"/chatters", [](const auto & words, auto channel) {
if (words.size() != 1)
{
channel->addMessage(makeSystemMessage("Usage: /chatters"));
return "";
}

auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());

if (twitchChannel == nullptr)
Expand All @@ -889,9 +895,31 @@ void CommandController::initialize(Settings &, Paths &paths)
return "";
}

channel->addMessage(makeSystemMessage(
QString("Chatter count: %1")
.arg(localizeNumbers(twitchChannel->chatterCount()))));
auto addChatterCountMessage = [channel, twitchChannel]() -> void {
channel->addMessage(makeSystemMessage(
QString("Chatter count: %1")
.arg(localizeNumbers(twitchChannel->chatterCount()))));
};

// Fail when user is not a moderator
if (!twitchChannel->hasModRights()) {
channel->addMessage(makeSystemMessage(QString("Error: Only moderators can get number of chatters")));
return "";
}

// Refresh chatter list via helix api for mods
getHelix()->getChatters(
twitchChannel->roomId(),
getApp()->accounts->twitch.getCurrent()->getUserId(),
[twitchChannel, addChatterCountMessage](HelixUserList *chatterList) {
twitchChannel->updateOnlineChatters(chatterList->users);
addChatterCountMessage();
},
[channel](auto error, auto message) {
auto errorMessage = Helix::formatHelixUserListErrorString(QString("chatters"), error, message);
channel->addMessage(makeSystemMessage(errorMessage));
}
);

return "";
});
Expand Down
74 changes: 20 additions & 54 deletions src/providers/twitch/TwitchChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,6 @@ namespace {
"Failed to create a clip - an unknown error occurred.");
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)};
}

} // namespace

TwitchChannel::TwitchChannel(const QString &name)
Expand Down Expand Up @@ -135,10 +111,10 @@ 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 All @@ -164,8 +140,8 @@ TwitchChannel::TwitchChannel(const QString &name)
void TwitchChannel::initialize()
{
this->fetchDisplayName();
this->refreshChatters();
pajlada marked this conversation as resolved.
Show resolved Hide resolved
this->refreshBadges();
this->refreshChatters();
}

bool TwitchChannel::isEmpty() const
Expand Down Expand Up @@ -621,7 +597,7 @@ const QString &TwitchChannel::popoutPlayerUrl()

int TwitchChannel::chatterCount()
{
return this->chatterCount_;
return this->accessChatters()->getNumChatters();
pajlada marked this conversation as resolved.
Show resolved Hide resolved
}

void TwitchChannel::setLive(bool newLiveStatus)
Expand Down Expand Up @@ -903,8 +879,13 @@ void TwitchChannel::refreshPubSub()
getApp()->twitch->pubsub->listenToChannelPointRewards(roomId);
}

void TwitchChannel::refreshChatters()
{
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 +898,16 @@ 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(),
[this](HelixUserList *chatterList) {
pajlada marked this conversation as resolved.
Show resolved Hide resolved
this->updateOnlineChatters(chatterList->users);
},
// Refresh chatters should only be used when failing silently is an option
[this](auto error, auto message) { }
);
}

void TwitchChannel::fetchDisplayName()
Expand Down
1 change: 0 additions & 1 deletion src/providers/twitch/TwitchChannel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ class TwitchChannel : public Channel, public ChannelChatters
const QString subscriptionUrl_;
const QString channelUrl_;
const QString popoutPlayerUrl_;
int chatterCount_;
UniqueAccess<StreamStatus> streamStatus_;
UniqueAccess<RoomModes> roomModes_;
std::atomic_flag loadingRecentMessages_ = ATOMIC_FLAG_INIT;
Expand Down
164 changes: 164 additions & 0 deletions src/providers/twitch/api/Helix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@

namespace chatterino {

HelixUserList::HelixUserList() {
pajlada marked this conversation as resolved.
Show resolved Hide resolved
users = std::unordered_set<QString>();
}

void HelixUserList::AddUsersFromResponse(const QJsonObject &response) {
pajlada marked this conversation as resolved.
Show resolved Hide resolved
for (const auto jsonStream : response.value("data").toArray())
{
auto user = jsonStream.toObject().value("user_login").toString();
users.insert(user);
pajlada marked this conversation as resolved.
Show resolved Hide resolved
}
users = std::move(users);
pajlada marked this conversation as resolved.
Show resolved Hide resolved
}

static IHelix *instance = nullptr;

void Helix::fetchUsers(QStringList userIds, QStringList userLogins,
Expand Down Expand Up @@ -1991,6 +2004,157 @@ void Helix::sendWhisper(
.execute();
}

QString Helix::formatHelixUserListErrorString(
QString userType,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
HelixUserListError error,
QString message
pajlada marked this conversation as resolved.
Show resolved Hide resolved
) {
using Error = HelixUserListError;

QString errorMessage = QString("Failed to get list of ") + userType + QString(": ");

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 += "You don't have permission to "
"perform that action.";
}
break;

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

// Recursive function with a maximun page of 50
void Helix::getApiListRecursive(
HelixUserList *list,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
QString url, QUrlQuery urlQuery,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
int page, QString paginationCursor,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
ResultCallback<HelixUserList*> successCallback,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
FailureCallback<HelixUserListError, QString> failureCallback
pajlada marked this conversation as resolved.
Show resolved Hide resolved
) {
using Error = HelixUserListError;

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

this->makeRequest(url, urlQuery)
.type(NetworkRequestType::Get)
.header("Content-Type", "application/json")
.onSuccess([=](auto result) -> Outcome {
if (result.status() != 200)
{
qCWarning(chatterinoTwitch)
<< "Success result for getting user list data was "
<< result.status() << "but we expected it to be 200";
}

auto response = result.parseJson();

if (page == 1) {
list->total = response.value("total").toInt();
}

QString newCursor = response.value("pagination").toObject().value("cursor").toString();
list->AddUsersFromResponse(response);

// Call function with next page until page 50 is reached or there are no more results
if (page <= 50 && !newCursor.isEmpty()) {
pajlada marked this conversation as resolved.
Show resolved Hide resolved
getApiListRecursive(list, url, urlQuery, page+1, newCursor, successCallback, failureCallback);
} else {
successCallback(list);
}
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
Copy link
Contributor

Choose a reason for hiding this comment

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

warning: repeated branch in conditional chain [bugprone-branch-clone]

                    {
                    ^

src/providers/twitch/api/Helix.cpp:2050: end of the original

                    }
                     ^

src/providers/twitch/api/Helix.cpp:2052: clone 1 starts here

                    {
                    ^

src/providers/twitch/api/Helix.cpp:2056: clone 2 starts here

                    {
                    ^

failureCallback(Error::UserMissingScope, message);
}
else if (message.compare(
"The ID in moderator_id must match the user "
"ID found in the request's OAuth token.",
Qt::CaseInsensitive) == 0 || message.compare(
"The ID in broadcaster_id must match the user "
"ID found in the request's OAuth token.",
Qt::CaseInsensitive) == 0)
{
// get moderators: Must be broadcaster
// get chatters: Must must have permission to moderate the broadcaster’s chat room.
failureCallback(Error::UserNotAuthorized, message);
}
else
{
failureCallback(Error::Forwarded, message);
}
}
break;

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

default: {
qCDebug(chatterinoTwitch)
<< "Unhandled error getting user list:" << result.status()
<< result.getData() << obj;
failureCallback(Error::Unknown, message);
}
break;
}
})
.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,
pajlada marked this conversation as resolved.
Show resolved Hide resolved
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: the parameter 'broadcasterID' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param]

Suggested change
QString broadcasterID, QString moderatorID,
const QString& broadcasterID, QString moderatorID,

Copy link
Contributor

Choose a reason for hiding this comment

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

warning: the parameter 'moderatorID' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param]

Suggested change
QString broadcasterID, QString moderatorID,
QString broadcasterID, const QString& moderatorID,

ResultCallback<HelixUserList*> successCallback,
FailureCallback<HelixUserListError, 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 'successCallback' is unused [misc-unused-parameters]

Suggested change
ResultCallback<HelixUserList*> successCallback,
FailureCallback<HelixUserListError, QString> failureCallback)
,
, /*successCallback*/

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<HelixUserListError, QString> failureCallback)
, /*failureCallback*/

{
auto chatterList = new HelixUserList();
Copy link
Contributor

Choose a reason for hiding this comment

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

warning: 'auto chatterList' can be declared as 'auto *chatterList' [llvm-qualified-auto]

Suggested change
auto chatterList = new HelixUserList();
{auto *

QString paginationCursor("");

QUrlQuery urlQuery;

urlQuery.addQueryItem("broadcaster_id", broadcasterID);
urlQuery.addQueryItem("moderator_id", moderatorID);

this->getApiListRecursive(chatterList, "chat/chatters", urlQuery, 1, paginationCursor, successCallback, failureCallback);
}

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