diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce7242c..b51ec9c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index bd60c152..7ceec7a1 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -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); diff --git a/src/controllers/commands/builtin/twitch/Prediction.cpp b/src/controllers/commands/builtin/twitch/Prediction.cpp index 9ba68a1a..c2c57f88 100644 --- a/src/controllers/commands/builtin/twitch/Prediction.cpp +++ b/src/controllers/commands/builtin/twitch/Prediction.cpp @@ -10,6 +10,9 @@ #include "providers/twitch/TwitchChannel.hpp" #include "util/Helpers.hpp" +#include +#include + #include namespace { @@ -218,4 +221,179 @@ QString cancelPrediction(const CommandContext &ctx) return ""; } +QString completePrediction(const CommandContext &ctx) +{ + const auto usage = QStringLiteral( + R"(Usage: /completeprediction --choice "" or /completeprediction --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 diff --git a/src/controllers/commands/builtin/twitch/Prediction.hpp b/src/controllers/commands/builtin/twitch/Prediction.hpp index 50095a4f..00430dd1 100644 --- a/src/controllers/commands/builtin/twitch/Prediction.hpp +++ b/src/controllers/commands/builtin/twitch/Prediction.hpp @@ -19,4 +19,7 @@ QString lockPrediction(const CommandContext &ctx); /// /cancelprediction QString cancelPrediction(const CommandContext &ctx); +/// /completeprediction +QString completePrediction(const CommandContext &ctx); + } // namespace chatterino::commands