feat: add /monitor and /restrict commands for moderators (#6750)
Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
- Minor: Added message read/update methods to the `Channel` plugin API. (#6650)
|
||||
- Minor: Added action to reset `/watching`. (#6759)
|
||||
- Minor: Removed messaging about running flatpak. This is already apparent in the newer Flatpak runtimes. (#6768)
|
||||
- Minor: Add `/(un)monitor` and `/(un)restrict` commands for moderators. (#6750)
|
||||
- Bugfix: Moderation checks now include the lead moderator badge. (#6642)
|
||||
- Bugfix: Fixed lead moderator badges not being filtered by the `Channel` badge setting. (#6665)
|
||||
- Bugfix: Expose the "Extra extension IDs" setting on non-Windows systems too. (#6509)
|
||||
|
||||
@@ -339,6 +339,20 @@ public:
|
||||
(FailureCallback<HelixWarnUserError, QString> failureCallback)),
|
||||
(override)); // /warn
|
||||
|
||||
// /monitor and /restrict
|
||||
MOCK_METHOD(void, addSuspiciousUser,
|
||||
(QString broadcasterID, QString moderatorID, QString userID,
|
||||
bool restricted, ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback),
|
||||
(override)); // /monitor and /restrict
|
||||
|
||||
// /unmonitor and /unrestrict
|
||||
MOCK_METHOD(void, removeSuspiciousUser,
|
||||
(QString broadcasterID, QString moderatorID, QString userID,
|
||||
ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback),
|
||||
(override)); // /unmonitor and /unrestrict
|
||||
|
||||
// /w
|
||||
// The extra parenthesis around the failure callback is because its type
|
||||
// contains a comma
|
||||
|
||||
@@ -99,6 +99,8 @@ set(SOURCE_FILES
|
||||
controllers/commands/builtin/twitch/GetModerators.hpp
|
||||
controllers/commands/builtin/twitch/GetVIPs.cpp
|
||||
controllers/commands/builtin/twitch/GetVIPs.hpp
|
||||
controllers/commands/builtin/twitch/LowTrust.cpp
|
||||
controllers/commands/builtin/twitch/LowTrust.hpp
|
||||
controllers/commands/builtin/twitch/Poll.cpp
|
||||
controllers/commands/builtin/twitch/Poll.hpp
|
||||
controllers/commands/builtin/twitch/Prediction.cpp
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#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/LowTrust.hpp"
|
||||
#include "controllers/commands/builtin/twitch/Poll.hpp"
|
||||
#include "controllers/commands/builtin/twitch/Prediction.hpp"
|
||||
#include "controllers/commands/builtin/twitch/Raid.hpp"
|
||||
@@ -451,6 +452,11 @@ CommandController::CommandController(const Paths &paths)
|
||||
|
||||
this->registerCommand("/warn", &commands::sendWarn);
|
||||
|
||||
this->registerCommand("/monitor", &commands::monitorUser);
|
||||
this->registerCommand("/restrict", &commands::restrictUser);
|
||||
this->registerCommand("/unmonitor", &commands::unmonitorUser);
|
||||
this->registerCommand("/unrestrict", &commands::unrestrictUser);
|
||||
|
||||
for (const auto &cmd : TWITCH_WHISPER_COMMANDS)
|
||||
{
|
||||
this->registerCommand(cmd, &commands::sendWhisper);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "controllers/commands/builtin/twitch/LowTrust.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/CommandContext.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
void addSuspiciousTreatment(const CommandContext &ctx, const QString &command,
|
||||
const QString &usage, bool restrict)
|
||||
{
|
||||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
// This action must be performed with a twitch channel as a context
|
||||
const QString error =
|
||||
"The " % command % " command only works in Twitch channels";
|
||||
if (ctx.channel != nullptr)
|
||||
{
|
||||
ctx.channel->addSystemMessage(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommands) << "Error parsing command:" << error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.words.size() < 2)
|
||||
{
|
||||
ctx.channel->addSystemMessage(usage);
|
||||
return;
|
||||
}
|
||||
|
||||
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
ctx.channel->addSystemMessage("You must be logged in to " % command %
|
||||
" someone!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto roomId = ctx.twitchChannel->roomId();
|
||||
auto modId = currentUser->getUserId();
|
||||
getHelix()->getUserByName(
|
||||
ctx.words.at(1),
|
||||
[chan{ctx.channel}, roomId, modId, command, restrict](const auto &u) {
|
||||
getHelix()->addSuspiciousUser(
|
||||
roomId, modId, u.id, restrict,
|
||||
[] {
|
||||
// treatment notification is handled by eventsub
|
||||
},
|
||||
[chan, command](const auto &err) {
|
||||
chan->addSystemMessage("Failed to " % command % " user - " %
|
||||
err);
|
||||
});
|
||||
},
|
||||
[chan = ctx.channel, command] {
|
||||
chan->addSystemMessage("Failed to query user to " % command);
|
||||
});
|
||||
}
|
||||
|
||||
void removeSuspiciousTreatment(const CommandContext &ctx,
|
||||
const QString &command, const QString &usage)
|
||||
{
|
||||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
// This action must be performed with a twitch channel as a context
|
||||
const QString error =
|
||||
"The " % command % " command only works in Twitch channels";
|
||||
if (ctx.channel != nullptr)
|
||||
{
|
||||
ctx.channel->addSystemMessage(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommands) << "Error parsing command:" << error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.words.size() < 2)
|
||||
{
|
||||
ctx.channel->addSystemMessage(usage);
|
||||
return;
|
||||
}
|
||||
|
||||
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
ctx.channel->addSystemMessage("You must be logged in to " % command %
|
||||
" someone!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto roomId = ctx.twitchChannel->roomId();
|
||||
auto modId = currentUser->getUserId();
|
||||
getHelix()->getUserByName(
|
||||
ctx.words.at(1),
|
||||
[chan{ctx.channel}, roomId, modId, command](const auto &user) {
|
||||
getHelix()->removeSuspiciousUser(
|
||||
roomId, modId, user.id,
|
||||
[] {
|
||||
// treatment notification is handled by eventsub
|
||||
},
|
||||
[chan, command](const auto &err) {
|
||||
chan->addSystemMessage("Failed to " % command % " user - " %
|
||||
err);
|
||||
});
|
||||
},
|
||||
[chan = ctx.channel, command] {
|
||||
chan->addSystemMessage("Failed to query user to " % command);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
QString monitorUser(const CommandContext &ctx)
|
||||
{
|
||||
const auto command = QStringLiteral("/monitor");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/monitor <username>" - Mark a user as monitored.)");
|
||||
|
||||
addSuspiciousTreatment(ctx, command, usage, false);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
QString restrictUser(const CommandContext &ctx)
|
||||
{
|
||||
const auto command = QStringLiteral("/restrict");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/restrict <username>" - Mark a user as restricted.)");
|
||||
|
||||
addSuspiciousTreatment(ctx, command, usage, true);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
QString unmonitorUser(const CommandContext &ctx)
|
||||
{
|
||||
const auto command = QStringLiteral("/unmonitor");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/unmonitor <username>" - Remove a user from suspicious treatment.)");
|
||||
|
||||
removeSuspiciousTreatment(ctx, command, usage);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
QString unrestrictUser(const CommandContext &ctx)
|
||||
{
|
||||
const auto command = QStringLiteral("/unrestrict");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/unrestrict <username>" - Remove a user from suspicious treatment.)");
|
||||
|
||||
removeSuspiciousTreatment(ctx, command, usage);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace chatterino::commands
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
class QString;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct CommandContext;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
/// /monitor
|
||||
QString monitorUser(const CommandContext &ctx);
|
||||
|
||||
/// /restrict
|
||||
QString restrictUser(const CommandContext &ctx);
|
||||
|
||||
/// /unmonitor
|
||||
QString unmonitorUser(const CommandContext &ctx);
|
||||
|
||||
/// /unrestrict
|
||||
QString unrestrictUser(const CommandContext &ctx);
|
||||
|
||||
} // namespace chatterino::commands
|
||||
@@ -2393,6 +2393,98 @@ void Helix::warnUser(
|
||||
.execute();
|
||||
}
|
||||
|
||||
void Helix::addSuspiciousUser(const QString broadcasterID,
|
||||
const QString moderatorID, const QString userID,
|
||||
const bool restricted,
|
||||
ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback)
|
||||
{
|
||||
QUrlQuery urlQuery;
|
||||
|
||||
urlQuery.addQueryItem("broadcaster_id", broadcasterID);
|
||||
urlQuery.addQueryItem("moderator_id", moderatorID);
|
||||
|
||||
QJsonObject payload;
|
||||
payload["user_id"] = userID;
|
||||
payload["status"] = restricted ? "RESTRICTED" : "ACTIVE_MONITORING";
|
||||
|
||||
this->makePost("moderation/suspicious_users", urlQuery)
|
||||
.json(payload)
|
||||
.onSuccess([successCallback](const auto &result) {
|
||||
if (result.status() != 200)
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Success result for treating a suspicious user was"
|
||||
<< result.formatError() << "but we expected it to be 200";
|
||||
}
|
||||
// we don't care about the response
|
||||
successCallback();
|
||||
})
|
||||
.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::removeSuspiciousUser(const QString broadcasterID,
|
||||
const QString moderatorID,
|
||||
const QString userID,
|
||||
ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback)
|
||||
{
|
||||
QUrlQuery urlQuery;
|
||||
|
||||
urlQuery.addQueryItem("broadcaster_id", broadcasterID);
|
||||
urlQuery.addQueryItem("moderator_id", moderatorID);
|
||||
urlQuery.addQueryItem("user_id", userID);
|
||||
|
||||
this->makeDelete("moderation/suspicious_users", urlQuery)
|
||||
.onSuccess([successCallback](const auto &result) {
|
||||
if (result.status() != 200)
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Success result for un-treating a suspicious user was"
|
||||
<< result.formatError() << "but we expected it to be 200";
|
||||
}
|
||||
// we don't care about the response
|
||||
successCallback();
|
||||
})
|
||||
.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();
|
||||
}
|
||||
|
||||
// https://dev.twitch.tv/docs/api/reference#send-whisper
|
||||
void Helix::sendWhisper(
|
||||
QString fromUserID, QString toUserID, QString message,
|
||||
|
||||
@@ -1238,6 +1238,20 @@ public:
|
||||
QString reason, ResultCallback<> successCallback,
|
||||
FailureCallback<HelixWarnUserError, QString> failureCallback) = 0;
|
||||
|
||||
// Monitor or restrict a user
|
||||
// https://dev.twitch.tv/docs/api/reference/#add-suspicious-status-to-chat-user
|
||||
virtual void addSuspiciousUser(
|
||||
QString broadcasterID, QString moderatorID, QString userID,
|
||||
bool restricted, ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback) = 0;
|
||||
|
||||
// Remove a user from monitored or restricted suspicious treatment
|
||||
// https://dev.twitch.tv/docs/api/reference/#remove-suspicious-status-from-chat-user
|
||||
virtual void removeSuspiciousUser(
|
||||
QString broadcasterID, QString moderatorID, QString userID,
|
||||
ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback) = 0;
|
||||
|
||||
// Send a whisper
|
||||
// https://dev.twitch.tv/docs/api/reference#send-whisper
|
||||
virtual void sendWhisper(
|
||||
@@ -1632,6 +1646,19 @@ public:
|
||||
QString reason, ResultCallback<> successCallback,
|
||||
FailureCallback<HelixWarnUserError, QString> failureCallback) final;
|
||||
|
||||
// Monitor or restrict a user
|
||||
// https://dev.twitch.tv/docs/api/reference/#add-suspicious-status-to-chat-user
|
||||
void addSuspiciousUser(QString broadcasterID, QString moderatorID,
|
||||
QString userID, bool restricted,
|
||||
ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback) final;
|
||||
|
||||
// Remove a user from monitored or restricted suspicious treatment
|
||||
// https://dev.twitch.tv/docs/api/reference/#remove-suspicious-status-from-chat-user
|
||||
void removeSuspiciousUser(QString broadcasterID, QString moderatorID,
|
||||
QString userID, ResultCallback<> successCallback,
|
||||
FailureCallback<QString> failureCallback) final;
|
||||
|
||||
// Send a whisper
|
||||
// https://dev.twitch.tv/docs/api/reference#send-whisper
|
||||
void sendWhisper(
|
||||
|
||||
Reference in New Issue
Block a user