feat: add /endpoll and /cancelpoll commands for broadcasters (#6605)
Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
- Minor: Added a menu action to sort tabs alphabetically. (#6551)
|
- Minor: Added a menu action to sort tabs alphabetically. (#6551)
|
||||||
- Minor: Fixed "edit hotkey" dialog opening like a normal window. (#6540)
|
- Minor: Fixed "edit hotkey" dialog opening like a normal window. (#6540)
|
||||||
- Minor: Added a setting to show the stream title in live messages. (#6572)
|
- Minor: Added a setting to show the stream title in live messages. (#6572)
|
||||||
- Minor: Added broadcaster-only `/poll` command to start a poll. (#6583)
|
- Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
|
||||||
- Minor: Added broadcaster-only `/prediction` command to start a prediction. (#6583)
|
- Minor: Added broadcaster-only `/prediction` command to start a prediction. (#6583)
|
||||||
- Bugfix: Expose the "Extra extension IDs" setting on non-Windows systems too. (#6509)
|
- Bugfix: Expose the "Extra extension IDs" setting on non-Windows systems too. (#6509)
|
||||||
- Bugfix: Fixed some commands and filters not working as expected in seach popups. (#6539)
|
- Bugfix: Fixed some commands and filters not working as expected in seach popups. (#6539)
|
||||||
|
|||||||
@@ -437,6 +437,20 @@ public:
|
|||||||
FailureCallback<QString> failureCallback),
|
FailureCallback<QString> failureCallback),
|
||||||
(override));
|
(override));
|
||||||
|
|
||||||
|
// get polls
|
||||||
|
MOCK_METHOD(void, getPolls,
|
||||||
|
(QString broadcasterID, QStringList ids, int first,
|
||||||
|
QString after, ResultCallback<HelixPolls> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback),
|
||||||
|
(override));
|
||||||
|
|
||||||
|
// end poll
|
||||||
|
MOCK_METHOD(void, endPoll,
|
||||||
|
(QString broadcasterID, QString id, bool immediatelyHide,
|
||||||
|
ResultCallback<HelixPoll> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback),
|
||||||
|
(override));
|
||||||
|
|
||||||
// create prediction
|
// create prediction
|
||||||
MOCK_METHOD(void, createPrediction,
|
MOCK_METHOD(void, createPrediction,
|
||||||
(QString broadcasterID, QString title, QStringList outcomes,
|
(QString broadcasterID, QString title, QStringList outcomes,
|
||||||
|
|||||||
@@ -484,6 +484,9 @@ CommandController::CommandController(const Paths &paths)
|
|||||||
this->registerCommand("/shoutout", &commands::sendShoutout);
|
this->registerCommand("/shoutout", &commands::sendShoutout);
|
||||||
|
|
||||||
this->registerCommand("/poll", &commands::createPoll);
|
this->registerCommand("/poll", &commands::createPoll);
|
||||||
|
this->registerCommand("/cancelpoll", &commands::cancelPoll);
|
||||||
|
this->registerCommand("/endpoll", &commands::endPoll);
|
||||||
|
|
||||||
this->registerCommand("/prediction", &commands::createPrediction);
|
this->registerCommand("/prediction", &commands::createPrediction);
|
||||||
|
|
||||||
this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules);
|
this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules);
|
||||||
|
|||||||
@@ -67,4 +67,164 @@ QString createPoll(const CommandContext &ctx)
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString endPoll(const CommandContext &ctx)
|
||||||
|
{
|
||||||
|
if (ctx.twitchChannel == nullptr)
|
||||||
|
{
|
||||||
|
const auto err = QStringLiteral(
|
||||||
|
"The /endpoll command only works in Twitch channels");
|
||||||
|
if (ctx.channel != nullptr)
|
||||||
|
{
|
||||||
|
ctx.channel->addSystemMessage(err);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoCommands) << "Invalid command context:" << err;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
||||||
|
if (currentUser->isAnon())
|
||||||
|
{
|
||||||
|
ctx.channel->addSystemMessage("You must be logged in to end a poll!");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto roomId = ctx.twitchChannel->roomId();
|
||||||
|
getHelix()->getPolls(
|
||||||
|
roomId, {}, 1, {},
|
||||||
|
[channel = ctx.channel, roomId](const auto &result) {
|
||||||
|
if (result.polls.empty())
|
||||||
|
{
|
||||||
|
channel->addSystemMessage("Failed to find any polls");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto poll = result.polls.front();
|
||||||
|
if (poll.status != "ACTIVE")
|
||||||
|
{
|
||||||
|
channel->addSystemMessage("Could not find an active poll");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHelix()->endPoll(
|
||||||
|
roomId, poll.id, false,
|
||||||
|
[channel](const HelixPoll &data) {
|
||||||
|
// find most popular choice
|
||||||
|
HelixPollChoice winner = data.choices.front();
|
||||||
|
int totalVotes = 0;
|
||||||
|
int winnerCount = 0;
|
||||||
|
for (const auto &choice : data.choices)
|
||||||
|
{
|
||||||
|
totalVotes += choice.votes;
|
||||||
|
if (choice.votes > winner.votes)
|
||||||
|
{
|
||||||
|
winner = choice;
|
||||||
|
winnerCount = 1;
|
||||||
|
}
|
||||||
|
else if (choice.votes == winner.votes)
|
||||||
|
{
|
||||||
|
winnerCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalVotes == 0)
|
||||||
|
{
|
||||||
|
channel->addSystemMessage(
|
||||||
|
QString("Poll ended with zero votes: '%1'")
|
||||||
|
.arg(data.title));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (winnerCount > 1)
|
||||||
|
{
|
||||||
|
channel->addSystemMessage(
|
||||||
|
QString("Poll ended in a draw: '%1'")
|
||||||
|
.arg(data.title));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double percent =
|
||||||
|
100.0 * winner.votes / std::max(totalVotes, 1);
|
||||||
|
|
||||||
|
channel->addSystemMessage(
|
||||||
|
QString(
|
||||||
|
"Ended poll: '%1' - '%2' won with %3 votes (%4%)")
|
||||||
|
.arg(data.title, winner.title,
|
||||||
|
QString::number(winner.votes),
|
||||||
|
QString::number(percent, 'f', 1)));
|
||||||
|
},
|
||||||
|
[channel](const auto &error) {
|
||||||
|
channel->addSystemMessage("Failed to end the poll - " +
|
||||||
|
error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[channel = ctx.channel](const auto &error) {
|
||||||
|
channel->addSystemMessage("Failed to query polls - " + error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString cancelPoll(const CommandContext &ctx)
|
||||||
|
{
|
||||||
|
if (ctx.twitchChannel == nullptr)
|
||||||
|
{
|
||||||
|
const auto err = QStringLiteral(
|
||||||
|
"The /cancelpoll command only works in Twitch channels");
|
||||||
|
if (ctx.channel != nullptr)
|
||||||
|
{
|
||||||
|
ctx.channel->addSystemMessage(err);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoCommands) << "Invalid command context:" << err;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
||||||
|
if (currentUser->isAnon())
|
||||||
|
{
|
||||||
|
ctx.channel->addSystemMessage(
|
||||||
|
"You must be logged in to cancel a poll!");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto roomId = ctx.twitchChannel->roomId();
|
||||||
|
getHelix()->getPolls(
|
||||||
|
roomId, {}, 1, {},
|
||||||
|
[channel = ctx.channel, roomId](const auto &result) {
|
||||||
|
if (result.polls.empty())
|
||||||
|
{
|
||||||
|
channel->addSystemMessage("Failed to find any polls");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto poll = result.polls.front();
|
||||||
|
if (poll.status != "ACTIVE")
|
||||||
|
{
|
||||||
|
channel->addSystemMessage("Could not find an active poll");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHelix()->endPoll(
|
||||||
|
roomId, poll.id, true,
|
||||||
|
[channel](const HelixPoll &data) {
|
||||||
|
channel->addSystemMessage(
|
||||||
|
QString("Canceled poll: '%1'").arg(data.title));
|
||||||
|
},
|
||||||
|
[channel](const auto &error) {
|
||||||
|
channel->addSystemMessage("Failed to cancel the poll - " +
|
||||||
|
error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[channel = ctx.channel](const auto &error) {
|
||||||
|
channel->addSystemMessage("Failed to query polls - " + error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino::commands
|
} // namespace chatterino::commands
|
||||||
|
|||||||
@@ -13,4 +13,10 @@ namespace chatterino::commands {
|
|||||||
/// /poll
|
/// /poll
|
||||||
QString createPoll(const CommandContext &ctx);
|
QString createPoll(const CommandContext &ctx);
|
||||||
|
|
||||||
|
/// /endpoll
|
||||||
|
QString endPoll(const CommandContext &ctx);
|
||||||
|
|
||||||
|
/// /cancelpoll
|
||||||
|
QString cancelPoll(const CommandContext &ctx);
|
||||||
|
|
||||||
} // namespace chatterino::commands
|
} // namespace chatterino::commands
|
||||||
|
|||||||
@@ -3276,6 +3276,103 @@ void Helix::createPoll(QString broadcasterID, QString title,
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Helix::getPolls(const QString broadcasterID, QStringList ids,
|
||||||
|
const int first, const QString after,
|
||||||
|
ResultCallback<HelixPolls> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback)
|
||||||
|
{
|
||||||
|
QUrlQuery urlQuery;
|
||||||
|
urlQuery.addQueryItem("broadcaster_id", broadcasterID);
|
||||||
|
urlQuery.addQueryItem("first", QString::number(first));
|
||||||
|
|
||||||
|
if (!after.isEmpty())
|
||||||
|
{
|
||||||
|
urlQuery.addQueryItem("after", after);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &id : ids)
|
||||||
|
{
|
||||||
|
urlQuery.addQueryItem("id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->makeGet("polls", urlQuery)
|
||||||
|
.onSuccess([successCallback](const auto &result) {
|
||||||
|
if (result.status() != 200)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Success result for getting polls was "
|
||||||
|
<< result.formatError() << "but we expected it to be 200";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto response = result.parseJson();
|
||||||
|
successCallback(HelixPolls(response));
|
||||||
|
})
|
||||||
|
.onError([failureCallback](const auto &result) -> void {
|
||||||
|
if (!result.status())
|
||||||
|
{
|
||||||
|
failureCallback(result.formatError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto obj = result.parseJson();
|
||||||
|
auto message = obj.value("message").toString();
|
||||||
|
if (!message.isEmpty())
|
||||||
|
{
|
||||||
|
failureCallback(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failureCallback(result.formatError());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Helix::endPoll(const QString broadcasterID, const QString id,
|
||||||
|
const bool immediatelyHide,
|
||||||
|
ResultCallback<HelixPoll> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback)
|
||||||
|
{
|
||||||
|
QJsonObject payload;
|
||||||
|
payload.insert("broadcaster_id", broadcasterID);
|
||||||
|
payload.insert("id", id);
|
||||||
|
payload.insert("status", immediatelyHide ? "ARCHIVED" : "TERMINATED");
|
||||||
|
|
||||||
|
this->makePatch("polls", {})
|
||||||
|
.json(payload)
|
||||||
|
.onSuccess([successCallback](const NetworkResult &result) {
|
||||||
|
if (result.status() != 200)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Success result for ending a poll was "
|
||||||
|
<< result.formatError() << "but we expected it to be 200";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto response = result.parseJson();
|
||||||
|
const auto data = HelixPolls(response);
|
||||||
|
successCallback(data.polls.front());
|
||||||
|
})
|
||||||
|
.onError([failureCallback](const NetworkResult &result) -> void {
|
||||||
|
if (!result.status())
|
||||||
|
{
|
||||||
|
failureCallback(result.formatError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto obj = result.parseJson();
|
||||||
|
const auto message = obj.value("message").toString();
|
||||||
|
if (!message.isEmpty())
|
||||||
|
{
|
||||||
|
failureCallback(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failureCallback(result.formatError());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
void Helix::createPrediction(const QString broadcasterID, const QString title,
|
void Helix::createPrediction(const QString broadcasterID, const QString title,
|
||||||
QStringList outcomes,
|
QStringList outcomes,
|
||||||
const std::chrono::seconds duration,
|
const std::chrono::seconds duration,
|
||||||
|
|||||||
@@ -464,6 +464,55 @@ struct HelixSendMessageArgs {
|
|||||||
QString replyParentMessageID;
|
QString replyParentMessageID;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct HelixPollChoice {
|
||||||
|
QString id;
|
||||||
|
QString title;
|
||||||
|
int votes;
|
||||||
|
|
||||||
|
explicit HelixPollChoice(const QJsonObject &jsonObject)
|
||||||
|
: id(jsonObject.value("id").toString())
|
||||||
|
, title(jsonObject.value("title").toString())
|
||||||
|
, votes(jsonObject.value("votes").toInt())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HelixPoll {
|
||||||
|
QString id;
|
||||||
|
QString title;
|
||||||
|
std::vector<HelixPollChoice> choices;
|
||||||
|
QString status;
|
||||||
|
|
||||||
|
explicit HelixPoll(const QJsonObject &jsonObject)
|
||||||
|
: id(jsonObject.value("id").toString())
|
||||||
|
, title(jsonObject.value("title").toString())
|
||||||
|
, status(jsonObject.value("status").toString())
|
||||||
|
{
|
||||||
|
for (const auto &data = jsonObject.value("choices").toArray();
|
||||||
|
const auto &c : data)
|
||||||
|
{
|
||||||
|
HelixPollChoice choice(c.toObject());
|
||||||
|
this->choices.push_back(choice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HelixPolls {
|
||||||
|
std::vector<HelixPoll> polls;
|
||||||
|
|
||||||
|
HelixPolls() = default;
|
||||||
|
|
||||||
|
explicit HelixPolls(const QJsonObject &jsonObject)
|
||||||
|
{
|
||||||
|
for (const auto &data = jsonObject.value("data").toArray();
|
||||||
|
const auto &p : data)
|
||||||
|
{
|
||||||
|
HelixPoll poll(p.toObject());
|
||||||
|
this->polls.push_back(poll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
enum class HelixAnnouncementColor {
|
enum class HelixAnnouncementColor {
|
||||||
Blue,
|
Blue,
|
||||||
Green,
|
Green,
|
||||||
@@ -1218,6 +1267,18 @@ public:
|
|||||||
int pointsPerVote, ResultCallback<> successCallback,
|
int pointsPerVote, ResultCallback<> successCallback,
|
||||||
FailureCallback<QString> failureCallback) = 0;
|
FailureCallback<QString> failureCallback) = 0;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference#get-polls
|
||||||
|
virtual void getPolls(QString broadcasterID, QStringList ids, int first,
|
||||||
|
QString after,
|
||||||
|
ResultCallback<HelixPolls> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback) = 0;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference#end-poll
|
||||||
|
virtual void endPoll(QString broadcasterID, QString id,
|
||||||
|
bool immediatelyHide,
|
||||||
|
ResultCallback<HelixPoll> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback) = 0;
|
||||||
|
|
||||||
/// https://dev.twitch.tv/docs/api/reference#create-prediction
|
/// https://dev.twitch.tv/docs/api/reference#create-prediction
|
||||||
virtual void createPrediction(QString broadcasterID, QString title,
|
virtual void createPrediction(QString broadcasterID, QString title,
|
||||||
QStringList choices,
|
QStringList choices,
|
||||||
@@ -1586,6 +1647,16 @@ public:
|
|||||||
ResultCallback<> successCallback,
|
ResultCallback<> successCallback,
|
||||||
FailureCallback<QString> failureCallback) final;
|
FailureCallback<QString> failureCallback) final;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference#get-polls
|
||||||
|
void getPolls(QString broadcasterID, QStringList ids, int first,
|
||||||
|
QString after, ResultCallback<HelixPolls> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback) final;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference#end-poll
|
||||||
|
void endPoll(QString broadcasterID, QString id, bool immediatelyHide,
|
||||||
|
ResultCallback<HelixPoll> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback) final;
|
||||||
|
|
||||||
/// https://dev.twitch.tv/docs/api/reference#create-prediction
|
/// https://dev.twitch.tv/docs/api/reference#create-prediction
|
||||||
void createPrediction(QString broadcasterID, QString title,
|
void createPrediction(QString broadcasterID, QString title,
|
||||||
QStringList outcomes, std::chrono::seconds duration,
|
QStringList outcomes, std::chrono::seconds duration,
|
||||||
|
|||||||
Reference in New Issue
Block a user