From 6e7b4d8ec76d2aac684fea00b715b254f0afb22c Mon Sep 17 00:00:00 2001 From: Aiden Date: Sun, 18 Sep 2022 12:19:22 +0100 Subject: [PATCH] Migrate /clear command to Helix API (#3994) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + .../commands/CommandController.cpp | 86 +++++++++++++++++++ src/providers/twitch/api/Helix.cpp | 79 +++++++++++++++++ src/providers/twitch/api/Helix.hpp | 25 ++++++ tests/src/HighlightController.cpp | 8 ++ 5 files changed, 199 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e094ffa6c..1d87404dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Minor: Added `Go to message` context menu action to search popup, mentions, usercard and reply threads. (#3953) - Minor: Added link back to original message that was deleted. (#3953) - Minor: Migrate /color command to Helix API. (#3988) +- Minor: Migrate /clear command to Helix API. (#3994) - Bugfix: Fix crash that can occur when closing and quickly reopening a split, then running a command. (#3852) - Bugfix: Connection to Twitch PubSub now recovers more reliably. (#3643, #3716) - Bugfix: Fix crash that can occur when changing channels. (#3799) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index d610cd752..d04e4c05e 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -1269,6 +1269,92 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); + + auto deleteMessages = [](auto channel, const QString &messageID) { + const auto *commandName = messageID.isEmpty() ? "/clear" : "/delete"; + auto *twitchChannel = dynamic_cast(channel.get()); + if (twitchChannel == nullptr) + { + channel->addMessage(makeSystemMessage( + QString("The %1 command only works in Twitch channels") + .arg(commandName))); + return ""; + } + + auto user = getApp()->accounts->twitch.getCurrent(); + + // Avoid Helix calls without Client ID and/or OAuth Token + if (user->isAnon()) + { + channel->addMessage(makeSystemMessage( + QString("You must be logged in to use the %1 command.") + .arg(commandName))); + return ""; + } + + getHelix()->deleteChatMessages( + twitchChannel->roomId(), user->getUserId(), messageID, + []() { + // Success handling, we do nothing: IRC/pubsub-edge will dispatch the correct + // events to update state for us. + }, + [channel, messageID](auto error, auto message) { + QString errorMessage = + QString("Failed to delete chat messages - "); + + switch (error) + { + case HelixDeleteChatMessagesError::UserMissingScope: { + errorMessage += + "Missing required scope. Re-login with your " + "account and try again."; + } + break; + + case HelixDeleteChatMessagesError::UserNotAuthorized: { + errorMessage += + "you don't have permission to perform that action."; + } + break; + + case HelixDeleteChatMessagesError::MessageUnavailable: { + // Override default message prefix to match with IRC message format + errorMessage = + QString( + "The message %1 does not exist, was deleted, " + "or is too old to be deleted.") + .arg(messageID); + } + break; + + case HelixDeleteChatMessagesError::UserNotAuthenticated: { + errorMessage += "you need to re-authenticate."; + } + break; + + case HelixDeleteChatMessagesError::Forwarded: { + errorMessage += message + "."; + } + break; + + case HelixDeleteChatMessagesError::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + channel->addMessage(makeSystemMessage(errorMessage)); + }); + + return ""; + }; + + this->registerCommand( + "/clear", [deleteMessages](const QStringList &words, auto channel) { + (void)words; // unused + return deleteMessages(channel, QString()); + }); } void CommandController::save() diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 27f2ebcf0..8726424e8 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -843,6 +843,85 @@ void Helix::updateUserChatColor( .execute(); }; +void Helix::deleteChatMessages( + QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixDeleteChatMessagesError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + if (!messageID.isEmpty()) + { + // If message ID is empty, it's equivalent to /clear + urlQuery.addQueryItem("message_id", messageID); + } + + this->makeRequest("moderation/chat", urlQuery) + .type(NetworkRequestType::Delete) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for deleting chat messages was" + << result.status() << "but we only expected it to be 204"; + } + + successCallback(); + return Success; + }) + .onError([failureCallback](auto result) { + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (result.status()) + { + case 404: { + // A 404 on this endpoint means message id is invalid or unable to be deleted. + // See: https://dev.twitch.tv/docs/api/reference#delete-chat-messages + failureCallback(Error::MessageUnavailable, message); + } + break; + + case 403: { + // 403 endpoint means the user does not have permission to perform this action in that channel + // Most likely to missing moderator permissions + // Missing documentation issue: https://github.com/twitchdev/issues/issues/659 + // `message` value is well-formed so no need for a specific error type + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error deleting chat messages:" + << result.status() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + 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 5531ba22e..742e86d18 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -329,6 +329,17 @@ enum class HelixUpdateUserChatColorError { Forwarded, }; +enum class HelixDeleteChatMessagesError { + Unknown, + UserMissingScope, + UserNotAuthenticated, + UserNotAuthorized, + MessageUnavailable, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + class IHelix { public: @@ -458,6 +469,13 @@ public: FailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference#delete-chat-messages + virtual void deleteChatMessages( + QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + FailureCallback + failureCallback) = 0; + virtual void update(QString clientId, QString oauthToken) = 0; }; @@ -580,6 +598,13 @@ public: FailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference#delete-chat-messages + void deleteChatMessages( + QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + FailureCallback failureCallback) + final; + void update(QString clientId, QString oauthToken) final; static void initialize(); diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index 959ecc32f..bb824c41e 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -217,6 +217,14 @@ public: failureCallback)), (override)); + // The extra parenthesis around the failure callback is because its type contains a comma + MOCK_METHOD(void, deleteChatMessages, + (QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + MOCK_METHOD(void, update, (QString clientId, QString oauthToken), (override)); };