From e746201c4f08878d32cc2952d51f1ba3d4912ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 14 May 2021 13:14:43 +0200 Subject: [PATCH] Refactored and Migrated to Helix AutoMod message management (#2779) This uses new Helix endpoint which requires new scopes and users need to reauthenticate to approve/deny AutoMod messages again. --- CHANGELOG.md | 2 + src/providers/twitch/TwitchAccount.cpp | 110 +++++++++++++++++++------ src/providers/twitch/TwitchAccount.hpp | 5 +- src/providers/twitch/api/Helix.cpp | 62 ++++++++++++++ src/providers/twitch/api/Helix.hpp | 14 ++++ src/providers/twitch/api/README.md | 23 +++--- src/widgets/helper/ChannelView.cpp | 6 +- 7 files changed, 179 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d4cc1ad6..1c3450072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Bugfix: `Login expired` message no longer highlights all tabs. (#2735) - Bugfix: Fix a deadlock that would occur during user badge loading. (#1704, #2756) - Bugfix: Tabbing in `Select a channel to open` is now consistent. (#1797) +- Bugfix: Approving/denying AutoMod messages works again. (#2779) +- Dev: Migrated AutoMod approve/deny endpoints to Helix. (#2779) ## 2.3.1 diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 909eafcd8..675155681 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -3,12 +3,16 @@ #include #include "Application.hpp" +#include "common/Channel.hpp" #include "common/Env.hpp" #include "common/NetworkRequest.hpp" #include "common/Outcome.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" #include "providers/IvrApi.hpp" +#include "providers/irc/IrcMessageBuilder.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchUser.hpp" #include "providers/twitch/api/Helix.hpp" @@ -354,42 +358,96 @@ SharedAccessGuard } // AutoModActions -void TwitchAccount::autoModAllow(const QString msgID) +void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel) { - QString url("https://api.twitch.tv/kraken/chat/twitchbot/approve"); + getHelix()->manageAutoModMessages( + this->getUserId(), msgID, "ALLOW", + [] { + // success + }, + [channel](auto error) { + // failure + QString errorMessage("Failed to allow AutoMod message - "); - auto qba = (QString("{\"msg_id\":\"") + msgID + "\"}").toUtf8(); + switch (error) + { + case HelixAutoModMessageError::MessageAlreadyProcessed: { + errorMessage += "message has already been processed."; + } + break; - NetworkRequest(url, NetworkRequestType::Post) - .header("Content-Type", "application/json") - .header("Content-Length", QByteArray::number(qba.size())) - .payload(qba) + case HelixAutoModMessageError::UserNotAuthenticated: { + errorMessage += "you need to re-authenticate."; + } + break; - .authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken()) - .onError([=](NetworkResult result) { - qCWarning(chatterinoTwitch) - << "[TwitchAccounts::autoModAllow] Error" << result.status(); - }) - .execute(); + case HelixAutoModMessageError::UserNotAuthorized: { + errorMessage += + "you don't have permission to perform that action"; + } + break; + + case HelixAutoModMessageError::MessageNotFound: { + errorMessage += "target message not found."; + } + break; + + // This would most likely happen if the service is down, or if the JSON payload returned has changed format + case HelixAutoModMessageError::Unknown: + default: { + errorMessage += "an unknown error occured."; + } + break; + } + + channel->addMessage(makeSystemMessage(errorMessage)); + }); } -void TwitchAccount::autoModDeny(const QString msgID) +void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel) { - QString url("https://api.twitch.tv/kraken/chat/twitchbot/deny"); + getHelix()->manageAutoModMessages( + this->getUserId(), msgID, "DENY", + [] { + // success + }, + [channel](auto error) { + // failure + QString errorMessage("Failed to deny AutoMod message - "); - auto qba = (QString("{\"msg_id\":\"") + msgID + "\"}").toUtf8(); + switch (error) + { + case HelixAutoModMessageError::MessageAlreadyProcessed: { + errorMessage += "message has already been processed."; + } + break; - NetworkRequest(url, NetworkRequestType::Post) - .header("Content-Type", "application/json") - .header("Content-Length", QByteArray::number(qba.size())) - .payload(qba) + case HelixAutoModMessageError::UserNotAuthenticated: { + errorMessage += "you need to re-authenticate."; + } + break; - .authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken()) - .onError([=](NetworkResult result) { - qCWarning(chatterinoTwitch) - << "[TwitchAccounts::autoModDeny] Error" << result.status(); - }) - .execute(); + case HelixAutoModMessageError::UserNotAuthorized: { + errorMessage += + "you don't have permission to perform that action"; + } + break; + + case HelixAutoModMessageError::MessageNotFound: { + errorMessage += "target message not found."; + } + break; + + // This would most likely happen if the service is down, or if the JSON payload returned has changed format + case HelixAutoModMessageError::Unknown: + default: { + errorMessage += "an unknown error occured."; + } + break; + } + + channel->addMessage(makeSystemMessage(errorMessage)); + }); } void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index 2ae80d0ee..d45c09881 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -2,6 +2,7 @@ #include "common/Aliases.hpp" #include "common/Atomic.hpp" +#include "common/Channel.hpp" #include "common/UniqueAccess.hpp" #include "controllers/accounts/Account.hpp" #include "messages/Emote.hpp" @@ -114,8 +115,8 @@ public: SharedAccessGuard accessEmotes() const; // Automod actions - void autoModAllow(const QString msgID); - void autoModDeny(const QString msgID); + void autoModAllow(const QString msgID, ChannelPtr channel); + void autoModDeny(const QString msgID, ChannelPtr channel); private: void loadEmoteSetData(std::shared_ptr emoteSet); diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 34f5a409a..8d6ab1a55 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -659,6 +659,68 @@ void Helix::updateChannel(QString broadcasterId, QString gameId, }) .execute(); } + +void Helix::manageAutoModMessages( + QString userID, QString msgID, QString action, + std::function successCallback, + std::function failureCallback) +{ + QJsonObject payload; + + payload.insert("user_id", userID); + payload.insert("msg_id", msgID); + payload.insert("action", action); + + this->makeRequest("moderation/automod/message", QUrlQuery()) + .type(NetworkRequestType::Post) + .header("Content-Type", "application/json") + .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + successCallback(); + return Success; + }) + .onError([failureCallback, msgID, action](NetworkResult result) { + switch (result.status()) + { + case 400: { + // Message was already processed + failureCallback( + HelixAutoModMessageError::MessageAlreadyProcessed); + } + break; + + case 401: { + // User is missing the required scope + failureCallback( + HelixAutoModMessageError::UserNotAuthenticated); + } + break; + + case 403: { + // Requesting user is not authorized to manage messages + failureCallback( + HelixAutoModMessageError::UserNotAuthorized); + } + break; + + case 404: { + // Message not found or invalid msgID + failureCallback(HelixAutoModMessageError::MessageNotFound); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Failed to manage automod message: " << action + << msgID << result.status() << result.getData(); + failureCallback(HelixAutoModMessageError::Unknown); + } + break; + } + }) + .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 ea5c7beed..b9b53e16d 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -205,6 +205,14 @@ enum class HelixStreamMarkerError { UserNotAuthenticated, }; +enum class HelixAutoModMessageError { + Unknown, + MessageAlreadyProcessed, + UserNotAuthenticated, + UserNotAuthorized, + MessageNotFound, +}; + class Helix final : boost::noncopyable { public: @@ -307,6 +315,12 @@ public: std::function successCallback, HelixFailureCallback failureCallback); + // https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages + void manageAutoModMessages( + QString userID, QString msgID, QString action, + std::function successCallback, + std::function 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 779758fe3..c279b39de 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -4,7 +4,7 @@ this folder describes what sort of API requests we do, what permissions are requ ## Kraken (V5) -We use a bunch of Kraken (V5) in Chatterino2. +We use few Kraken endpoints in Chatterino2. ### Get Cheermotes @@ -23,18 +23,6 @@ Migration path: **Unknown** - We use this in `providers/twitch/TwitchAccount.cpp loadEmotes` to figure out which emotes a user is allowed to use! -### AUTOMOD APPROVE - -**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2 - -- We use this in `providers/twitch/TwitchAccount.cpp autoModAllow` to approve an automod deny/allow question - -### AUTOMOD DENY - -**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2 - -- We use this in `providers/twitch/TwitchAccount.cpp autoModDeny` to deny an automod deny/allow question - ## Helix Full Helix API reference: https://dev.twitch.tv/docs/api/reference @@ -160,6 +148,15 @@ URL: https://dev.twitch.tv/docs/api/reference#search-categories Used in: - `controllers/commands/CommandController.cpp` in `/setgame` command to fuzzy search for game titles +### Manage Held AutoMod Messages + +URL: https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages +Requires `moderator:manage:automod` scope + +- We implement this in `providers/twitch/api/Helix.cpp manageAutoModMessages` + Used in: + - `providers/twitch/TwitchAccount.cpp` to approve/deny held AutoMod messages + ## TMI The TMI api is undocumented. diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 64477525c..14e864e87 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -2068,12 +2068,14 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link, break; case Link::AutoModAllow: { - getApp()->accounts->twitch.getCurrent()->autoModAllow(link.value); + getApp()->accounts->twitch.getCurrent()->autoModAllow( + link.value, this->channel()); } break; case Link::AutoModDeny: { - getApp()->accounts->twitch.getCurrent()->autoModDeny(link.value); + getApp()->accounts->twitch.getCurrent()->autoModDeny( + link.value, this->channel()); } break;