mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Migrate Remaining Chat Settings Commands to Helix API (#4040)
This commit is contained in:
parent
4c2e97bea6
commit
25bccc90b4
|
@ -51,6 +51,12 @@
|
||||||
- Minor: Migrated /unvip command to Helix API. (#4025)
|
- Minor: Migrated /unvip command to Helix API. (#4025)
|
||||||
- Minor: Migrated /untimeout to Helix API. (#4026)
|
- Minor: Migrated /untimeout to Helix API. (#4026)
|
||||||
- Minor: Migrated /unban to Helix API. (#4026)
|
- Minor: Migrated /unban to Helix API. (#4026)
|
||||||
|
- Minor: Migrated /subscribers to Helix API. (#4040)
|
||||||
|
- Minor: Migrated /subscribersoff to Helix API. (#4040)
|
||||||
|
- Minor: Migrated /slow to Helix API. (#4040)
|
||||||
|
- Minor: Migrated /slowoff to Helix API. (#4040)
|
||||||
|
- 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 /raid command to Helix API. Chat command will continue to be used until February 11th 2023. (#4029)
|
||||||
- Bugfix: Connection to Twitch PubSub now recovers more reliably. (#3643, #3716)
|
- 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 `Smooth scrolling on new messages` setting sometimes hiding messages. (#4028)
|
||||||
|
|
|
@ -2108,46 +2108,74 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
return "";
|
return "";
|
||||||
}); // /raid
|
}); // /raid
|
||||||
|
|
||||||
const auto formatChatSettingsError =
|
const auto formatChatSettingsError = [](const HelixUpdateChatSettingsError
|
||||||
[](const HelixUpdateChatSettingsError error, const QString &message) {
|
error,
|
||||||
QString errorMessage = QString("Failed to update - ");
|
const QString &message,
|
||||||
using Error = HelixUpdateChatSettingsError;
|
int durationUnitMultiplier = 1) {
|
||||||
switch (error)
|
static const QRegularExpression invalidRange("(\\d+) through (\\d+)");
|
||||||
{
|
|
||||||
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: {
|
QString errorMessage = QString("Failed to update - ");
|
||||||
// TODO(pajlada): Phrase MISSING_PERMISSION
|
using Error = HelixUpdateChatSettingsError;
|
||||||
errorMessage += "You don't have permission to "
|
switch (error)
|
||||||
"perform that action.";
|
{
|
||||||
}
|
case Error::UserMissingScope: {
|
||||||
break;
|
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
|
||||||
|
errorMessage += "Missing required scope. "
|
||||||
case Error::Ratelimited: {
|
"Re-login with your "
|
||||||
errorMessage += "You are being ratelimited by Twitch. Try "
|
"account and try again.";
|
||||||
"again in a few seconds.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::Forwarded: {
|
|
||||||
errorMessage = message;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::Unknown:
|
|
||||||
default: {
|
|
||||||
errorMessage += "An unknown error has occurred.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
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](
|
this->registerCommand("/emoteonly", [formatChatSettingsError](
|
||||||
const QStringList & /* words */,
|
const QStringList & /* words */,
|
||||||
|
@ -2223,6 +2251,266 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
});
|
});
|
||||||
return "";
|
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()
|
void CommandController::save()
|
||||||
|
|
|
@ -1650,7 +1650,17 @@ void Helix::updateChatSettings(
|
||||||
|
|
||||||
switch (result.status())
|
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 409:
|
||||||
case 422:
|
case 422:
|
||||||
case 425: {
|
case 425: {
|
||||||
|
|
|
@ -460,6 +460,8 @@ enum class HelixUpdateChatSettingsError { // update chat settings
|
||||||
UserNotAuthorized,
|
UserNotAuthorized,
|
||||||
Ratelimited,
|
Ratelimited,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
|
OutOfRange,
|
||||||
|
|
||||||
// The error message is forwarded directly from the Twitch API
|
// The error message is forwarded directly from the Twitch API
|
||||||
Forwarded,
|
Forwarded,
|
||||||
}; // update chat settings
|
}; // update chat settings
|
||||||
|
|
|
@ -8,6 +8,110 @@
|
||||||
|
|
||||||
namespace chatterino {
|
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,
|
bool startsWithOrContains(const QString &str1, const QString &str2,
|
||||||
Qt::CaseSensitivity caseSensitivity, bool startsWith)
|
Qt::CaseSensitivity caseSensitivity, bool startsWith)
|
||||||
{
|
{
|
||||||
|
@ -93,4 +197,76 @@ QString formatUserMention(const QString &userName, bool isFirstWord,
|
||||||
return result;
|
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
|
} // namespace chatterino
|
||||||
|
|
|
@ -2,11 +2,54 @@
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringRef>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
namespace chatterino {
|
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
|
* @brief startsWithOrContains is a wrapper for checking
|
||||||
* whether str1 starts with or contains str2 within itself
|
* whether str1 starts with or contains str2 within itself
|
||||||
|
@ -29,6 +72,37 @@ QString kFormatNumbers(const int &number);
|
||||||
|
|
||||||
QColor getRandomColor(const QString &userId);
|
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
|
* @brief Takes a user's name and some formatting parameter and spits out the standardized way to format it
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
using namespace _helpers_internal;
|
||||||
|
|
||||||
TEST(Helpers, formatUserMention)
|
TEST(Helpers, formatUserMention)
|
||||||
{
|
{
|
||||||
|
@ -250,3 +251,253 @@ TEST(Helpers, BatchDifferentInputType)
|
||||||
|
|
||||||
EXPECT_EQ(result, expectation);
|
EXPECT_EQ(result, expectation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Helpers, skipSpace)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
int startIdx;
|
||||||
|
int expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> tests{{"foo bar", 3, 6}, {"foo bar", 3, 3},
|
||||||
|
{"foo ", 3, 3}, {"foo ", 3, 6},
|
||||||
|
{" ", 0, 2}, {" ", 0, 0}};
|
||||||
|
|
||||||
|
for (const auto &c : tests)
|
||||||
|
{
|
||||||
|
const auto actual = skipSpace(&c.input, c.startIdx);
|
||||||
|
|
||||||
|
EXPECT_EQ(actual, c.expected)
|
||||||
|
<< actual << " (" << qUtf8Printable(c.input)
|
||||||
|
<< ") did not match expected value " << c.expected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Helpers, findUnitMultiplierToSec)
|
||||||
|
{
|
||||||
|
constexpr uint64_t sec = 1;
|
||||||
|
constexpr uint64_t min = 60;
|
||||||
|
constexpr uint64_t hour = min * 60;
|
||||||
|
constexpr uint64_t day = hour * 24;
|
||||||
|
constexpr uint64_t week = day * 7;
|
||||||
|
constexpr uint64_t month = day * 30;
|
||||||
|
constexpr uint64_t bad = 0;
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
int startPos;
|
||||||
|
int expectedEndPos;
|
||||||
|
uint64_t expectedMultiplier;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> tests{
|
||||||
|
{"s", 0, 0, sec},
|
||||||
|
{"m", 0, 0, min},
|
||||||
|
{"h", 0, 0, hour},
|
||||||
|
{"d", 0, 0, day},
|
||||||
|
{"w", 0, 0, week},
|
||||||
|
{"mo", 0, 1, month},
|
||||||
|
|
||||||
|
{"s alienpls", 0, 0, sec},
|
||||||
|
{"m alienpls", 0, 0, min},
|
||||||
|
{"h alienpls", 0, 0, hour},
|
||||||
|
{"d alienpls", 0, 0, day},
|
||||||
|
{"w alienpls", 0, 0, week},
|
||||||
|
{"mo alienpls", 0, 1, month},
|
||||||
|
|
||||||
|
{"alienpls s", 9, 9, sec},
|
||||||
|
{"alienpls m", 9, 9, min},
|
||||||
|
{"alienpls h", 9, 9, hour},
|
||||||
|
{"alienpls d", 9, 9, day},
|
||||||
|
{"alienpls w", 9, 9, week},
|
||||||
|
{"alienpls mo", 9, 10, month},
|
||||||
|
|
||||||
|
{"alienpls s alienpls", 9, 9, sec},
|
||||||
|
{"alienpls m alienpls", 9, 9, min},
|
||||||
|
{"alienpls h alienpls", 9, 9, hour},
|
||||||
|
{"alienpls d alienpls", 9, 9, day},
|
||||||
|
{"alienpls w alienpls", 9, 9, week},
|
||||||
|
{"alienpls mo alienpls", 9, 10, month},
|
||||||
|
|
||||||
|
{"second", 0, 5, sec},
|
||||||
|
{"minute", 0, 5, min},
|
||||||
|
{"hour", 0, 3, hour},
|
||||||
|
{"day", 0, 2, day},
|
||||||
|
{"week", 0, 3, week},
|
||||||
|
{"month", 0, 4, month},
|
||||||
|
|
||||||
|
{"alienpls2 second", 10, 15, sec},
|
||||||
|
{"alienpls2 minute", 10, 15, min},
|
||||||
|
{"alienpls2 hour", 10, 13, hour},
|
||||||
|
{"alienpls2 day", 10, 12, day},
|
||||||
|
{"alienpls2 week", 10, 13, week},
|
||||||
|
{"alienpls2 month", 10, 14, month},
|
||||||
|
|
||||||
|
{"alienpls2 second alienpls", 10, 15, sec},
|
||||||
|
{"alienpls2 minute alienpls", 10, 15, min},
|
||||||
|
{"alienpls2 hour alienpls", 10, 13, hour},
|
||||||
|
{"alienpls2 day alienpls", 10, 12, day},
|
||||||
|
{"alienpls2 week alienpls", 10, 13, week},
|
||||||
|
{"alienpls2 month alienpls", 10, 14, month},
|
||||||
|
|
||||||
|
{"seconds", 0, 6, sec},
|
||||||
|
{"minutes", 0, 6, min},
|
||||||
|
{"hours", 0, 4, hour},
|
||||||
|
{"days", 0, 3, day},
|
||||||
|
{"weeks", 0, 4, week},
|
||||||
|
{"months", 0, 5, month},
|
||||||
|
|
||||||
|
{"alienpls2 seconds", 10, 16, sec},
|
||||||
|
{"alienpls2 minutes", 10, 16, min},
|
||||||
|
{"alienpls2 hours", 10, 14, hour},
|
||||||
|
{"alienpls2 days", 10, 13, day},
|
||||||
|
{"alienpls2 weeks", 10, 14, week},
|
||||||
|
{"alienpls2 months", 10, 15, month},
|
||||||
|
|
||||||
|
{"alienpls2 seconds alienpls", 10, 16, sec},
|
||||||
|
{"alienpls2 minutes alienpls", 10, 16, min},
|
||||||
|
{"alienpls2 hours alienpls", 10, 14, hour},
|
||||||
|
{"alienpls2 days alienpls", 10, 13, day},
|
||||||
|
{"alienpls2 weeks alienpls", 10, 14, week},
|
||||||
|
{"alienpls2 months alienpls", 10, 15, month},
|
||||||
|
|
||||||
|
{"sec", 0, 0, bad},
|
||||||
|
{"min", 0, 0, bad},
|
||||||
|
{"ho", 0, 0, bad},
|
||||||
|
{"da", 0, 0, bad},
|
||||||
|
{"we", 0, 0, bad},
|
||||||
|
{"mon", 0, 0, bad},
|
||||||
|
{"foo", 0, 0, bad},
|
||||||
|
{"S", 0, 0, bad},
|
||||||
|
{"M", 0, 0, bad},
|
||||||
|
{"H", 0, 0, bad},
|
||||||
|
{"D", 0, 0, bad},
|
||||||
|
{"W", 0, 0, bad},
|
||||||
|
{"MO", 0, 1, bad},
|
||||||
|
|
||||||
|
{"alienpls2 sec", 10, 0, bad},
|
||||||
|
{"alienpls2 min", 10, 0, bad},
|
||||||
|
{"alienpls2 ho", 10, 0, bad},
|
||||||
|
{"alienpls2 da", 10, 0, bad},
|
||||||
|
{"alienpls2 we", 10, 0, bad},
|
||||||
|
{"alienpls2 mon", 10, 0, bad},
|
||||||
|
{"alienpls2 foo", 10, 0, bad},
|
||||||
|
{"alienpls2 S", 10, 0, bad},
|
||||||
|
{"alienpls2 M", 10, 0, bad},
|
||||||
|
{"alienpls2 H", 10, 0, bad},
|
||||||
|
{"alienpls2 D", 10, 0, bad},
|
||||||
|
{"alienpls2 W", 10, 0, bad},
|
||||||
|
{"alienpls2 MO", 10, 0, bad},
|
||||||
|
|
||||||
|
{"alienpls2 sec alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 min alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 ho alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 da alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 we alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 mon alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 foo alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 S alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 M alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 H alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 D alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 W alienpls", 10, 0, bad},
|
||||||
|
{"alienpls2 MO alienpls", 10, 0, bad},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &c : tests)
|
||||||
|
{
|
||||||
|
int pos = c.startPos;
|
||||||
|
const auto actual = findUnitMultiplierToSec(&c.input, pos);
|
||||||
|
|
||||||
|
if (c.expectedMultiplier == bad)
|
||||||
|
{
|
||||||
|
EXPECT_FALSE(actual.second) << qUtf8Printable(c.input);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(pos == c.expectedEndPos && actual.second &&
|
||||||
|
actual.first == c.expectedMultiplier)
|
||||||
|
<< qUtf8Printable(c.input)
|
||||||
|
<< ": Expected(end: " << c.expectedEndPos
|
||||||
|
<< ", mult: " << c.expectedMultiplier << ") Actual(end: " << pos
|
||||||
|
<< ", mult: " << actual.first << ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Helpers, parseDurationToSeconds)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
int64_t output;
|
||||||
|
int64_t noUnitMultiplier = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto wrongInput = [](QString &&input) {
|
||||||
|
return TestCase{input, -1};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> tests{
|
||||||
|
{"1 minutes 9s", 69},
|
||||||
|
{"22 m 17 s", 1337},
|
||||||
|
{"7d 5h 10m 52s", 623452},
|
||||||
|
{"2h 19m 5s ", 8345},
|
||||||
|
{"3d 15 h 13m 54s", 314034},
|
||||||
|
{"27s", 27},
|
||||||
|
{
|
||||||
|
"9h 36 m 29s", 34589,
|
||||||
|
7, // should be unused
|
||||||
|
},
|
||||||
|
{"1h 59s", 3659},
|
||||||
|
{"12d2h22m25s", 1045345},
|
||||||
|
{"2h22m25s12d", 1045345},
|
||||||
|
{"1d32s", 86432},
|
||||||
|
{"0", 0},
|
||||||
|
{"0 s", 0},
|
||||||
|
{"1weeks", 604800},
|
||||||
|
{"2 day5days", 604800},
|
||||||
|
{"1 day", 86400},
|
||||||
|
{"4 hours 30m 19h 30 minute", 86400},
|
||||||
|
{"3 months", 7776000},
|
||||||
|
{"1 mo 2month", 7776000},
|
||||||
|
// from documentation
|
||||||
|
{"1w 2h", 612000},
|
||||||
|
{"1w 1w 0s 4d", 1555200},
|
||||||
|
{"5s3h4w", 2430005},
|
||||||
|
// from twitch response
|
||||||
|
{"30m", 1800},
|
||||||
|
{"1 week", 604800},
|
||||||
|
{"5 days 12 hours", 475200},
|
||||||
|
// noUnitMultiplier
|
||||||
|
{"0", 0, 60},
|
||||||
|
{
|
||||||
|
"60", 3600,
|
||||||
|
60, // minute
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"1",
|
||||||
|
86400, // 1d
|
||||||
|
86400,
|
||||||
|
},
|
||||||
|
// wrong input
|
||||||
|
wrongInput("1min"),
|
||||||
|
wrongInput(""),
|
||||||
|
wrongInput("1m5w+5"),
|
||||||
|
wrongInput("1h30"),
|
||||||
|
wrongInput("12 34w"),
|
||||||
|
wrongInput("4W"),
|
||||||
|
wrongInput("1min"),
|
||||||
|
wrongInput("4Min"),
|
||||||
|
wrongInput("4sec"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &c : tests)
|
||||||
|
{
|
||||||
|
const auto actual = parseDurationToSeconds(c.input, c.noUnitMultiplier);
|
||||||
|
|
||||||
|
EXPECT_EQ(actual, c.output)
|
||||||
|
<< actual << " (" << qUtf8Printable(c.input)
|
||||||
|
<< ") did not match expected value " << c.output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue