Migrate Remaining Chat Settings Commands to Helix API (#4040)

This commit is contained in:
nerix
2022-10-03 19:42:02 +02:00
committed by GitHub
parent 4c2e97bea6
commit 25bccc90b4
7 changed files with 846 additions and 39 deletions
+326 -38
View File
@@ -2108,46 +2108,74 @@ void CommandController::initialize(Settings &, Paths &paths)
return "";
}); // /raid
const auto formatChatSettingsError =
[](const HelixUpdateChatSettingsError error, const QString &message) {
QString errorMessage = QString("Failed to update - ");
using Error = HelixUpdateChatSettingsError;
switch (error)
{
case Error::UserMissingScope: {
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
errorMessage += "Missing required scope. "
"Re-login with your "
"account and try again.";
}
break;
const auto formatChatSettingsError = [](const HelixUpdateChatSettingsError
error,
const QString &message,
int durationUnitMultiplier = 1) {
static const QRegularExpression invalidRange("(\\d+) through (\\d+)");
case Error::UserNotAuthorized: {
// TODO(pajlada): Phrase MISSING_PERMISSION
errorMessage += "You don't have permission to "
"perform that action.";
}
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;
QString errorMessage = QString("Failed to update - ");
using Error = HelixUpdateChatSettingsError;
switch (error)
{
case Error::UserMissingScope: {
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
errorMessage += "Missing required scope. "
"Re-login with your "
"account and try again.";
}
return errorMessage;
};
break;
case Error::UserNotAuthorized:
case Error::Forbidden: {
// TODO(pajlada): Phrase MISSING_PERMISSION
errorMessage += "You don't have permission to "
"perform that action.";
}
break;
case Error::Ratelimited: {
errorMessage += "You are being ratelimited by Twitch. Try "
"again in a few seconds.";
}
break;
case Error::OutOfRange: {
QRegularExpressionMatch matched = invalidRange.match(message);
if (matched.hasMatch())
{
auto from = matched.captured(1).toInt();
auto to = matched.captured(2).toInt();
errorMessage +=
QString("The duration is out of the valid range: "
"%1 through %2.")
.arg(from == 0 ? "0s"
: formatTime(from *
durationUnitMultiplier),
to == 0
? "0s"
: formatTime(to * durationUnitMultiplier));
}
else
{
errorMessage += message;
}
}
break;
case Error::Forwarded: {
errorMessage = message;
}
break;
case Error::Unknown:
default: {
errorMessage += "An unknown error has occurred.";
}
break;
}
return errorMessage;
};
this->registerCommand("/emoteonly", [formatChatSettingsError](
const QStringList & /* words */,
@@ -2223,6 +2251,266 @@ void CommandController::initialize(Settings &, Paths &paths)
});
return "";
});
this->registerCommand(
"/subscribers", [formatChatSettingsError](
const QStringList & /* words */, auto channel) {
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(makeSystemMessage(
"You must be logged in to update chat settings!"));
return "";
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /subscribers command only works in Twitch channels"));
return "";
}
if (twitchChannel->accessRoomModes()->submode)
{
channel->addMessage(makeSystemMessage(
"This room is already in subscribers-only mode."));
return "";
}
getHelix()->updateSubscriberMode(
twitchChannel->roomId(), currentUser->getUserId(), true,
[](auto) {
//we'll get a message from irc
},
[channel, formatChatSettingsError](auto error, auto message) {
channel->addMessage(makeSystemMessage(
formatChatSettingsError(error, message)));
});
return "";
});
this->registerCommand("/subscribersoff", [formatChatSettingsError](
const QStringList
& /* words */,
auto channel) {
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(makeSystemMessage(
"You must be logged in to update chat settings!"));
return "";
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /subscribersoff command only works in Twitch channels"));
return "";
}
if (!twitchChannel->accessRoomModes()->submode)
{
channel->addMessage(makeSystemMessage(
"This room is not in subscribers-only mode."));
return "";
}
getHelix()->updateSubscriberMode(
twitchChannel->roomId(), currentUser->getUserId(), false,
[](auto) {
// we'll get a message from irc
},
[channel, formatChatSettingsError](auto error, auto message) {
channel->addMessage(
makeSystemMessage(formatChatSettingsError(error, message)));
});
return "";
});
this->registerCommand("/slow", [formatChatSettingsError](
const QStringList &words, auto channel) {
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(makeSystemMessage(
"You must be logged in to update chat settings!"));
return "";
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /slow command only works in Twitch channels"));
return "";
}
int duration = 30;
if (words.length() >= 2)
{
bool ok = false;
duration = words.at(1).toInt(&ok);
if (!ok || duration <= 0)
{
channel->addMessage(makeSystemMessage(
"Usage: \"/slow [duration]\" - Enables slow mode (limit "
"how often users may send messages). Duration (optional, "
"default=30) must be a positive number of seconds. Use "
"\"slowoff\" to disable. "));
return "";
}
}
if (twitchChannel->accessRoomModes()->slowMode == duration)
{
channel->addMessage(makeSystemMessage(
QString("This room is already in %1-second slow mode.")
.arg(duration)));
return "";
}
getHelix()->updateSlowMode(
twitchChannel->roomId(), currentUser->getUserId(), duration,
[](auto) {
//we'll get a message from irc
},
[channel, formatChatSettingsError](auto error, auto message) {
channel->addMessage(
makeSystemMessage(formatChatSettingsError(error, message)));
});
return "";
});
this->registerCommand(
"/slowoff", [formatChatSettingsError](const QStringList & /* words */,
auto channel) {
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(makeSystemMessage(
"You must be logged in to update chat settings!"));
return "";
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /slowoff command only works in Twitch channels"));
return "";
}
if (twitchChannel->accessRoomModes()->slowMode <= 0)
{
channel->addMessage(
makeSystemMessage("This room is not in slow mode."));
return "";
}
getHelix()->updateSlowMode(
twitchChannel->roomId(), currentUser->getUserId(), boost::none,
[](auto) {
// we'll get a message from irc
},
[channel, formatChatSettingsError](auto error, auto message) {
channel->addMessage(makeSystemMessage(
formatChatSettingsError(error, message)));
});
return "";
});
this->registerCommand("/followers", [formatChatSettingsError](
const QStringList &words,
auto channel) {
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(makeSystemMessage(
"You must be logged in to update chat settings!"));
return "";
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /followers command only works in Twitch channels"));
return "";
}
int duration = 0;
if (words.length() >= 2)
{
auto parsed = parseDurationToSeconds(words.mid(1).join(' '), 60);
duration = (int)(parsed / 60);
// -1 / 60 == 0 => use parsed
if (parsed < 0)
{
channel->addMessage(makeSystemMessage(
"Usage: \"/followers [duration]\" - Enables followers-only"
" mode (only users who have followed for 'duration' may "
"chat). Examples: \"30m\", \"1 week\", \"5 days 12 "
"hours\". Must be less than 3 months. "));
return "";
}
}
if (twitchChannel->accessRoomModes()->followerOnly == duration)
{
channel->addMessage(makeSystemMessage(
QString("This room is already in %1 followers-only mode.")
.arg(formatTime(duration * 60))));
return "";
}
getHelix()->updateFollowerMode(
twitchChannel->roomId(), currentUser->getUserId(), duration,
[](auto) {
//we'll get a message from irc
},
[channel, formatChatSettingsError](auto error, auto message) {
channel->addMessage(makeSystemMessage(
formatChatSettingsError(error, message, 60)));
});
return "";
});
this->registerCommand("/followersoff", [formatChatSettingsError](
const QStringList & /* words */,
auto channel) {
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(makeSystemMessage(
"You must be logged in to update chat settings!"));
return "";
}
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /followersoff command only works in Twitch channels"));
return "";
}
if (twitchChannel->accessRoomModes()->followerOnly < 0)
{
channel->addMessage(
makeSystemMessage("This room is not in followers-only mode. "));
return "";
}
getHelix()->updateFollowerMode(
twitchChannel->roomId(), currentUser->getUserId(), boost::none,
[](auto) {
// we'll get a message from irc
},
[channel, formatChatSettingsError](auto error, auto message) {
channel->addMessage(
makeSystemMessage(formatChatSettingsError(error, message)));
});
return "";
});
}
void CommandController::save()
+11 -1
View File
@@ -1650,7 +1650,17 @@ void Helix::updateChatSettings(
switch (result.status())
{
case 400:
case 400: {
if (message.contains("must be in the range"))
{
failureCallback(Error::OutOfRange, message);
}
else
{
failureCallback(Error::Forwarded, message);
}
}
break;
case 409:
case 422:
case 425: {
+2
View File
@@ -460,6 +460,8 @@ enum class HelixUpdateChatSettingsError { // update chat settings
UserNotAuthorized,
Ratelimited,
Forbidden,
OutOfRange,
// The error message is forwarded directly from the Twitch API
Forwarded,
}; // update chat settings
+176
View File
@@ -8,6 +8,110 @@
namespace chatterino {
namespace _helpers_internal {
int skipSpace(const QStringRef &view, int startPos)
{
while (startPos < view.length() && view.at(startPos).isSpace())
{
startPos++;
}
return startPos - 1;
}
bool matchesIgnorePlural(const QStringRef &word, const QString &singular)
{
if (!word.startsWith(singular))
{
return false;
}
if (word.length() == singular.length())
{
return true;
}
return word.length() == singular.length() + 1 &&
word.at(word.length() - 1).toLatin1() == 's';
}
std::pair<uint64_t, bool> findUnitMultiplierToSec(const QStringRef &view,
int &pos)
{
// Step 1. find end of unit
int startIdx = pos;
int endIdx = view.length();
for (; pos < view.length(); pos++)
{
auto c = view.at(pos);
if (c.isSpace() || c.isDigit())
{
endIdx = pos;
break;
}
}
pos--;
// TODO(QT6): use sliced (more readable)
auto unit = view.mid(startIdx, endIdx - startIdx);
if (unit.isEmpty())
{
return std::make_pair(0, false);
}
auto first = unit.at(0).toLatin1();
switch (first)
{
case 's': {
if (unit.length() == 1 ||
matchesIgnorePlural(unit, QStringLiteral("second")))
{
return std::make_pair(1, true);
}
}
break;
case 'm': {
if (unit.length() == 1 ||
matchesIgnorePlural(unit, QStringLiteral("minute")))
{
return std::make_pair(60, true);
}
if ((unit.length() == 2 && unit.at(1).toLatin1() == 'o') ||
matchesIgnorePlural(unit, QStringLiteral("month")))
{
return std::make_pair(60 * 60 * 24 * 30, true);
}
}
break;
case 'h': {
if (unit.length() == 1 ||
matchesIgnorePlural(unit, QStringLiteral("hour")))
{
return std::make_pair(60 * 60, true);
}
}
break;
case 'd': {
if (unit.length() == 1 ||
matchesIgnorePlural(unit, QStringLiteral("day")))
{
return std::make_pair(60 * 60 * 24, true);
}
}
break;
case 'w': {
if (unit.length() == 1 ||
matchesIgnorePlural(unit, QStringLiteral("week")))
{
return std::make_pair(60 * 60 * 24 * 7, true);
}
}
break;
}
return std::make_pair(0, false);
}
} // namespace _helpers_internal
using namespace _helpers_internal;
bool startsWithOrContains(const QString &str1, const QString &str2,
Qt::CaseSensitivity caseSensitivity, bool startsWith)
{
@@ -93,4 +197,76 @@ QString formatUserMention(const QString &userName, bool isFirstWord,
return result;
}
int64_t parseDurationToSeconds(const QString &inputString,
uint64_t noUnitMultiplier)
{
if (inputString.length() == 0)
{
return -1;
}
// TODO(QT6): use QStringView
QStringRef input(&inputString);
input = input.trimmed();
uint64_t currentValue = 0;
bool visitingNumber = true; // input must start with a number
int numberStartIdx = 0;
for (int pos = 0; pos < input.length(); pos++)
{
QChar c = input.at(pos);
if (visitingNumber && !c.isDigit())
{
uint64_t parsed =
(uint64_t)input.mid(numberStartIdx, pos - numberStartIdx)
.toUInt();
if (c.isSpace())
{
pos = skipSpace(input, pos) + 1;
if (pos >= input.length())
{
// input like "40 ", this shouldn't happen
// since we trimmed the view
return -1;
}
c = input.at(pos);
}
auto result = findUnitMultiplierToSec(input, pos);
if (!result.second)
{
return -1; // invalid unit or leading spaces (shouldn't happen)
}
currentValue += parsed * result.first;
visitingNumber = false;
}
else if (!visitingNumber && !c.isSpace())
{
if (!c.isDigit())
{
return -1; // expected a digit
}
visitingNumber = true;
numberStartIdx = pos;
}
// else: visitingNumber && isDigit || !visitingNumber && isSpace
}
if (visitingNumber)
{
if (numberStartIdx != 0)
{
return -1; // input like "1w 3s 70", 70 what? apples?
}
currentValue += input.toUInt() * noUnitMultiplier;
}
return (int64_t)currentValue;
}
} // namespace chatterino
+74
View File
@@ -2,11 +2,54 @@
#include <QColor>
#include <QString>
#include <QStringRef>
#include <cmath>
namespace chatterino {
// only qualified for tests
namespace _helpers_internal {
/**
* Skips all spaces.
* The caller must guarantee view.at(startPos).isSpace().
*
* @param view The string to skip spaces in.
* @param startPos The starting position (there must be a space in the view).
* @return The position of the last space.
*/
int skipSpace(const QStringRef &view, int startPos);
/**
* Checks if `word` equals `expected` (singular) or `expected` + 's' (plural).
*
* @param word Word to test. Must not be empty.
* @param expected Singular of the expected word.
* @return true if `word` is singular or plural of `expected`.
*/
bool matchesIgnorePlural(const QStringRef &word, const QString &expected);
/**
* Tries to find the unit starting at `pos` and returns its multiplier so
* `valueInUnit * multiplier = valueInSeconds` (e.g. 60 for minutes).
*
* Supported units are
* 'w[eek(s)]', 'd[ay(s)]',
* 'h[our(s)]', 'm[inute(s)]', 's[econd(s)]'.
* The unit must be in lowercase.
*
* @param view A view into a string
* @param pos The starting position.
* This is set to the last position of the unit
* if it's a valid unit, undefined otherwise.
* @return (multiplier, ok)
*/
std::pair<uint64_t, bool> findUnitMultiplierToSec(const QStringRef &view,
int &pos);
} // namespace _helpers_internal
/**
* @brief startsWithOrContains is a wrapper for checking
* whether str1 starts with or contains str2 within itself
@@ -29,6 +72,37 @@ QString kFormatNumbers(const int &number);
QColor getRandomColor(const QString &userId);
/**
* Parses a duration.
* Spaces are allowed before and after a unit but not mandatory.
* Supported units are
* 'w[eek(s)]', 'd[ay(s)]',
* 'h[our(s)]', 'm[inute(s)]', 's[econd(s)]'.
* Units must be lowercase.
*
* If the entire input string is a number (e.g. "12345"),
* then it's multiplied by noUnitMultiplier.
*
* Examples:
*
* - "1w 2h"
* - "1w 1w 0s 4d" (2weeks, 4days)
* - "5s3h4w" (4weeks, 3hours, 5seconds)
* - "30m"
* - "1 week"
* - "5 days 12 hours"
* - "10" (10 * noUnitMultiplier seconds)
*
* @param inputString A non-empty string to parse
* @param noUnitMultiplier A multiplier if the input string only contains one number.
* For example, if a number without a unit should be interpreted
* as a minute, set this to 60. If it should be interpreted
* as a second, set it to 1 (default).
* @return The parsed duration in seconds, -1 if the input is invalid.
*/
int64_t parseDurationToSeconds(const QString &inputString,
uint64_t noUnitMultiplier = 1);
/**
* @brief Takes a user's name and some formatting parameter and spits out the standardized way to format it
*