From 29272e130a6e83690a770c71151c54755292b378 Mon Sep 17 00:00:00 2001 From: Marko Date: Sat, 8 Oct 2022 14:10:38 +0200 Subject: [PATCH] Migrate `/unraid` to Helix. (#4030) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + .../commands/CommandController.cpp | 106 ++++++++++++++++++ src/providers/twitch/api/Helix.cpp | 73 ++++++++++++ src/providers/twitch/api/Helix.hpp | 23 ++++ tests/src/HighlightController.cpp | 7 ++ 5 files changed, 210 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eab2696f2..8fb9ed70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ - Minor: Migrated /followers to Helix API. (#4040) - Minor: Migrated /followersoff to Helix API. (#4040) - Minor: Migrated /raid command to Helix API. Chat command will continue to be used until February 11th 2023. (#4029) +- Minor: Migrated /unraid command to Helix API. Chat command will continue to be used until February 11th 2023. (#4030) - Minor: Migrated /ban to Helix API. (#4049) - Minor: Migrated /timeout to Helix API. (#4049) - Minor: Migrated /w to Helix API. Chat command will continue to be used until February 11th 2023. (#4052) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 40de938fa..b76806468 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -2231,6 +2231,112 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); // /raid + this->registerCommand( // /unraid + "/unraid", [](const QStringList &words, auto channel) -> QString { + switch (getSettings()->helixTimegateRaid.getValue()) + { + case HelixTimegateOverride::Timegate: { + if (areIRCCommandsStillAvailable()) + { + return useIRCCommand(words); + } + + // fall through to Helix logic + } + break; + + case HelixTimegateOverride::AlwaysUseIRC: { + return useIRCCommand(words); + } + break; + + case HelixTimegateOverride::AlwaysUseHelix: { + // do nothing and fall through to Helix logic + } + break; + } + + if (words.size() != 1) + { + channel->addMessage(makeSystemMessage( + "Usage: \"/unraid\" - Cancel the current raid. " + "Only the broadcaster can cancel the raid.")); + return ""; + } + + auto currentUser = getApp()->accounts->twitch.getCurrent(); + if (currentUser->isAnon()) + { + channel->addMessage(makeSystemMessage( + "You must be logged in to cancel the raid!")); + return ""; + } + + auto *twitchChannel = dynamic_cast(channel.get()); + if (twitchChannel == nullptr) + { + channel->addMessage(makeSystemMessage( + "The /unraid command only works in Twitch channels")); + return ""; + } + + getHelix()->cancelRaid( + twitchChannel->roomId(), + [channel] { + channel->addMessage( + makeSystemMessage(QString("You cancelled the raid."))); + }, + [channel](auto error, auto message) { + QString errorMessage = + QString("Failed to cancel the raid - "); + + using Error = HelixCancelRaidError; + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + errorMessage += "You must be the broadcaster " + "to cancel the raid."; + } + break; + + case Error::NoRaidPending: { + errorMessage += "You don't have an active raid."; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + channel->addMessage(makeSystemMessage(errorMessage)); + }); + + return ""; + }); // unraid + const auto formatChatSettingsError = [](const HelixUpdateChatSettingsError error, const QString &message, diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 087febdfa..7684b2720 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -1535,6 +1535,79 @@ void Helix::startRaid( .execute(); } +void Helix::cancelRaid( + QString broadcasterID, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixCancelRaidError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + + this->makeRequest("raids", urlQuery) + .type(NetworkRequestType::Delete) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for canceling the raid 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 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare( + "The ID in broadcaster_id must match the user " + "ID " + "found in the request's OAuth token.", + Qt::CaseInsensitive) == 0) + { + // Must be the broadcaster. + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 404: { + failureCallback(Error::NoRaidPending, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error while canceling the raid:" + << result.status() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} // cancelRaid + void Helix::updateEmoteMode( QString broadcasterID, QString moderatorID, bool emoteMode, ResultCallback successCallback, diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 89a7ba689..00f7037f8 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -454,6 +454,17 @@ enum class HelixStartRaidError { // /raid Forwarded, }; // /raid +enum class HelixCancelRaidError { // /unraid + Unknown, + UserMissingScope, + UserNotAuthorized, + NoRaidPending, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /unraid + enum class HelixUpdateChatSettingsError { // update chat settings Unknown, UserMissingScope, @@ -673,6 +684,12 @@ public: FailureCallback failureCallback) = 0; // https://dev.twitch.tv/docs/api/reference#start-a-raid + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + virtual void cancelRaid( + QString broadcasterID, ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + // Updates the emote mode using // https://dev.twitch.tv/docs/api/reference#update-chat-settings virtual void updateEmoteMode( @@ -923,6 +940,12 @@ public: FailureCallback failureCallback) final; // https://dev.twitch.tv/docs/api/reference#start-a-raid + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + void cancelRaid( + QString broadcasterID, ResultCallback<> successCallback, + FailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + // Updates the emote mode using // https://dev.twitch.tv/docs/api/reference#update-chat-settings void updateEmoteMode(QString broadcasterID, QString moderatorID, diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index 0cf5d6094..8e8b2ed74 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -282,6 +282,13 @@ public: (FailureCallback failureCallback)), (override)); // /raid + // The extra parenthesis around the failure callback is because its type contains a comma + MOCK_METHOD( // /unraid + void, cancelRaid, + (QString broadcasterID, ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /unraid + // The extra parenthesis around the failure callback is because its type contains a comma MOCK_METHOD(void, updateEmoteMode, (QString broadcasterID, QString moderatorID, bool emoteMode,