Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add /warn command #5474

Merged
merged 7 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Minor: Moderators can now see when users are warned. (#5441)
- Minor: Added support for Brave & google-chrome-stable browsers. (#5452)
- Minor: Added drop indicator line while dragging in tables. (#5256)
- Minor: Added `/warn <username> <reason>` command for mods. This prevents the user from chatting until they acknowledge the warning. (#5474)
- Minor: Introduce HTTP API for plugins. (#5383)
- Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426)
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
Expand Down
10 changes: 10 additions & 0 deletions mocks/include/mocks/Helix.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,16 @@ class Helix : public IHelix
(FailureCallback<HelixBanUserError, QString> failureCallback)),
(override)); // /timeout, /ban

// /warn
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
iProdigy marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: invalid case style for function 'MOCK_METHOD' [readability-identifier-naming]

Suggested change
MOCK_METHOD(
mockMethod(

void, warnUser,
(QString broadcasterID, QString moderatorID, QString userID,
QString reason, ResultCallback<> successCallback,
(FailureCallback<HelixWarnUserError, QString> failureCallback)),
(override)); // /warn

// /w
// The extra parenthesis around the failure callback is because its type
// contains a comma
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ set(SOURCE_FILES
controllers/commands/builtin/twitch/UpdateChannel.hpp
controllers/commands/builtin/twitch/UpdateColor.cpp
controllers/commands/builtin/twitch/UpdateColor.hpp
controllers/commands/builtin/twitch/Warn.cpp
controllers/commands/builtin/twitch/Warn.hpp
controllers/commands/common/ChannelAction.cpp
controllers/commands/common/ChannelAction.hpp
controllers/commands/CommandContext.hpp
Expand Down
3 changes: 3 additions & 0 deletions src/controllers/commands/CommandController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "controllers/commands/builtin/twitch/Unban.hpp"
#include "controllers/commands/builtin/twitch/UpdateChannel.hpp"
#include "controllers/commands/builtin/twitch/UpdateColor.hpp"
#include "controllers/commands/builtin/twitch/Warn.hpp"
#include "controllers/commands/Command.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "controllers/commands/CommandModel.hpp"
Expand Down Expand Up @@ -439,6 +440,8 @@ void CommandController::initialize(Settings &, const Paths &paths)
this->registerCommand("/ban", &commands::sendBan);
this->registerCommand("/banid", &commands::sendBanById);

this->registerCommand("/warn", &commands::sendWarn);

for (const auto &cmd : TWITCH_WHISPER_COMMANDS)
{
this->registerCommand(cmd, &commands::sendWhisper);
Expand Down
199 changes: 199 additions & 0 deletions src/controllers/commands/builtin/twitch/Warn.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#include "controllers/commands/builtin/twitch/Warn.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 "messages/MessageBuilder.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"

namespace {

using namespace chatterino;

void warnUserByID(const ChannelPtr &channel, const QString &channelID,
const QString &sourceUserID, const QString &targetUserID,
const QString &reason, const QString &displayName)
{
using Error = HelixWarnUserError;

getHelix()->warnUser(
channelID, sourceUserID, targetUserID, reason,
[] {
// No response for warns, they're emitted over pubsub instead
},
[channel, displayName](auto error, auto message) {
QString errorMessage = QString("Failed to warn user - ");
switch (error)
{
case Error::ConflictingOperation: {
errorMessage += "There was a conflicting warn operation on "
"this user. Please try again.";
}
break;

case Error::Forwarded: {
errorMessage += message;
}
break;

case Error::Ratelimited: {
errorMessage += "You are being ratelimited by Twitch. Try "
"again in a few seconds.";
}
break;

case Error::CannotWarnUser: {
errorMessage +=
QString("You cannot warn %1.").arg(displayName);
}
break;

case Error::UserMissingScope: {
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
errorMessage += "Missing required scope. "
"Re-login with your "
"account and try again.";
}
break;

case Error::UserNotAuthorized: {
// TODO(pajlada): Phrase MISSING_PERMISSION
errorMessage += "You don't have permission to "
"perform that action.";
}
break;

case Error::Unknown: {
errorMessage += "An unknown error has occurred.";
}
break;
}

channel->addMessage(makeSystemMessage(errorMessage));
});
}

} // namespace

namespace chatterino::commands {

QString sendWarn(const CommandContext &ctx)
{
const auto command = QStringLiteral("/warn");
const auto usage = QStringLiteral(
R"(Usage: "/warn [options...] <username> <reason>" - Warn a user via their username. Reason is required and will be shown to the target user and other moderators. Options: --channel <channel> to override which channel the warn takes place in (can be specified multiple times).)");
const auto actions = parseChannelAction(ctx, command, usage, false, true);

if (!actions.has_value())
{
if (ctx.channel != nullptr)
{
ctx.channel->addMessage(makeSystemMessage(actions.error()));
}
else
{
qCWarning(chatterinoCommands)
<< "Error parsing command:" << actions.error();
}

return "";
}

assert(!actions.value().empty());

auto currentUser = getIApp()->getAccounts()->twitch.getCurrent();
if (currentUser->isAnon())
{
ctx.channel->addMessage(
makeSystemMessage("You must be logged in to warn someone!"));
return "";
}

for (const auto &action : actions.value())
{
const auto &reason = action.reason;
if (reason.isEmpty())
{
ctx.channel->addMessage(
makeSystemMessage("Failed to warn, you must specify a reason"));
break;
}

QStringList userLoginsToFetch;
QStringList userIDs;
if (action.target.id.isEmpty())
{
assert(!action.target.login.isEmpty() &&
"Warn Action target username AND user ID may not be "
"empty at the same time");
userLoginsToFetch.append(action.target.login);
}
else
{
// For hydration
userIDs.append(action.target.id);
}
if (action.channel.id.isEmpty())
{
assert(!action.channel.login.isEmpty() &&
"Warn Action channel username AND user ID may not be "
"empty at the same time");
userLoginsToFetch.append(action.channel.login);
}
else
{
// For hydration
userIDs.append(action.channel.id);
}

if (!userLoginsToFetch.isEmpty())
{
// At least 1 user ID needs to be resolved before we can take action
// userIDs is filled up with the data we already have to hydrate the action channel & action target
getHelix()->fetchUsers(
userIDs, userLoginsToFetch,
[channel{ctx.channel}, actionChannel{action.channel},
actionTarget{action.target}, currentUser, reason,
userLoginsToFetch](const auto &users) mutable {
if (!actionChannel.hydrateFrom(users))
{
channel->addMessage(makeSystemMessage(
QString("Failed to warn, bad channel name: %1")
.arg(actionChannel.login)));
return;
}
if (!actionTarget.hydrateFrom(users))
{
channel->addMessage(makeSystemMessage(
QString("Failed to warn, bad target name: %1")
.arg(actionTarget.login)));
return;
}

warnUserByID(channel, actionChannel.id,
currentUser->getUserId(), actionTarget.id,
reason, actionTarget.displayName);
},
[channel{ctx.channel}, userLoginsToFetch] {
channel->addMessage(makeSystemMessage(
QString("Failed to warn, bad username(s): %1")
.arg(userLoginsToFetch.join(", "))));
});
}
else
{
// If both IDs are available, we do no hydration & just use the id as the display name
warnUserByID(ctx.channel, action.channel.id,
currentUser->getUserId(), action.target.id, reason,
action.target.id);
}
}

return "";
}

} // namespace chatterino::commands
16 changes: 16 additions & 0 deletions src/controllers/commands/builtin/twitch/Warn.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

class QString;

namespace chatterino {

struct CommandContext;

} // namespace chatterino

namespace chatterino::commands {

/// /warn
QString sendWarn(const CommandContext &ctx);

} // namespace chatterino::commands
1 change: 1 addition & 0 deletions src/providers/twitch/TwitchCommon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ static const QStringList TWITCH_DEFAULT_COMMANDS{
"delete",
"announce",
"requests",
"warn",
};

static const QStringList TWITCH_WHISPER_COMMANDS{"/w", ".w"};
Expand Down
Loading
Loading