From 7d9f4c2b0c8fac7ed39124f72809f698ab1f0534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sun, 14 Feb 2021 14:01:13 +0100 Subject: [PATCH] Migrated block, unblock and get user block list methods to Helix (#2370) Co-authored-by: pajlada --- CHANGELOG.md | 2 + .../commands/CommandController.cpp | 195 +++++++++++------ src/providers/twitch/TwitchAccount.cpp | 201 ++++-------------- src/providers/twitch/TwitchAccount.hpp | 30 +-- src/providers/twitch/TwitchAccountManager.cpp | 2 +- src/providers/twitch/TwitchMessageBuilder.cpp | 7 +- src/providers/twitch/TwitchUser.hpp | 8 + src/providers/twitch/api/Helix.cpp | 94 +++++++- src/providers/twitch/api/Helix.hpp | 27 +++ src/providers/twitch/api/README.md | 51 ++--- src/singletons/Settings.hpp | 6 +- src/widgets/dialogs/UserInfoPopup.cpp | 110 ++++++---- src/widgets/dialogs/UserInfoPopup.hpp | 2 +- src/widgets/settingspages/IgnoresPage.cpp | 20 +- 14 files changed, 406 insertions(+), 349 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 816270312..e3d8158c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Major: Added "Channel Filters". See https://wiki.chatterino.com/Filters/ for how they work or how to configure them. (#1748, #2083, #2090, #2200, #2225) - Major: Added Streamer Mode configuration (under `Settings -> General`), where you can select which features of Chatterino should behave differently when you are in Streamer Mode. (#2001, #2316, #2342, #2376) - Major: Color mentions to match the mentioned users. You can disable this by unchecking "Color @usernames" under `Settings -> General -> Advanced (misc.)`. (#1963, #2284) +- Major: Commands `/ignore` and `/unignore` have been renamed to `/block` and `/unblock` in order to keep consistency with Twitch's terms. (#2370) - Minor: Added `/marker` command - similar to webchat, it creates a stream marker. (#2360) - Minor: Added `/chatters` command showing chatter count. (#2344) - Minor: Added a button to the split context menu to open the moderation view for a channel when the account selected has moderator permissions. (#2321) @@ -80,6 +81,7 @@ - Dev: Migrated `Kraken::getUser` to Helix (#2260) - Dev: Migrated `TwitchAccount::(un)followUser` from Kraken to Helix and moved it to `Helix::(un)followUser`. (#2306) - Dev: Migrated `Kraken::getChannel` to Helix. (#2381) +- Dev: Migrated `TwitchAccount::(un)ignoreUser` to Helix and made `TwitchAccount::loadIgnores` use Helix call. (#2370) - Dev: Build in CI with multiple Qt versions (#2349) - Dev: Updated minimum required macOS version to 10.14 (#2386) - Dev: Removed unused `humanize` library (#2422) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index f1b7479a7..068d4ace1 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -230,15 +230,135 @@ void CommandController::initialize(Settings &, Paths &paths) this->items_.append(command); } - this->registerCommand("/debug-args", [](const auto &words, auto channel) { - QString msg = QApplication::instance()->arguments().join(' '); + /// Deprecated commands - channel->addMessage(makeSystemMessage(msg)); + auto blockLambda = [](const auto &words, auto channel) { + if (words.size() < 2) + { + channel->addMessage(makeSystemMessage("Usage: /block [user]")); + return ""; + } + auto currentUser = getApp()->accounts->twitch.getCurrent(); + + if (currentUser->isAnon()) + { + channel->addMessage( + makeSystemMessage("You must be logged in to block someone!")); + return ""; + } + + auto target = words.at(1); + + getHelix()->getUserByName( + target, + [currentUser, channel, target](const HelixUser &targetUser) { + getApp()->accounts->twitch.getCurrent()->blockUser( + targetUser.id, + [channel, target, targetUser] { + channel->addMessage(makeSystemMessage( + QString("You successfully blocked user %1") + .arg(target))); + }, + [channel, target] { + channel->addMessage(makeSystemMessage( + QString("User %1 couldn't be blocked, an unknown " + "error occurred!") + .arg(target))); + }); + }, + [channel, target] { + channel->addMessage( + makeSystemMessage(QString("User %1 couldn't be blocked, no " + "user with that name found!") + .arg(target))); + }); + + return ""; + }; + + auto unblockLambda = [](const auto &words, auto channel) { + if (words.size() < 2) + { + channel->addMessage(makeSystemMessage("Usage: /unblock [user]")); + return ""; + } + + auto currentUser = getApp()->accounts->twitch.getCurrent(); + + if (currentUser->isAnon()) + { + channel->addMessage( + makeSystemMessage("You must be logged in to unblock someone!")); + return ""; + } + + auto target = words.at(1); + + getHelix()->getUserByName( + target, + [currentUser, channel, target](const auto &targetUser) { + getApp()->accounts->twitch.getCurrent()->unblockUser( + targetUser.id, + [channel, target, targetUser] { + channel->addMessage(makeSystemMessage( + QString("You successfully unblocked user %1") + .arg(target))); + }, + [channel, target] { + channel->addMessage(makeSystemMessage( + QString("User %1 couldn't be unblocked, an unknown " + "error occurred!") + .arg(target))); + }); + }, + [channel, target] { + channel->addMessage( + makeSystemMessage(QString("User %1 couldn't be unblocked, " + "no user with that name found!") + .arg(target))); + }); + + return ""; + }; + + this->registerCommand("/logs", [](const auto & /*words*/, auto channel) { + channel->addMessage(makeSystemMessage( + "Online logs functionality has been removed. If you're a " + "moderator, you can use the /user command")); return ""; }); - this->registerCommand("/uptime", [](const auto &words, auto channel) { + this->registerCommand( + "/ignore", [blockLambda](const auto &words, auto channel) { + channel->addMessage(makeSystemMessage( + "Ignore command has been renamed to /block, please use it from " + "now on as /ignore is going to be removed soon.")); + blockLambda(words, channel); + return ""; + }); + + this->registerCommand( + "/unignore", [unblockLambda](const auto &words, auto channel) { + channel->addMessage(makeSystemMessage( + "Unignore command has been renamed to /unblock, please use it " + "from now on as /unignore is going to be removed soon.")); + unblockLambda(words, channel); + return ""; + }); + + /// Supported commands + + this->registerCommand( + "/debug-args", [](const auto & /*words*/, auto channel) { + QString msg = QApplication::instance()->arguments().join(' '); + + channel->addMessage(makeSystemMessage(msg)); + + return ""; + }); + + this->registerCommand("/uptime", [](const auto & /*words*/, auto channel) { auto *twitchChannel = dynamic_cast(channel.get()); if (twitchChannel == nullptr) { @@ -257,57 +377,9 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); - this->registerCommand("/ignore", [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage("Usage: /ignore [user]")); - return ""; - } - auto app = getApp(); + this->registerCommand("/block", blockLambda); - auto user = app->accounts->twitch.getCurrent(); - auto target = words.at(1); - - if (user->isAnon()) - { - channel->addMessage( - makeSystemMessage("You must be logged in to ignore someone")); - return ""; - } - - user->ignore(target, - [channel](auto resultCode, const QString &message) { - channel->addMessage(makeSystemMessage(message)); - }); - - return ""; - }); - - this->registerCommand("/unignore", [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage("Usage: /unignore [user]")); - return ""; - } - auto app = getApp(); - - auto user = app->accounts->twitch.getCurrent(); - auto target = words.at(1); - - if (user->isAnon()) - { - channel->addMessage( - makeSystemMessage("You must be logged in to ignore someone")); - return ""; - } - - user->unignore(target, - [channel](auto resultCode, const QString &message) { - channel->addMessage(makeSystemMessage(message)); - }); - - return ""; - }); + this->registerCommand("/unblock", unblockLambda); this->registerCommand("/follow", [](const auto &words, auto channel) { if (words.size() < 2) @@ -321,7 +393,7 @@ void CommandController::initialize(Settings &, Paths &paths) if (currentUser->isAnon()) { channel->addMessage( - makeSystemMessage("You must be logged in to follow someone")); + makeSystemMessage("You must be logged in to follow someone!")); return ""; } @@ -364,8 +436,8 @@ void CommandController::initialize(Settings &, Paths &paths) if (currentUser->isAnon()) { - channel->addMessage( - makeSystemMessage("You must be logged in to follow someone")); + channel->addMessage(makeSystemMessage( + "You must be logged in to unfollow someone!")); return ""; } @@ -393,13 +465,6 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); - this->registerCommand("/logs", [](const auto & /*words*/, auto channel) { - channel->addMessage(makeSystemMessage( - "Online logs functionality has been removed. If you're a " - "moderator, you can use the /user command")); - return ""; - }); - this->registerCommand("/user", [](const auto &words, auto channel) { if (words.size() < 2) { @@ -455,7 +520,7 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); - this->registerCommand("/clip", [](const auto &words, auto channel) { + this->registerCommand("/clip", [](const auto & /*words*/, auto channel) { if (!channel->isTwitchChannel()) { return ""; diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 166b0e34f..430868359 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -7,7 +7,9 @@ #include "common/NetworkRequest.hpp" #include "common/Outcome.hpp" #include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include "providers/twitch/TwitchUser.hpp" #include "providers/twitch/api/Helix.hpp" #include "singletons/Emotes.hpp" #include "util/RapidjsonHelpers.hpp" @@ -89,189 +91,60 @@ bool TwitchAccount::isAnon() const return this->isAnon_; } -void TwitchAccount::loadIgnores() +void TwitchAccount::loadBlocks() { - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + - "/blocks"); + getHelix()->loadBlocks( + getApp()->accounts->twitch.getCurrent()->userId_, + [this](std::vector blocks) { + std::lock_guard lock(this->ignoresMutex_); + this->ignores_.clear(); - NetworkRequest(url) - - .authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken()) - .onSuccess([=](auto result) -> Outcome { - auto document = result.parseRapidJson(); - if (!document.IsObject()) + for (const HelixBlock &block : blocks) { - return Failure; + TwitchUser blockedUser; + blockedUser.fromHelixBlock(block); + this->ignores_.insert(blockedUser); } - - auto blocksIt = document.FindMember("blocks"); - if (blocksIt == document.MemberEnd()) - { - return Failure; - } - const auto &blocks = blocksIt->value; - - if (!blocks.IsArray()) - { - return Failure; - } - - { - std::lock_guard lock(this->ignoresMutex_); - this->ignores_.clear(); - - for (const auto &block : blocks.GetArray()) - { - if (!block.IsObject()) - { - continue; - } - auto userIt = block.FindMember("user"); - if (userIt == block.MemberEnd()) - { - continue; - } - TwitchUser ignoredUser; - if (!rj::getSafe(userIt->value, ignoredUser)) - { - qCWarning(chatterinoTwitch) - << "Error parsing twitch user JSON" - << rj::stringify(userIt->value).c_str(); - continue; - } - - this->ignores_.insert(ignoredUser); - } - } - - return Success; - }) - .execute(); + }, + [] { + qDebug() << "Fetching blocks failed!"; + }); } -void TwitchAccount::ignore( - const QString &targetName, - std::function onFinished) +void TwitchAccount::blockUser(QString userId, std::function onSuccess, + std::function onFailure) { - const auto onUserFetched = [this, targetName, - onFinished](const auto &user) { - this->ignoreByID(user.id, targetName, onFinished); - }; - - const auto onUserFetchFailed = [] {}; - - getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed); -} - -void TwitchAccount::ignoreByID( - const QString &targetUserID, const QString &targetName, - std::function onFinished) -{ - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + - "/blocks/" + targetUserID); - - NetworkRequest(url, NetworkRequestType::Put) - - .authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken()) - .onError([=](NetworkResult result) { - onFinished(IgnoreResult_Failed, - QString("An unknown error occurred while trying to " - "ignore user %1 (%2)") - .arg(targetName) - .arg(result.status())); - }) - .onSuccess([=](auto result) -> Outcome { - auto document = result.parseRapidJson(); - if (!document.IsObject()) - { - onFinished(IgnoreResult_Failed, - "Bad JSON data while ignoring user " + targetName); - return Failure; - } - - auto userIt = document.FindMember("user"); - if (userIt == document.MemberEnd()) - { - onFinished(IgnoreResult_Failed, - "Bad JSON data while ignoring user (missing user) " + - targetName); - return Failure; - } - - TwitchUser ignoredUser; - if (!rj::getSafe(userIt->value, ignoredUser)) - { - onFinished(IgnoreResult_Failed, - "Bad JSON data while ignoring user (invalid user) " + - targetName); - return Failure; - } + getHelix()->blockUser( + userId, + [this, userId, onSuccess] { + TwitchUser blockedUser; + blockedUser.id = userId; { std::lock_guard lock(this->ignoresMutex_); - auto res = this->ignores_.insert(ignoredUser); - if (!res.second) - { - const TwitchUser &existingUser = *(res.first); - existingUser.update(ignoredUser); - onFinished(IgnoreResult_AlreadyIgnored, - "User " + targetName + " is already ignored"); - return Failure; - } + this->ignores_.insert(blockedUser); } - onFinished(IgnoreResult_Success, - "Successfully ignored user " + targetName); - - return Success; - }) - .execute(); + onSuccess(); + }, + onFailure); } -void TwitchAccount::unignore( - const QString &targetName, - std::function onFinished) +void TwitchAccount::unblockUser(QString userId, std::function onSuccess, + std::function onFailure) { - const auto onUserFetched = [this, targetName, - onFinished](const auto &user) { - this->unignoreByID(user.id, targetName, onFinished); - }; - - const auto onUserFetchFailed = [] {}; - - getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed); -} - -void TwitchAccount::unignoreByID( - const QString &targetUserID, const QString &targetName, - std::function onFinished) -{ - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + - "/blocks/" + targetUserID); - - NetworkRequest(url, NetworkRequestType::Delete) - - .authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken()) - .onError([=](NetworkResult result) { - onFinished( - UnignoreResult_Failed, - "An unknown error occurred while trying to unignore user " + - targetName + " (" + QString::number(result.status()) + ")"); - }) - .onSuccess([=](auto result) -> Outcome { - auto document = result.parseRapidJson(); + getHelix()->unblockUser( + userId, + [this, userId, onSuccess] { TwitchUser ignoredUser; - ignoredUser.id = targetUserID; + ignoredUser.id = userId; { std::lock_guard lock(this->ignoresMutex_); this->ignores_.erase(ignoredUser); } - onFinished(UnignoreResult_Success, - "Successfully unignored user " + targetName); - - return Success; - }) - .execute(); + onSuccess(); + }, + onFailure); } void TwitchAccount::checkFollow(const QString targetUserID, @@ -291,7 +164,7 @@ void TwitchAccount::checkFollow(const QString targetUserID, [] {}); } -std::set TwitchAccount::getIgnores() const +std::set TwitchAccount::getBlocks() const { std::lock_guard lock(this->ignoresMutex_); diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index d4e2bfd33..d1101fe46 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -17,17 +17,6 @@ namespace chatterino { -enum IgnoreResult { - IgnoreResult_Success, - IgnoreResult_AlreadyIgnored, - IgnoreResult_Failed, -}; - -enum UnignoreResult { - UnignoreResult_Success, - UnignoreResult_Failed, -}; - enum FollowResult { FollowResult_Following, FollowResult_NotFollowing, @@ -83,23 +72,16 @@ public: bool isAnon() const; - void loadIgnores(); - void ignore(const QString &targetName, - std::function onFinished); - void ignoreByID( - const QString &targetUserID, const QString &targetName, - std::function onFinished); - void unignore( - const QString &targetName, - std::function onFinished); - void unignoreByID( - const QString &targetUserID, const QString &targetName, - std::function onFinished); + void loadBlocks(); + void blockUser(QString userId, std::function onSuccess, + std::function onFailure); + void unblockUser(QString userId, std::function onSuccess, + std::function onFailure); void checkFollow(const QString targetUserID, std::function onFinished); - std::set getIgnores() const; + std::set getBlocks() const; void loadEmotes(); AccessGuard accessEmotes() const; diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index 4766553b2..157875338 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -15,7 +15,7 @@ TwitchAccountManager::TwitchAccountManager() { this->currentUserChanged.connect([this] { auto currentUser = this->getCurrent(); - currentUser->loadIgnores(); + currentUser->loadBlocks(); }); this->accounts.itemRemoved.connect([this](const auto &acc) { diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index a3ed3d9ac..522c7f463 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -138,18 +138,17 @@ bool TwitchMessageBuilder::isIgnored() const auto app = getApp(); - if (getSettings()->enableTwitchIgnoredUsers && + if (getSettings()->enableTwitchBlockedUsers && this->tags.contains("user-id")) { auto sourceUserID = this->tags.value("user-id").toString(); - for (const auto &user : - app->accounts->twitch.getCurrent()->getIgnores()) + for (const auto &user : app->accounts->twitch.getCurrent()->getBlocks()) { if (sourceUserID == user.id) { switch (static_cast( - getSettings()->showIgnoredUsersMessages.getValue())) + getSettings()->showBlockedUsersMessages.getValue())) { case ShowIgnoredUsersMessages::IfModerator: if (this->channel->isMod() || diff --git a/src/providers/twitch/TwitchUser.hpp b/src/providers/twitch/TwitchUser.hpp index 962ce8d38..53dde8d7a 100644 --- a/src/providers/twitch/TwitchUser.hpp +++ b/src/providers/twitch/TwitchUser.hpp @@ -1,5 +1,6 @@ #pragma once +#include "providers/twitch/api/Helix.hpp" #include "util/RapidjsonHelpers.hpp" #include @@ -23,6 +24,13 @@ struct TwitchUser { this->displayName = other.displayName; } + void fromHelixBlock(const HelixBlock &ignore) + { + this->id = ignore.userId; + this->name = ignore.userName; + this->displayName = ignore.displayName; + } + bool operator<(const TwitchUser &rhs) const { return this->id < rhs.id; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 827de4a09..3b6564eae 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -46,7 +46,7 @@ void Helix::fetchUsers(QStringList userIds, QStringList userLogins, return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](auto /*result*/) { // TODO: make better xd failureCallback(); }) @@ -125,7 +125,7 @@ void Helix::fetchUsersFollows( successCallback(HelixUsersFollowsResponse(root)); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](auto /*result*/) { // TODO: make better xd failureCallback(); }) @@ -198,7 +198,7 @@ void Helix::fetchStreams( return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](auto /*result*/) { // TODO: make better xd failureCallback(); }) @@ -288,7 +288,7 @@ void Helix::fetchGames(QStringList gameIds, QStringList gameNames, return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](auto /*result*/) { // TODO: make better xd failureCallback(); }) @@ -326,11 +326,11 @@ void Helix::followUser(QString userId, QString targetId, this->makeRequest("users/follows", urlQuery) .type(NetworkRequestType::Post) - .onSuccess([successCallback](auto result) -> Outcome { + .onSuccess([successCallback](auto /*result*/) -> Outcome { successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](auto /*result*/) { // TODO: make better xd failureCallback(); }) @@ -348,11 +348,11 @@ void Helix::unfollowUser(QString userId, QString targetId, this->makeRequest("users/follows", urlQuery) .type(NetworkRequestType::Delete) - .onSuccess([successCallback](auto result) -> Outcome { + .onSuccess([successCallback](auto /*result*/) -> Outcome { successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](auto /*result*/) { // TODO: make better xd failureCallback(); }) @@ -385,7 +385,7 @@ void Helix::createClip(QString channelId, successCallback(clip); return Success; }) - .onError([failureCallback](NetworkResult result) { + .onError([failureCallback](auto result) { switch (result.status()) { case 503: { @@ -502,6 +502,82 @@ void Helix::createStreamMarker( .execute(); }; +void Helix::loadBlocks(QString userId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + urlQuery.addQueryItem("broadcaster_id", userId); + + this->makeRequest("users/blocks", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return Failure; + } + + std::vector ignores; + + for (const auto &jsonStream : data.toArray()) + { + ignores.emplace_back(jsonStream.toObject()); + } + + successCallback(ignores); + + return Success; + }) + .onError([failureCallback](auto /*result*/) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +void Helix::blockUser(QString targetUserId, + std::function successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + urlQuery.addQueryItem("target_user_id", targetUserId); + + this->makeRequest("users/blocks", urlQuery) + .type(NetworkRequestType::Put) + .onSuccess([successCallback](auto /*result*/) -> Outcome { + successCallback(); + return Success; + }) + .onError([failureCallback](auto /*result*/) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +void Helix::unblockUser(QString targetUserId, + std::function successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + urlQuery.addQueryItem("target_user_id", targetUserId); + + this->makeRequest("users/blocks", urlQuery) + .type(NetworkRequestType::Delete) + .onSuccess([successCallback](auto /*result*/) -> Outcome { + successCallback(); + return Success; + }) + .onError([failureCallback](auto /*result*/) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) { assert(!url.startsWith("/")); diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 4249c5a3e..eafa04ab9 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -180,6 +180,19 @@ struct HelixStreamMarker { } }; +struct HelixBlock { + QString userId; + QString userName; + QString displayName; + + explicit HelixBlock(QJsonObject jsonObject) + : userId(jsonObject.value("user_id").toString()) + , userName(jsonObject.value("user_login").toString()) + , displayName(jsonObject.value("display_name").toString()) + { + } +}; + enum class HelixClipError { Unknown, ClipsDisabled, @@ -269,6 +282,20 @@ public: ResultCallback successCallback, std::function failureCallback); + // https://dev.twitch.tv/docs/api/reference#get-user-block-list + void loadBlocks(QString userId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#block-user + void blockUser(QString targetUserId, std::function successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#unblock-user + void unblockUser(QString targetUserId, + std::function successCallback, + HelixFailureCallback failureCallback); + void update(QString clientId, QString oauthToken); static void initialize(); diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index bf3eac98e..036bf38d3 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -11,29 +11,6 @@ Migration path: **Not checked** * We implement this API in `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000` -### Get User Block List -URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-block-list - -Migration path: **Unknown** - - * We use this in `providers/twitch/TwitchAccount.cpp loadIgnores` - -### Block User -URL: https://dev.twitch.tv/docs/v5/reference/users#block-user -Requires `user_blocks_edit` scope - -Migration path: **Unknown** - - * We use this in `providers/twitch/TwitchAccount.cpp ignoreByID` - -### Unblock User -URL: https://dev.twitch.tv/docs/v5/reference/users#unblock-user -Requires `user_blocks_edit` scope - -Migration path: **Unknown** - - * We use this in `providers/twitch/TwitchAccount.cpp unignoreByID` - ### Get User Emotes URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-emotes Requires `user_subscriptions` scope @@ -63,7 +40,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-users * `UserInfoPopup` to get ID, viewCount, displayName, createdAt of username we clicked * `CommandController` to power any commands that need to get a user ID * `Toasts` to get the profile picture of a streamer who just went live - * `TwitchAccount` ignore and unignore features to translate user name to user ID + * `TwitchAccount` block and unblock features to translate user name to user ID ### Get Users Follows URL: https://dev.twitch.tv/docs/api/reference#get-users-follows @@ -121,6 +98,32 @@ Requires `user:edit:broadcast` scope Used in: * `controllers/commands/CommandController.cpp` in /marker command +### Get User Block List +URL: https://dev.twitch.tv/docs/api/reference#get-user-block-list +Requires `user:read:blocked_users` scope + + * We implement this in `providers/twitch/api/Helix.cpp loadBlocks` + Used in: + * `providers/twitch/TwitchAccount.cpp loadBlocks` to load list of blocked (blocked) users by current user + +### Block User +URL: https://dev.twitch.tv/docs/api/reference#block-user +Requires `user:manage:blocked_users` scope + + * We implement this in `providers/twitch/api/Helix.cpp blockUser` + Used in: + * `widgets/dialogs/UserInfoPopup.cpp` to block a user via checkbox in the usercard + * `controllers/commands/CommandController.cpp` to block a user via "/block" command + +### Unblock User +URL: https://dev.twitch.tv/docs/api/reference#unblock-user +Requires `user:manage:blocked_users` scope + + * We implement this in `providers/twitch/api/Helix.cpp unblockUser` + Used in: + * `widgets/dialogs/UserInfoPopup.cpp` to unblock a user via checkbox in the usercard + * `controllers/commands/CommandController.cpp` to unblock a user via "/unblock" command + ## TMI The TMI api is undocumented. diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 493bfbdcf..c960af695 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -200,10 +200,10 @@ public: QStringSetting ignoredPhraseReplace = {"/ignore/ignoredPhraseReplace", "***"}; - /// Ingored Users - BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers", + /// Blocked Users + BoolSetting enableTwitchBlockedUsers = {"/ignore/enableTwitchBlockedUsers", true}; - IntSetting showIgnoredUsersMessages = {"/ignore/showIgnoredUsers", 0}; + IntSetting showBlockedUsersMessages = {"/ignore/showBlockedUsers", 0}; /// Moderation QStringSetting timeoutAction = {"/moderation/timeoutAction", "Disable"}; diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index f8785b313..ea2756daa 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -7,6 +7,7 @@ #include "controllers/highlights/HighlightBlacklistUser.hpp" #include "messages/Message.hpp" #include "providers/IvrApi.hpp" +#include "providers/irc/IrcMessageBuilder.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Kraken.hpp" @@ -188,7 +189,7 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent) user->addStretch(1); user.emplace("Follow").assign(&this->ui_.follow); - user.emplace("Ignore").assign(&this->ui_.ignore); + user.emplace("Block").assign(&this->ui_.block); user.emplace("Ignore highlights") .assign(&this->ui_.ignoreHighlights); auto usercard = user.emplace(this); @@ -414,50 +415,71 @@ void UserInfoPopup::installEvents() std::shared_ptr ignoreNext = std::make_shared(false); - // ignore + // block QObject::connect( - this->ui_.ignore, &QCheckBox::stateChanged, - [this, ignoreNext, hack](int) mutable { - if (*ignoreNext) + this->ui_.block, &QCheckBox::stateChanged, + [this](int newState) mutable { + auto currentUser = getApp()->accounts->twitch.getCurrent(); + + const auto reenableBlockCheckbox = [this] { + this->ui_.block->setEnabled(true); + }; + + if (!this->ui_.block->isEnabled()) { - *ignoreNext = false; + reenableBlockCheckbox(); return; } - this->ui_.ignore->setEnabled(false); + switch (newState) + { + case Qt::CheckState::Unchecked: { + this->ui_.block->setEnabled(false); - auto currentUser = getApp()->accounts->twitch.getCurrent(); - if (this->ui_.ignore->isChecked()) - { - currentUser->ignoreByID( - this->userId_, this->userName_, - [=](auto result, const auto &message) mutable { - if (hack.lock()) - { - if (result == IgnoreResult_Failed) - { - *ignoreNext = true; - this->ui_.ignore->setChecked(false); - } - this->ui_.ignore->setEnabled(true); - } - }); - } - else - { - currentUser->unignoreByID( - this->userId_, this->userName_, - [=](auto result, const auto &message) mutable { - if (hack.lock()) - { - if (result == UnignoreResult_Failed) - { - *ignoreNext = true; - this->ui_.ignore->setChecked(true); - } - this->ui_.ignore->setEnabled(true); - } - }); + getApp()->accounts->twitch.getCurrent()->unblockUser( + this->userId_, + [this, reenableBlockCheckbox, currentUser] { + this->channel_->addMessage(makeSystemMessage( + QString("You successfully unblocked user %1") + .arg(this->userName_))); + reenableBlockCheckbox(); + }, + [this, reenableBlockCheckbox] { + this->channel_->addMessage( + makeSystemMessage(QString( + "User %1 couldn't be unblocked, an unknown " + "error occurred!"))); + reenableBlockCheckbox(); + }); + } + break; + + case Qt::CheckState::PartiallyChecked: { + // We deliberately ignore this state + } + break; + + case Qt::CheckState::Checked: { + this->ui_.block->setEnabled(false); + + getApp()->accounts->twitch.getCurrent()->blockUser( + this->userId_, + [this, reenableBlockCheckbox, currentUser] { + this->channel_->addMessage(makeSystemMessage( + QString("You successfully blocked user %1") + .arg(this->userName_))); + reenableBlockCheckbox(); + }, + [this, reenableBlockCheckbox] { + this->channel_->addMessage(makeSystemMessage( + QString( + "User %1 couldn't be blocked, an unknown " + "error occurred!") + .arg(this->userName_))); + reenableBlockCheckbox(); + }); + } + break; } }); @@ -634,9 +656,9 @@ void UserInfoPopup::updateUserData() // get ignore state bool isIgnoring = false; - for (const auto &ignoredUser : currentUser->getIgnores()) + for (const auto &blockedUser : currentUser->getBlocks()) { - if (user.id == ignoredUser.id) + if (user.id == blockedUser.id) { isIgnoring = true; break; @@ -663,8 +685,8 @@ void UserInfoPopup::updateUserData() { this->ui_.ignoreHighlights->setEnabled(true); } - this->ui_.ignore->setEnabled(true); - this->ui_.ignore->setChecked(isIgnoring); + this->ui_.block->setChecked(isIgnoring); + this->ui_.block->setEnabled(true); this->ui_.ignoreHighlights->setChecked(isIgnoringHighlights); // get followage and subage @@ -711,7 +733,7 @@ void UserInfoPopup::updateUserData() onUserFetchFailed); this->ui_.follow->setEnabled(false); - this->ui_.ignore->setEnabled(false); + this->ui_.block->setEnabled(false); this->ui_.ignoreHighlights->setEnabled(false); } diff --git a/src/widgets/dialogs/UserInfoPopup.hpp b/src/widgets/dialogs/UserInfoPopup.hpp index 2f5c2b82d..bb189b6a4 100644 --- a/src/widgets/dialogs/UserInfoPopup.hpp +++ b/src/widgets/dialogs/UserInfoPopup.hpp @@ -59,7 +59,7 @@ private: Label *subageLabel = nullptr; QCheckBox *follow = nullptr; - QCheckBox *ignore = nullptr; + QCheckBox *block = nullptr; QCheckBox *ignoreHighlights = nullptr; Label *noMessagesLabel = nullptr; diff --git a/src/widgets/settingspages/IgnoresPage.cpp b/src/widgets/settingspages/IgnoresPage.cpp index d35884980..b57d839e5 100644 --- a/src/widgets/settingspages/IgnoresPage.cpp +++ b/src/widgets/settingspages/IgnoresPage.cpp @@ -19,7 +19,7 @@ #include // clang-format off -#define INFO "/ignore in chat ignores a user.\n/unignore in chat unignores a user.\nYou can also click on a user to open the usercard." +#define INFO "/block in chat blocks a user.\n/block in chat unblocks a user.\nYou can also click on a user to open the usercard." // clang-format on namespace chatterino { @@ -73,18 +73,18 @@ void addUsersTab(IgnoresPage &page, LayoutCreator users, { auto label = users.emplace(INFO); label->setWordWrap(true); - users.append(page.createCheckBox("Enable twitch ignored users", - getSettings()->enableTwitchIgnoredUsers)); + users.append(page.createCheckBox("Enable twitch blocked users", + getSettings()->enableTwitchBlockedUsers)); auto anyways = users.emplace().withoutMargin(); { - anyways.emplace("Show messages from ignored users anyways:"); + anyways.emplace("Show messages from blocked users:"); auto combo = anyways.emplace().getElement(); combo->addItems( {"Never", "If you are Moderator", "If you are Broadcaster"}); - auto &setting = getSettings()->showIgnoredUsersMessages; + auto &setting = getSettings()->showBlockedUsersMessages; setting.connect([combo](const int value) { combo->setCurrentIndex(value); @@ -102,12 +102,12 @@ void addUsersTab(IgnoresPage &page, LayoutCreator users, /*auto addremove = users.emplace().withoutMargin(); { - auto add = addremove.emplace("Ignore user"); - auto remove = addremove.emplace("Unignore User"); + auto add = addremove.emplace("Block user"); + auto remove = addremove.emplace("Unblock User"); addremove->addStretch(1); }*/ - users.emplace("List of ignored users:"); + users.emplace("List of blocked users:"); users.emplace()->setModel(&userModel); } @@ -123,9 +123,9 @@ void IgnoresPage::onShow() } QStringList users; - for (const auto &ignoredUser : user->getIgnores()) + for (const auto &blockedUser : user->getBlocks()) { - users << ignoredUser.name; + users << blockedUser.name; } users.sort(Qt::CaseInsensitive); this->userListModel_.setStringList(users);