feat: add poll and prediction commands for broadcasters (#6583)
Reviewed-by: nerix <nero.9@hotmail.de> Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
#include "controllers/commands/builtin/twitch/DeleteMessages.hpp"
|
||||
#include "controllers/commands/builtin/twitch/GetModerators.hpp"
|
||||
#include "controllers/commands/builtin/twitch/GetVIPs.hpp"
|
||||
#include "controllers/commands/builtin/twitch/Poll.hpp"
|
||||
#include "controllers/commands/builtin/twitch/Prediction.hpp"
|
||||
#include "controllers/commands/builtin/twitch/Raid.hpp"
|
||||
#include "controllers/commands/builtin/twitch/RemoveModerator.hpp"
|
||||
#include "controllers/commands/builtin/twitch/RemoveVIP.hpp"
|
||||
@@ -481,6 +483,9 @@ CommandController::CommandController(const Paths &paths)
|
||||
|
||||
this->registerCommand("/shoutout", &commands::sendShoutout);
|
||||
|
||||
this->registerCommand("/poll", &commands::createPoll);
|
||||
this->registerCommand("/prediction", &commands::createPrediction);
|
||||
|
||||
this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules);
|
||||
this->registerCommand("/c2-theme-autoreload", &commands::toggleThemeReload);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "controllers/commands/builtin/twitch/Poll.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/CommandContext.hpp"
|
||||
#include "controllers/commands/common/ChannelAction.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
constexpr auto MIN_POLL_DURATION = std::chrono::seconds(10);
|
||||
constexpr auto MAX_POLL_DURATION = std::chrono::seconds(1800);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
QString createPoll(const CommandContext &ctx)
|
||||
{
|
||||
const auto command = QStringLiteral("/poll");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/poll --title "<title>" --duration <duration>[time unit] --choice "<choice1>" --choice "<choice2>" [options...]" - Creates a poll for users to vote among the defined options. Title may not exceed 60 characters. There must be between two and five poll choices. Duration must be a positive integer; time unit (optional, default=s) must be one of s, m; maximum duration is 30 minutes. Options: --points <points> to allow spending the specified channel points for each additional vote.)");
|
||||
const auto action = parseUserParticipationAction(
|
||||
ctx, command, usage, MIN_POLL_DURATION, MAX_POLL_DURATION);
|
||||
|
||||
if (!action.has_value())
|
||||
{
|
||||
if (ctx.channel != nullptr)
|
||||
{
|
||||
ctx.channel->addSystemMessage(action.error());
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommands)
|
||||
<< "Error parsing command:" << action.error();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
ctx.channel->addSystemMessage(
|
||||
"You must be logged in to create a poll!");
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto &poll = action.value();
|
||||
getHelix()->createPoll(
|
||||
poll.broadcasterID, poll.title, poll.choices, poll.duration,
|
||||
poll.pointsPerVote,
|
||||
[channel = ctx.channel, poll] {
|
||||
channel->addSystemMessage(
|
||||
QString("Created poll: '%1'").arg(poll.title));
|
||||
},
|
||||
[channel = ctx.channel](const auto &error) {
|
||||
channel->addSystemMessage("Failed to create poll - " + error);
|
||||
});
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace chatterino::commands
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
class QString;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct CommandContext;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
/// /poll
|
||||
QString createPoll(const CommandContext &ctx);
|
||||
|
||||
} // namespace chatterino::commands
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "controllers/commands/builtin/twitch/Prediction.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/CommandContext.hpp"
|
||||
#include "controllers/commands/common/ChannelAction.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
constexpr auto MIN_PREDICT_DURATION = std::chrono::seconds(30);
|
||||
constexpr auto MAX_PREDICT_DURATION = std::chrono::seconds(1800);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
QString createPrediction(const CommandContext &ctx)
|
||||
{
|
||||
const auto command = QStringLiteral("/prediction");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/prediction --title "<title>" --choice "<choice1>" --choice "<choice2>" --duration <duration>[time unit]" - Creates a prediction for users to guess among the defined options. Title may not exceed 45 characters. There must be between two and ten choices. Duration must be a positive integer; time unit (optional, default=s) must be one of s, m; maximum duration is 30 minutes.)");
|
||||
const auto action = parseUserParticipationAction(
|
||||
ctx, command, usage, MIN_PREDICT_DURATION, MAX_PREDICT_DURATION);
|
||||
|
||||
if (!action.has_value())
|
||||
{
|
||||
if (ctx.channel != nullptr)
|
||||
{
|
||||
ctx.channel->addSystemMessage(action.error());
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommands)
|
||||
<< "Error parsing command:" << action.error();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
ctx.channel->addSystemMessage(
|
||||
"You must be logged in to create a prediction!");
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto &data = action.value();
|
||||
getHelix()->createPrediction(
|
||||
data.broadcasterID, data.title, data.choices, data.duration,
|
||||
[channel = ctx.channel, data] {
|
||||
channel->addSystemMessage(
|
||||
QString("Created prediction: '%1'").arg(data.title));
|
||||
},
|
||||
[channel = ctx.channel](const auto &error) {
|
||||
channel->addSystemMessage("Failed to create prediction - " + error);
|
||||
});
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace chatterino::commands
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
class QString;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct CommandContext;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
/// /prediction
|
||||
QString createPrediction(const CommandContext &ctx);
|
||||
|
||||
} // namespace chatterino::commands
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "util/Twitch.hpp"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QProcess>
|
||||
#include <QStringBuilder>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -181,4 +182,83 @@ nonstd::expected<std::vector<PerformChannelAction>, QString> parseChannelAction(
|
||||
return actions;
|
||||
}
|
||||
|
||||
ExpectedStr<StartUserParticipationAction> parseUserParticipationAction(
|
||||
const CommandContext &ctx, const QString &command, const QString &usage,
|
||||
const std::chrono::seconds minDuration,
|
||||
const std::chrono::seconds maxDuration)
|
||||
{
|
||||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
// This action must be performed with a twitch channel as a context
|
||||
return makeUnexpected("The " % command %
|
||||
" command only works in Twitch channels");
|
||||
}
|
||||
|
||||
// Define arguments
|
||||
QCommandLineParser parser;
|
||||
parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
|
||||
parser.setOptionsAfterPositionalArgumentsMode(
|
||||
QCommandLineParser::ParseAsOptions);
|
||||
QCommandLineOption titleOption({"t", "title"},
|
||||
"The title of the " % command, "title");
|
||||
QCommandLineOption durationOption(
|
||||
{"d", "duration"}, "The duration of the " % command, "duration");
|
||||
QCommandLineOption pointsOption({"p", "points"},
|
||||
"The channel points per vote", "points");
|
||||
QCommandLineOption choiceOption(
|
||||
{"c", "choice"}, "A viewer-selectable choice for the " % command,
|
||||
"choice");
|
||||
parser.addOptions({
|
||||
titleOption,
|
||||
durationOption,
|
||||
choiceOption,
|
||||
pointsOption,
|
||||
});
|
||||
const auto joined = ctx.words.join(" ");
|
||||
parser.parse(QProcess::splitCommand(joined));
|
||||
|
||||
// Input validation
|
||||
if (!parser.isSet(titleOption))
|
||||
{
|
||||
return makeUnexpected("Missing title - " % usage);
|
||||
}
|
||||
|
||||
if (!parser.isSet(durationOption))
|
||||
{
|
||||
return makeUnexpected("Missing duration - " % usage);
|
||||
}
|
||||
const auto dur = parseDurationToSeconds(parser.value(durationOption));
|
||||
if (dur <= 0)
|
||||
{
|
||||
return makeUnexpected("Invalid duration - " % usage);
|
||||
}
|
||||
const auto duration =
|
||||
std::clamp(std::chrono::seconds(dur), minDuration, maxDuration);
|
||||
|
||||
if (!parser.isSet(choiceOption))
|
||||
{
|
||||
return makeUnexpected("Missing choices - " % usage);
|
||||
}
|
||||
|
||||
// Build action
|
||||
StartUserParticipationAction action{
|
||||
.broadcasterID = ctx.twitchChannel->roomId(),
|
||||
.title = parser.value(titleOption),
|
||||
.choices = parser.values(choiceOption),
|
||||
.duration = duration,
|
||||
};
|
||||
|
||||
if (parser.isSet(pointsOption))
|
||||
{
|
||||
bool validPoints = true;
|
||||
action.pointsPerVote = parser.value(pointsOption).toInt(&validPoints);
|
||||
if (!validPoints)
|
||||
{
|
||||
return makeUnexpected("Invalid points - " % usage);
|
||||
}
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
} // namespace chatterino::commands
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/Expected.hpp"
|
||||
|
||||
#include <nonstd/expected.hpp>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <chrono>
|
||||
#include <ostream>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
@@ -47,6 +51,14 @@ struct PerformChannelAction {
|
||||
}
|
||||
};
|
||||
|
||||
struct StartUserParticipationAction {
|
||||
QString broadcasterID;
|
||||
QString title;
|
||||
QStringList choices;
|
||||
std::chrono::seconds duration;
|
||||
int pointsPerVote = 0;
|
||||
};
|
||||
|
||||
std::ostream &operator<<(std::ostream &os, const IncompleteHelixUser &u);
|
||||
// gtest printer
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
@@ -56,4 +68,8 @@ nonstd::expected<std::vector<PerformChannelAction>, QString> parseChannelAction(
|
||||
const CommandContext &ctx, const QString &command, const QString &usage,
|
||||
bool withDuration, bool withReason);
|
||||
|
||||
ExpectedStr<StartUserParticipationAction> parseUserParticipationAction(
|
||||
const CommandContext &ctx, const QString &command, const QString &usage,
|
||||
std::chrono::seconds minDuration, std::chrono::seconds maxDuration);
|
||||
|
||||
} // namespace chatterino::commands
|
||||
|
||||
Reference in New Issue
Block a user