feat: add broadcaster-only /completeprediction command (#6632)

Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
iProdigy
2025-12-14 10:07:48 -06:00
committed by GitHub
parent b9cf385a40
commit 9f46cae6c0
4 changed files with 183 additions and 1 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
- Minor: Added a setting to show the stream title in live messages. (#6572)
- Minor: Add categories to the emoji viewer. (#6598)
- Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, and `/lockprediction` commands. (#6583, #6612)
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632)
- Minor: Added support for BetterTTV Pro subscriber badges. (#6625)
- Bugfix: Moderation checks now include the lead moderator badge. (#6642)
- Bugfix: Expose the "Extra extension IDs" setting on non-Windows systems too. (#6509)
@@ -490,6 +490,7 @@ CommandController::CommandController(const Paths &paths)
this->registerCommand("/prediction", &commands::createPrediction);
this->registerCommand("/lockprediction", &commands::lockPrediction);
this->registerCommand("/cancelprediction", &commands::cancelPrediction);
this->registerCommand("/completeprediction", &commands::completePrediction);
this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules);
this->registerCommand("/c2-theme-autoreload", &commands::toggleThemeReload);
@@ -10,6 +10,9 @@
#include "providers/twitch/TwitchChannel.hpp"
#include "util/Helpers.hpp"
#include <QCommandLineParser>
#include <QProcess>
#include <chrono>
namespace {
@@ -218,4 +221,179 @@ QString cancelPrediction(const CommandContext &ctx)
return "";
}
QString completePrediction(const CommandContext &ctx)
{
const auto usage = QStringLiteral(
R"(Usage: /completeprediction --choice "<choice>" or /completeprediction --index <index> - Selects a winner for an outstanding prediction. The choice title must exactly match the wording in the prediction. Alternatively, you may specify the one-based index of the winning outcome.)");
if (ctx.twitchChannel == nullptr)
{
const auto err = QStringLiteral(
"The /completeprediction command only works in Twitch channels");
if (ctx.channel != nullptr)
{
ctx.channel->addSystemMessage(err);
}
else
{
qCWarning(chatterinoCommands) << "Invalid command context:" << err;
}
return "";
}
// Define arguments
QCommandLineParser parser;
parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
parser.setOptionsAfterPositionalArgumentsMode(
QCommandLineParser::ParseAsOptions);
QCommandLineOption choiceOption(
{"c", "choice"}, "The prediction outcome to select as the winner",
"choice");
QCommandLineOption indexOption(
{"i", "index"},
"The one-based index of the prediction outcome to select as the winner",
"index");
parser.addOptions({
choiceOption,
indexOption,
});
const auto joined = ctx.words.join(" ");
parser.parse(QProcess::splitCommand(joined));
// Input validation
const bool hasName = parser.isSet(choiceOption);
const bool hasIndex = parser.isSet(indexOption);
if (hasName && hasIndex)
{
ctx.channel->addSystemMessage(
"You may not specify choice and index simultaneously - " + usage);
return "";
}
if (!hasIndex && !hasName)
{
ctx.channel->addSystemMessage(
"You must specify either choice or index - " + usage);
return "";
}
int targetIndex = 0;
QString targetName;
if (hasName)
{
targetName = parser.value(choiceOption);
}
else
{
bool ok = true;
targetIndex = parser.value(indexOption).toInt(&ok);
if (!ok || targetIndex <= 0)
{
ctx.channel->addSystemMessage("Invalid index - " + usage);
return "";
}
}
// Perform action
const auto roomId = ctx.twitchChannel->roomId();
getHelix()->getPredictions(
roomId, {}, 1, {},
[channel = ctx.channel, roomId, hasIndex, targetIndex,
targetName](const auto &queryResult) {
if (queryResult.predictions.empty())
{
channel->addSystemMessage(
"You must start a prediction before you can complete one");
return;
}
auto prediction = queryResult.predictions.front();
if (prediction.status != "ACTIVE" && prediction.status != "LOCKED")
{
channel->addSystemMessage(
"Could not find an open prediction to complete");
return;
}
// identify winning outcome
auto outcomes = prediction.outcomes;
QString winnerId = "";
if (hasIndex)
{
auto maxIndex = outcomes.size();
if (targetIndex > maxIndex)
{
channel->addSystemMessage(
QString("Specified index (%1) exceeds the number of "
"outcomes (%2)")
.arg(QString::number(targetIndex))
.arg(QString::number(maxIndex)));
return;
}
winnerId = outcomes[targetIndex - 1].id;
}
else
{
for (const auto &outcome : outcomes)
{
if (outcome.title == targetName)
{
winnerId = outcome.id;
break;
}
}
if (winnerId == "")
{
auto options = std::accumulate(
outcomes.begin(), outcomes.end(), QString{},
[](const QString &acc,
const HelixPredictionOutcome &outcome) {
auto title = "'" + outcome.title + "'";
return acc.isEmpty() ? title : acc + ", " + title;
});
channel->addSystemMessage(
"Could not find the desired winner. Options include: " +
options);
return;
}
}
// resolve prediction
getHelix()->endPrediction(
roomId, prediction.id, false, winnerId,
[channel](const HelixPrediction &result) {
int totalPoints = 0;
HelixPredictionOutcome winner = result.outcomes.front();
for (const auto &outcome : result.outcomes)
{
totalPoints += outcome.channelPoints;
if (outcome.id == result.winningOutcomeID)
{
winner = outcome;
}
}
channel->addSystemMessage(
QString("Completed prediction: %1 - '%2' won %3 points "
"(%4 profit) to be distributed among %5 users")
.arg(result.title, winner.title,
localizeNumbers(totalPoints),
localizeNumbers(totalPoints -
winner.channelPoints),
localizeNumbers(winner.users)));
},
[channel](const auto &error) {
channel->addSystemMessage(
"Failed to complete prediction - " + error);
});
},
[channel = ctx.channel](const auto &error) {
channel->addSystemMessage(
"Failed to query predictions to complete - " + error);
});
return "";
}
} // namespace chatterino::commands
@@ -19,4 +19,7 @@ QString lockPrediction(const CommandContext &ctx);
/// /cancelprediction
QString cancelPrediction(const CommandContext &ctx);
/// /completeprediction
QString completePrediction(const CommandContext &ctx);
} // namespace chatterino::commands