Migrate /raid to Helix. (#4029)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Marko 2022-10-02 15:27:55 +02:00 committed by GitHub
parent 9816722b5e
commit f8f9903892
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 337 additions and 7 deletions

View file

@ -49,6 +49,7 @@
- Minor: Migrated /unvip command to Helix API. (#4025)
- Minor: Migrated /untimeout to Helix API. (#4026)
- Minor: Migrated /unban to Helix API. (#4026)
- Minor: Migrated /raid command to Helix API. (#4029)
- 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)

View file

@ -33,9 +33,10 @@ Q_LOGGING_CATEGORY(chatterinoNuulsuploader, "chatterino.nuulsuploader",
Q_LOGGING_CATEGORY(chatterinoPubSub, "chatterino.pubsub", logThreshold);
Q_LOGGING_CATEGORY(chatterinoRecentMessages, "chatterino.recentmessages",
logThreshold);
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
Q_LOGGING_CATEGORY(chatterinoSettings, "chatterino.settings", logThreshold);
Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode",
logThreshold);
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTwitch, "chatterino.twitch", logThreshold);
Q_LOGGING_CATEGORY(chatterinoUpdate, "chatterino.update", logThreshold);

View file

@ -25,8 +25,9 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification);
Q_DECLARE_LOGGING_CATEGORY(chatterinoNuulsuploader);
Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub);
Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages);
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings);
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode);
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch);
Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);

View file

@ -38,8 +38,30 @@
#include <QUrl>
namespace {
using namespace chatterino;
bool areIRCCommandsStillAvailable()
{
// TODO: time-gate
return true;
}
QString useIRCCommand(const QStringList &words)
{
// Reform the original command
auto originalCommand = words.join(" ");
// Replace the / with a . to pass it along to TMI
auto newCommand = originalCommand;
newCommand.replace(0, 1, ".");
qCDebug(chatterinoTwitch)
<< "Forwarding command" << originalCommand << "as" << newCommand;
return newCommand;
}
void sendWhisperMessage(const QString &text)
{
// (hemirt) pajlada: "we should not be sending whispers through jtv, but
@ -1960,6 +1982,129 @@ void CommandController::initialize(Settings &, Paths &paths)
// These changes are from the helix-command-migration/unban-untimeout branch
// These changes are from the helix-command-migration/unban-untimeout branch
// These changes are from the helix-command-migration/unban-untimeout branch
this->registerCommand( // /raid
"/raid", [](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() < 2)
{
channel->addMessage(makeSystemMessage(
"Usage: \"/raid <username>\" - Raid a user. "
"Only the broadcaster can start a raid."));
return "";
}
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(makeSystemMessage(
"You must be logged in to start a raid!"));
return "";
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /raid command only works in Twitch channels"));
return "";
}
auto target = words.at(1);
stripChannelName(target);
getHelix()->getUserByName(
target,
[twitchChannel, channel](const HelixUser &targetUser) {
getHelix()->startRaid(
twitchChannel->roomId(), targetUser.id,
[channel, targetUser] {
channel->addMessage(makeSystemMessage(
QString("You started to raid %1.")
.arg(targetUser.displayName)));
},
[channel, targetUser](auto error, auto message) {
QString errorMessage =
QString("Failed to start a raid - ");
using Error = HelixStartRaidError;
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 start a raid.";
}
break;
case Error::CantRaidYourself: {
errorMessage +=
"A channel cannot raid itself.";
}
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));
});
},
[channel, target] {
// Equivalent error from IRC
channel->addMessage(makeSystemMessage(
QString("Invalid username: %1").arg(target)));
});
return "";
}); // /raid
}
void CommandController::save()

View file

@ -1447,6 +1447,89 @@ void Helix::unbanUser(
// These changes are from the helix-command-migration/unban-untimeout branch
// These changes are from the helix-command-migration/unban-untimeout branch
void Helix::startRaid(
QString fromBroadcasterID, QString toBroadcasterID,
ResultCallback<> successCallback,
FailureCallback<HelixStartRaidError, QString> failureCallback)
{
using Error = HelixStartRaidError;
QUrlQuery urlQuery;
urlQuery.addQueryItem("from_broadcaster_id", fromBroadcasterID);
urlQuery.addQueryItem("to_broadcaster_id", toBroadcasterID);
this->makeRequest("raids", urlQuery)
.type(NetworkRequestType::Post)
.onSuccess(
[successCallback, failureCallback](auto /*result*/) -> Outcome {
successCallback();
return Success;
})
.onError([failureCallback](auto result) {
auto obj = result.parseJson();
auto message = obj.value("message").toString();
switch (result.status())
{
case 400: {
if (message.compare("The IDs in from_broadcaster_id and "
"to_broadcaster_id cannot be the same.",
Qt::CaseInsensitive) == 0)
{
failureCallback(Error::CantRaidYourself, message);
}
else
{
failureCallback(Error::Forwarded, message);
}
}
break;
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 409: {
failureCallback(Error::Forwarded, message);
}
break;
case 429: {
failureCallback(Error::Ratelimited, message);
}
break;
default: {
qCDebug(chatterinoTwitch)
<< "Unhandled error while starting a raid:"
<< result.status() << result.getData() << obj;
failureCallback(Error::Unknown, message);
}
break;
}
})
.execute();
}
NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
{
assert(!url.startsWith("/"));

View file

@ -414,6 +414,17 @@ enum class HelixUnbanUserError {
Forwarded,
}; // These changes are from the helix-command-migration/unban-untimeout branch
enum class HelixStartRaidError { // /raid
Unknown,
UserMissingScope,
UserNotAuthorized,
CantRaidYourself,
Ratelimited,
// The error message is forwarded directly from the Twitch API
Forwarded,
}; // /raid
class IHelix
{
public:
@ -589,6 +600,13 @@ public:
FailureCallback<HelixUnbanUserError, QString> failureCallback) = 0;
// These changes are from the helix-command-migration/unban-untimeout branch
// https://dev.twitch.tv/docs/api/reference#start-a-raid
virtual void startRaid(
QString fromBroadcasterID, QString toBroadcasterID,
ResultCallback<> successCallback,
FailureCallback<HelixStartRaidError, QString> failureCallback) = 0;
// https://dev.twitch.tv/docs/api/reference#start-a-raid
virtual void update(QString clientId, QString oauthToken) = 0;
};
@ -758,6 +776,13 @@ public:
FailureCallback<HelixUnbanUserError, QString> failureCallback) final;
// These changes are from the helix-command-migration/unban-untimeout branch
// https://dev.twitch.tv/docs/api/reference#start-a-raid
void startRaid(
QString fromBroadcasterID, QString toBroadcasterID,
ResultCallback<> successCallback,
FailureCallback<HelixStartRaidError, QString> failureCallback) final;
// https://dev.twitch.tv/docs/api/reference#start-a-raid
void update(QString clientId, QString oauthToken) final;
static void initialize();

View file

@ -58,6 +58,20 @@ enum UsernameDisplayMode : int {
LocalizedName = 2, // Localized name
UsernameAndLocalizedName = 3, // Username (Localized name)
};
enum HelixTimegateOverride : int {
// Use the default timegated behaviour
// This means we use the old IRC command up until the migration date and
// switch over to the Helix API only after the migration date
Timegate = 1,
// Ignore timegating and always force use the IRC command
AlwaysUseIRC = 2,
// Ignore timegating and always force use the Helix API
AlwaysUseHelix = 3,
};
/// Settings which are availlable for reading and writing on the gui thread.
// These settings are still accessed concurrently in the code but it is bad practice.
class Settings : public ABSettings, public ConcurrentSettings
@ -395,6 +409,12 @@ public:
800,
};
// Temporary time-gate-overrides
EnumSetting<HelixTimegateOverride> helixTimegateRaid = {
"/misc/twitch/helix-timegate/raid",
HelixTimegateOverride::Timegate,
};
IntSetting emotesTooltipPreview = {"/misc/emotesTooltipPreview", 1};
BoolSetting openLinksIncognito = {"/misc/openLinksIncognito", 0};

View file

@ -1,10 +1,7 @@
#include "GeneralPage.hpp"
#include <QFontDialog>
#include <QLabel>
#include <QScrollArea>
#include "widgets/settingspages/GeneralPage.hpp"
#include "Application.hpp"
#include "common/QLogging.hpp"
#include "common/Version.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/NativeMessaging.hpp"
@ -21,6 +18,9 @@
#include <QDesktopServices>
#include <QFileDialog>
#include <QFontDialog>
#include <QLabel>
#include <QScrollArea>
#define CHROME_EXTENSION_LINK \
"https://chrome.google.com/webstore/detail/chatterino-native-host/" \
@ -720,6 +720,52 @@ void GeneralPage::initLayout(GeneralPageView &layout)
layout.addCheckbox("Messages in /mentions highlights tab",
s.highlightMentions);
// Helix timegate settings
auto helixTimegateGetValue = [](auto val) {
switch (val)
{
case HelixTimegateOverride::Timegate:
return "Timegate";
case HelixTimegateOverride::AlwaysUseIRC:
return "Always use IRC";
case HelixTimegateOverride::AlwaysUseHelix:
return "Always use Helix";
default:
return "Timegate";
}
};
auto helixTimegateSetValue = [](auto args) {
const auto &v = args.value;
if (v == "Timegate")
{
return HelixTimegateOverride::Timegate;
}
if (v == "Always use IRC")
{
return HelixTimegateOverride::AlwaysUseIRC;
}
if (v == "Always use Helix")
{
return HelixTimegateOverride::AlwaysUseHelix;
}
qCDebug(chatterinoSettings) << "Unknown Helix timegate override value"
<< v << ", using default value Timegate";
return HelixTimegateOverride::Timegate;
};
auto *helixTimegateRaid =
layout.addDropdown<std::underlying_type<HelixTimegateOverride>::type>(
"Helix timegate /raid behaviour",
{"Timegate", "Always use IRC", "Always use Helix"},
s.helixTimegateRaid,
helixTimegateGetValue, //
helixTimegateSetValue, //
false);
helixTimegateRaid->setMinimumWidth(
helixTimegateRaid->minimumSizeHint().width());
layout.addStretch();
// invisible element for width

View file

@ -273,6 +273,14 @@ public:
(FailureCallback<HelixUnbanUserError, QString> failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type contains a comma
MOCK_METHOD( // /raid
void, startRaid,
(QString fromBroadcasterID, QString toBroadcasterId,
ResultCallback<> successCallback,
(FailureCallback<HelixStartRaidError, QString> failureCallback)),
(override)); // /raid
MOCK_METHOD(void, update, (QString clientId, QString oauthToken),
(override));
};