feat: allow timeout-related commands to be used in multiple channels (#5402)
This changes the behaviour of the following commands: - `/ban` - `/timeout` - `/untimeout` - `/unban` All of those commands now accept one or more `--channel` parameters to override which channel the action should take place in. The `--channel` parameter accepts a channel ID or channel name with the same syntax as the other "user targets" do (e.g. `id:11148817` or `pajlada`) examples Ban user in the chat you're typing in: `/ban weeb123` Ban user in the chat you're typing in, with a reason specified: `/ban weeb123 the ban reason` Ban user in a separate chat, with a reason specified: `/ban --channel pajlada weeb123 the ban reason` Ban user in two separate chats, with a reason specified: `/ban --channel pajlada --channel id:117166826 weeb123 the ban reason` Timeout user in the chat you're typing in: `/timeout weeb123` Timeout user in the chat you're typing in, with a reason specified: `/timeout weeb123 10m the timeout reason` Timeout user in a separate chat, with a reason specified: `/timeout --channel pajlada weeb123 10m the timeout reason` Timeout user in two separate chats, with a reason specified: `/timeout --channel pajlada --channel id:117166826 weeb123 10m the timeout reason` Unban user in the chat you're typing in: `/unban weeb123` Unban user in a separate chat: `/unban --channel pajlada weeb123` Unban user in two separate chats: `/unban --channel pajlada --channel id:117166826 weeb123`
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
#include "controllers/commands/builtin/twitch/Ban.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"
|
||||
#include "util/Twitch.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -80,13 +81,12 @@ QString formatBanTimeoutError(const char *operation, HelixBanUserError error,
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
void banUserByID(const ChannelPtr &channel, const TwitchChannel *twitchChannel,
|
||||
void banUserByID(const ChannelPtr &channel, const QString &channelID,
|
||||
const QString &sourceUserID, const QString &targetUserID,
|
||||
const QString &reason, const QString &displayName)
|
||||
{
|
||||
getHelix()->banUser(
|
||||
twitchChannel->roomId(), sourceUserID, targetUserID, std::nullopt,
|
||||
reason,
|
||||
channelID, sourceUserID, targetUserID, std::nullopt, reason,
|
||||
[] {
|
||||
// No response for bans, they're emitted over pubsub/IRC instead
|
||||
},
|
||||
@@ -97,14 +97,13 @@ void banUserByID(const ChannelPtr &channel, const TwitchChannel *twitchChannel,
|
||||
});
|
||||
}
|
||||
|
||||
void timeoutUserByID(const ChannelPtr &channel,
|
||||
const TwitchChannel *twitchChannel,
|
||||
void timeoutUserByID(const ChannelPtr &channel, const QString &channelID,
|
||||
const QString &sourceUserID, const QString &targetUserID,
|
||||
int duration, const QString &reason,
|
||||
const QString &displayName)
|
||||
{
|
||||
getHelix()->banUser(
|
||||
twitchChannel->roomId(), sourceUserID, targetUserID, duration, reason,
|
||||
channelID, sourceUserID, targetUserID, duration, reason,
|
||||
[] {
|
||||
// No response for timeouts, they're emitted over pubsub/IRC instead
|
||||
},
|
||||
@@ -121,63 +120,108 @@ namespace chatterino::commands {
|
||||
|
||||
QString sendBan(const CommandContext &ctx)
|
||||
{
|
||||
const auto &words = ctx.words;
|
||||
const auto &channel = ctx.channel;
|
||||
const auto *twitchChannel = ctx.twitchChannel;
|
||||
const auto command = QStringLiteral("/ban");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/ban [options...] <username> [reason]" - Permanently prevent a user from chatting via their username. Reason is optional and will be shown to the target user and other moderators. Options: --channel <channel> to override which channel the ban takes place in (can be specified multiple times).)");
|
||||
const auto actions = parseChannelAction(ctx, command, usage, false, true);
|
||||
|
||||
if (channel == nullptr)
|
||||
if (!actions.has_value())
|
||||
{
|
||||
if (ctx.channel != nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(actions.error()));
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommands)
|
||||
<< "Error parsing command:" << actions.error();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
if (twitchChannel == nullptr)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("The /ban command only works in Twitch channels.")));
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto *usageStr =
|
||||
"Usage: \"/ban <username> [reason]\" - Permanently prevent a user "
|
||||
"from chatting. Reason is optional and will be shown to the target "
|
||||
"user and other moderators. Use \"/unban\" to remove a ban.";
|
||||
if (words.size() < 2)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(usageStr));
|
||||
return "";
|
||||
}
|
||||
assert(!actions.value().empty());
|
||||
|
||||
auto currentUser = getIApp()->getAccounts()->twitch.getCurrent();
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
channel->addMessage(
|
||||
ctx.channel->addMessage(
|
||||
makeSystemMessage("You must be logged in to ban someone!"));
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto &rawTarget = words.at(1);
|
||||
auto [targetUserName, targetUserID] = parseUserNameOrID(rawTarget);
|
||||
auto reason = words.mid(2).join(' ');
|
||||
for (const auto &action : actions.value())
|
||||
{
|
||||
const auto &reason = action.reason;
|
||||
|
||||
if (!targetUserID.isEmpty())
|
||||
{
|
||||
banUserByID(channel, twitchChannel, currentUser->getUserId(),
|
||||
targetUserID, reason, targetUserID);
|
||||
}
|
||||
else
|
||||
{
|
||||
getHelix()->getUserByName(
|
||||
targetUserName,
|
||||
[channel, currentUser, twitchChannel,
|
||||
reason](const auto &targetUser) {
|
||||
banUserByID(channel, twitchChannel, currentUser->getUserId(),
|
||||
targetUser.id, reason, targetUser.displayName);
|
||||
},
|
||||
[channel, targetUserName{targetUserName}] {
|
||||
// Equivalent error from IRC
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Invalid username: %1").arg(targetUserName)));
|
||||
});
|
||||
QStringList userLoginsToFetch;
|
||||
QStringList userIDs;
|
||||
if (action.target.id.isEmpty())
|
||||
{
|
||||
assert(!action.target.login.isEmpty() &&
|
||||
"Ban 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() &&
|
||||
"Ban 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 ban, bad channel name: %1")
|
||||
.arg(actionChannel.login)));
|
||||
return;
|
||||
}
|
||||
if (!actionTarget.hydrateFrom(users))
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Failed to ban, bad target name: %1")
|
||||
.arg(actionTarget.login)));
|
||||
return;
|
||||
}
|
||||
|
||||
banUserByID(channel, actionChannel.id,
|
||||
currentUser->getUserId(), actionTarget.id,
|
||||
reason, actionTarget.displayName);
|
||||
},
|
||||
[channel{ctx.channel}, userLoginsToFetch] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Failed to ban, 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
|
||||
banUserByID(ctx.channel, action.channel.id,
|
||||
currentUser->getUserId(), action.target.id, reason,
|
||||
action.target.id);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -221,87 +265,117 @@ QString sendBanById(const CommandContext &ctx)
|
||||
auto target = words.at(1);
|
||||
auto reason = words.mid(2).join(' ');
|
||||
|
||||
banUserByID(channel, twitchChannel, currentUser->getUserId(), target,
|
||||
reason, target);
|
||||
banUserByID(channel, twitchChannel->roomId(), currentUser->getUserId(),
|
||||
target, reason, target);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
QString sendTimeout(const CommandContext &ctx)
|
||||
{
|
||||
const auto &words = ctx.words;
|
||||
const auto &channel = ctx.channel;
|
||||
const auto *twitchChannel = ctx.twitchChannel;
|
||||
const auto command = QStringLiteral("/timeout");
|
||||
const auto usage = QStringLiteral(
|
||||
R"(Usage: "/timeout [options...] <username> [duration][time unit] [reason]" - Temporarily prevent a user from chatting. Duration (optional, default=10 minutes) must be a positive integer; time unit (optional, default=s) must be one of s, m, h, d, w; maximum duration is 2 weeks. Combinations like 1d2h are also allowed. Reason is optional and will be shown to the target user and other moderators. Use "/untimeout" to remove a timeout. Options: --channel <channel> to override which channel the timeout takes place in (can be specified multiple times).)");
|
||||
const auto actions = parseChannelAction(ctx, command, usage, true, true);
|
||||
|
||||
if (channel == nullptr)
|
||||
if (!actions.has_value())
|
||||
{
|
||||
if (ctx.channel != nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(actions.error()));
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommands)
|
||||
<< "Error parsing command:" << actions.error();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
if (twitchChannel == nullptr)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("The /timeout command only works in Twitch channels.")));
|
||||
return "";
|
||||
}
|
||||
const auto *usageStr =
|
||||
"Usage: \"/timeout <username> [duration][time unit] [reason]\" - "
|
||||
"Temporarily prevent a user from chatting. Duration (optional, "
|
||||
"default=10 minutes) must be a positive integer; time unit "
|
||||
"(optional, default=s) must be one of s, m, h, d, w; maximum "
|
||||
"duration is 2 weeks. Combinations like 1d2h are also allowed. "
|
||||
"Reason is optional and will be shown to the target user and other "
|
||||
"moderators. Use \"/untimeout\" to remove a timeout.";
|
||||
if (words.size() < 2)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(usageStr));
|
||||
return "";
|
||||
}
|
||||
assert(!actions.value().empty());
|
||||
|
||||
auto currentUser = getIApp()->getAccounts()->twitch.getCurrent();
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
channel->addMessage(
|
||||
ctx.channel->addMessage(
|
||||
makeSystemMessage("You must be logged in to timeout someone!"));
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto &rawTarget = words.at(1);
|
||||
auto [targetUserName, targetUserID] = parseUserNameOrID(rawTarget);
|
||||
|
||||
int duration = 10 * 60; // 10min
|
||||
if (words.size() >= 3)
|
||||
for (const auto &action : actions.value())
|
||||
{
|
||||
duration = (int)parseDurationToSeconds(words.at(2));
|
||||
if (duration <= 0)
|
||||
const auto &reason = action.reason;
|
||||
|
||||
QStringList userLoginsToFetch;
|
||||
QStringList userIDs;
|
||||
if (action.target.id.isEmpty())
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(usageStr));
|
||||
return "";
|
||||
assert(!action.target.login.isEmpty() &&
|
||||
"Timeout 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() &&
|
||||
"Timeout 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);
|
||||
}
|
||||
}
|
||||
auto reason = words.mid(3).join(' ');
|
||||
|
||||
if (!targetUserID.isEmpty())
|
||||
{
|
||||
timeoutUserByID(channel, twitchChannel, currentUser->getUserId(),
|
||||
targetUserID, duration, reason, targetUserID);
|
||||
}
|
||||
else
|
||||
{
|
||||
getHelix()->getUserByName(
|
||||
targetUserName,
|
||||
[channel, currentUser, twitchChannel,
|
||||
targetUserName{targetUserName}, duration,
|
||||
reason](const auto &targetUser) {
|
||||
timeoutUserByID(channel, twitchChannel,
|
||||
currentUser->getUserId(), targetUser.id,
|
||||
duration, reason, targetUser.displayName);
|
||||
},
|
||||
[channel, targetUserName{targetUserName}] {
|
||||
// Equivalent error from IRC
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Invalid username: %1").arg(targetUserName)));
|
||||
});
|
||||
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}, duration{action.duration},
|
||||
actionChannel{action.channel}, actionTarget{action.target},
|
||||
currentUser, reason,
|
||||
userLoginsToFetch](const auto &users) mutable {
|
||||
if (!actionChannel.hydrateFrom(users))
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Failed to timeout, bad channel name: %1")
|
||||
.arg(actionChannel.login)));
|
||||
return;
|
||||
}
|
||||
if (!actionTarget.hydrateFrom(users))
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Failed to timeout, bad target name: %1")
|
||||
.arg(actionTarget.login)));
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutUserByID(channel, actionChannel.id,
|
||||
currentUser->getUserId(), actionTarget.id,
|
||||
duration, reason, actionTarget.displayName);
|
||||
},
|
||||
[channel{ctx.channel}, userLoginsToFetch] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Failed to timeout, 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
|
||||
timeoutUserByID(ctx.channel, action.channel.id,
|
||||
currentUser->getUserId(), action.target.id,
|
||||
action.duration, reason, action.target.id);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
#include "controllers/commands/builtin/twitch/Unban.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/builtin/twitch/Ban.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"
|
||||
#include "util/Twitch.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
void unbanUserByID(const ChannelPtr &channel,
|
||||
const TwitchChannel *twitchChannel,
|
||||
void unbanUserByID(const ChannelPtr &channel, const QString &channelID,
|
||||
const QString &sourceUserID, const QString &targetUserID,
|
||||
const QString &displayName)
|
||||
{
|
||||
getHelix()->unbanUser(
|
||||
twitchChannel->roomId(), sourceUserID, targetUserID,
|
||||
channelID, sourceUserID, targetUserID,
|
||||
[] {
|
||||
// No response for unbans, they're emitted over pubsub/IRC instead
|
||||
},
|
||||
@@ -85,26 +86,28 @@ namespace chatterino::commands {
|
||||
|
||||
QString unbanUser(const CommandContext &ctx)
|
||||
{
|
||||
if (ctx.channel == nullptr)
|
||||
const auto command = ctx.words.at(0).toLower();
|
||||
const auto usage =
|
||||
QStringLiteral(
|
||||
R"(Usage: "%1 <username> - Removes a ban on a user. Options: --channel <channel> to override which channel the unban takes place in (can be specified multiple times).)")
|
||||
.arg(command);
|
||||
const auto actions = parseChannelAction(ctx, command, usage, false, false);
|
||||
if (!actions.has_value())
|
||||
{
|
||||
if (ctx.channel != nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(actions.error()));
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommands)
|
||||
<< "Error parsing command:" << actions.error();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
auto commandName = ctx.words.at(0).toLower();
|
||||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
QString("The %1 command only works in Twitch channels.")
|
||||
.arg(commandName)));
|
||||
return "";
|
||||
}
|
||||
if (ctx.words.size() < 2)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
QString("Usage: \"%1 <username>\" - Removes a ban on a user.")
|
||||
.arg(commandName)));
|
||||
return "";
|
||||
}
|
||||
assert(!actions.value().empty());
|
||||
|
||||
auto currentUser = getIApp()->getAccounts()->twitch.getCurrent();
|
||||
if (currentUser->isAnon())
|
||||
@@ -114,29 +117,78 @@ QString unbanUser(const CommandContext &ctx)
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto &rawTarget = ctx.words.at(1);
|
||||
auto [targetUserName, targetUserID] = parseUserNameOrID(rawTarget);
|
||||
for (const auto &action : actions.value())
|
||||
{
|
||||
const auto &reason = action.reason;
|
||||
|
||||
if (!targetUserID.isEmpty())
|
||||
{
|
||||
unbanUserByID(ctx.channel, ctx.twitchChannel, currentUser->getUserId(),
|
||||
targetUserID, targetUserID);
|
||||
}
|
||||
else
|
||||
{
|
||||
getHelix()->getUserByName(
|
||||
targetUserName,
|
||||
[channel{ctx.channel}, currentUser,
|
||||
twitchChannel{ctx.twitchChannel},
|
||||
targetUserName{targetUserName}](const auto &targetUser) {
|
||||
unbanUserByID(channel, twitchChannel, currentUser->getUserId(),
|
||||
targetUser.id, targetUser.displayName);
|
||||
},
|
||||
[channel{ctx.channel}, targetUserName{targetUserName}] {
|
||||
// Equivalent error from IRC
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Invalid username: %1").arg(targetUserName)));
|
||||
});
|
||||
QStringList userLoginsToFetch;
|
||||
QStringList userIDs;
|
||||
if (action.target.id.isEmpty())
|
||||
{
|
||||
assert(!action.target.login.isEmpty() &&
|
||||
"Unban 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() &&
|
||||
"Unban 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 timeout, bad channel name: %1")
|
||||
.arg(actionChannel.login)));
|
||||
return;
|
||||
}
|
||||
if (!actionTarget.hydrateFrom(users))
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Failed to timeout, bad target name: %1")
|
||||
.arg(actionTarget.login)));
|
||||
return;
|
||||
}
|
||||
|
||||
unbanUserByID(channel, actionChannel.id,
|
||||
currentUser->getUserId(), actionTarget.id,
|
||||
actionTarget.displayName);
|
||||
},
|
||||
[channel{ctx.channel}, userLoginsToFetch] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Failed to timeout, 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
|
||||
unbanUserByID(ctx.channel, action.channel.id,
|
||||
currentUser->getUserId(), action.target.id,
|
||||
action.target.id);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#include "controllers/commands/common/ChannelAction.hpp"
|
||||
|
||||
#include "controllers/commands/CommandContext.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/Twitch.hpp"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QStringBuilder>
|
||||
|
||||
#include <algorithm>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
bool IncompleteHelixUser::hydrateFrom(const std::vector<HelixUser> &users)
|
||||
{
|
||||
// Find user in list based on our id or login
|
||||
auto resolvedIt =
|
||||
std::find_if(users.begin(), users.end(), [this](const auto &user) {
|
||||
if (!this->login.isEmpty())
|
||||
{
|
||||
return user.login.compare(this->login, Qt::CaseInsensitive) ==
|
||||
0;
|
||||
}
|
||||
if (!this->id.isEmpty())
|
||||
{
|
||||
return user.id.compare(this->id, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (resolvedIt == users.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const auto &resolved = *resolvedIt;
|
||||
this->id = resolved.id;
|
||||
this->login = resolved.login;
|
||||
this->displayName = resolved.displayName;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &os, const IncompleteHelixUser &u)
|
||||
{
|
||||
os << "{id:" << u.id.toStdString() << ", login:" << u.login.toStdString()
|
||||
<< ", displayName:" << u.displayName.toStdString() << '}';
|
||||
return os;
|
||||
}
|
||||
|
||||
void PrintTo(const PerformChannelAction &a, std::ostream *os)
|
||||
{
|
||||
*os << "{channel:" << a.channel << ", target:" << a.target
|
||||
<< ", reason:" << a.reason.toStdString()
|
||||
<< ", duration:" << std::to_string(a.duration) << '}';
|
||||
}
|
||||
|
||||
nonstd::expected<std::vector<PerformChannelAction>, QString> parseChannelAction(
|
||||
const CommandContext &ctx, const QString &command, const QString &usage,
|
||||
bool withDuration, bool withReason)
|
||||
{
|
||||
if (ctx.channel == nullptr)
|
||||
{
|
||||
// A ban action must be performed with a channel as a context
|
||||
return nonstd::make_unexpected(
|
||||
"A " % command %
|
||||
" action must be performed with a channel as a context");
|
||||
}
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setOptionsAfterPositionalArgumentsMode(
|
||||
QCommandLineParser::ParseAsPositionalArguments);
|
||||
parser.addPositionalArgument("username", "The name of the user to ban");
|
||||
if (withDuration)
|
||||
{
|
||||
parser.addPositionalArgument("duration", "Duration of the action");
|
||||
}
|
||||
if (withReason)
|
||||
{
|
||||
parser.addPositionalArgument("reason", "The optional ban reason");
|
||||
}
|
||||
QCommandLineOption channelOption(
|
||||
"channel", "Override which channel(s) to perform the action in",
|
||||
"channel");
|
||||
parser.addOptions({
|
||||
channelOption,
|
||||
});
|
||||
parser.parse(ctx.words);
|
||||
|
||||
auto positionalArguments = parser.positionalArguments();
|
||||
if (positionalArguments.isEmpty())
|
||||
{
|
||||
return nonstd::make_unexpected("Missing target - " % usage);
|
||||
}
|
||||
|
||||
auto [targetUserName, targetUserID] =
|
||||
parseUserNameOrID(positionalArguments.takeFirst());
|
||||
|
||||
PerformChannelAction base{
|
||||
.target =
|
||||
IncompleteHelixUser{
|
||||
.id = targetUserID,
|
||||
.login = targetUserName,
|
||||
.displayName = "",
|
||||
},
|
||||
.duration = 0,
|
||||
};
|
||||
|
||||
if (withDuration)
|
||||
{
|
||||
if (positionalArguments.isEmpty())
|
||||
{
|
||||
base.duration = 10 * 60; // 10 min
|
||||
}
|
||||
else
|
||||
{
|
||||
auto durationStr = positionalArguments.takeFirst();
|
||||
base.duration = (int)parseDurationToSeconds(durationStr);
|
||||
if (base.duration <= 0)
|
||||
{
|
||||
return nonstd::make_unexpected("Invalid duration - " % usage);
|
||||
}
|
||||
if (withReason)
|
||||
{
|
||||
base.reason = positionalArguments.join(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (withReason)
|
||||
{
|
||||
base.reason = positionalArguments.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<PerformChannelAction> actions;
|
||||
|
||||
auto overrideChannels = parser.values(channelOption);
|
||||
if (overrideChannels.isEmpty())
|
||||
{
|
||||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
return nonstd::make_unexpected(
|
||||
"The " % command % " command only works in Twitch channels");
|
||||
}
|
||||
|
||||
actions.push_back(PerformChannelAction{
|
||||
.channel =
|
||||
{
|
||||
.id = ctx.twitchChannel->roomId(),
|
||||
.login = ctx.twitchChannel->getName(),
|
||||
.displayName = ctx.twitchChannel->getDisplayName(),
|
||||
},
|
||||
.target = base.target,
|
||||
.reason = base.reason,
|
||||
.duration = base.duration,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto &overrideChannelTarget : overrideChannels)
|
||||
{
|
||||
auto [channelUserName, channelUserID] =
|
||||
parseUserNameOrID(overrideChannelTarget);
|
||||
actions.push_back(PerformChannelAction{
|
||||
.channel =
|
||||
{
|
||||
.id = channelUserID,
|
||||
.login = channelUserName,
|
||||
},
|
||||
.target = base.target,
|
||||
.reason = base.reason,
|
||||
.duration = base.duration,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
} // namespace chatterino::commands
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <nonstd/expected.hpp>
|
||||
#include <QString>
|
||||
|
||||
#include <ostream>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct CommandContext;
|
||||
struct HelixUser;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::commands {
|
||||
|
||||
struct IncompleteHelixUser {
|
||||
QString id;
|
||||
QString login;
|
||||
QString displayName;
|
||||
|
||||
bool hydrateFrom(const std::vector<HelixUser> &users);
|
||||
|
||||
bool operator==(const IncompleteHelixUser &other) const
|
||||
{
|
||||
return std::tie(this->id, this->login, this->displayName) ==
|
||||
std::tie(other.id, other.login, other.displayName);
|
||||
}
|
||||
};
|
||||
|
||||
struct PerformChannelAction {
|
||||
// Channel to perform the action in
|
||||
IncompleteHelixUser channel;
|
||||
// Target to perform the action on
|
||||
IncompleteHelixUser target;
|
||||
QString reason;
|
||||
int duration{};
|
||||
|
||||
bool operator==(const PerformChannelAction &other) const
|
||||
{
|
||||
return std::tie(this->channel, this->target, this->reason,
|
||||
this->duration) == std::tie(other.channel, other.target,
|
||||
other.reason,
|
||||
other.duration);
|
||||
}
|
||||
};
|
||||
|
||||
std::ostream &operator<<(std::ostream &os, const IncompleteHelixUser &u);
|
||||
// gtest printer
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
void PrintTo(const PerformChannelAction &a, std::ostream *os);
|
||||
|
||||
nonstd::expected<std::vector<PerformChannelAction>, QString> parseChannelAction(
|
||||
const CommandContext &ctx, const QString &command, const QString &usage,
|
||||
bool withDuration, bool withReason);
|
||||
|
||||
} // namespace chatterino::commands
|
||||
Reference in New Issue
Block a user