2022-12-31 15:41:01 +01:00
|
|
|
#include "controllers/commands/CommandController.hpp"
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "Application.hpp"
|
2022-05-13 21:59:03 +02:00
|
|
|
#include "common/Env.hpp"
|
2023-04-23 00:58:37 +02:00
|
|
|
#include "common/LinkParser.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "common/NetworkResult.hpp"
|
2022-09-16 23:15:28 +02:00
|
|
|
#include "common/QLogging.hpp"
|
2018-06-28 20:25:37 +02:00
|
|
|
#include "common/SignalVector.hpp"
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "controllers/accounts/AccountController.hpp"
|
2023-05-17 23:32:50 +02:00
|
|
|
#include "controllers/commands/builtin/chatterino/Debugging.hpp"
|
Sort and force grouping of includes (#4172)
This change enforces strict include grouping using IncludeCategories
In addition to adding this to the .clang-format file and applying it in the tests/src and src directories, I also did the following small changes:
In ChatterSet.hpp, I changed lrucache to a <>include
In Irc2.hpp, I change common/SignalVector.hpp to a "project-include"
In AttachedWindow.cpp, NativeMessaging.cpp, WindowsHelper.hpp, BaseWindow.cpp, and StreamerMode.cpp, I disabled clang-format for the windows-includes
In WindowDescriptors.hpp, I added the missing vector include. It was previously not needed because the include was handled by another file that was previously included first.
clang-format minimum version has been bumped, so Ubuntu version used in the check-formatting job has been bumped to 22.04 (which is the latest LTS)
2022-11-27 19:32:53 +01:00
|
|
|
#include "controllers/commands/builtin/twitch/ChatSettings.hpp"
|
2023-05-07 12:52:05 +02:00
|
|
|
#include "controllers/commands/builtin/twitch/ShieldMode.hpp"
|
2023-05-20 18:32:06 +02:00
|
|
|
#include "controllers/commands/builtin/twitch/Shoutout.hpp"
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "controllers/commands/Command.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "controllers/commands/CommandContext.hpp"
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "controllers/commands/CommandModel.hpp"
|
2023-04-02 15:31:53 +02:00
|
|
|
#include "controllers/plugins/PluginController.hpp"
|
2022-11-13 18:21:21 +01:00
|
|
|
#include "controllers/userdata/UserDataController.hpp"
|
2023-05-27 14:18:08 +02:00
|
|
|
#include "messages/Image.hpp"
|
2018-08-11 22:23:06 +02:00
|
|
|
#include "messages/Message.hpp"
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "messages/MessageBuilder.hpp"
|
2018-08-11 22:23:06 +02:00
|
|
|
#include "messages/MessageElement.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "messages/MessageThread.hpp"
|
2022-11-18 20:11:56 +01:00
|
|
|
#include "providers/irc/IrcChannel2.hpp"
|
|
|
|
#include "providers/irc/IrcServer.hpp"
|
Sort and force grouping of includes (#4172)
This change enforces strict include grouping using IncludeCategories
In addition to adding this to the .clang-format file and applying it in the tests/src and src directories, I also did the following small changes:
In ChatterSet.hpp, I changed lrucache to a <>include
In Irc2.hpp, I change common/SignalVector.hpp to a "project-include"
In AttachedWindow.cpp, NativeMessaging.cpp, WindowsHelper.hpp, BaseWindow.cpp, and StreamerMode.cpp, I disabled clang-format for the windows-includes
In WindowDescriptors.hpp, I added the missing vector include. It was previously not needed because the include was handled by another file that was previously included first.
clang-format minimum version has been bumped, so Ubuntu version used in the check-formatting job has been bumped to 22.04 (which is the latest LTS)
2022-11-27 19:32:53 +01:00
|
|
|
#include "providers/twitch/api/Helix.hpp"
|
2022-12-18 15:36:39 +01:00
|
|
|
#include "providers/twitch/TwitchAccount.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "providers/twitch/TwitchChannel.hpp"
|
2021-12-26 14:21:52 +01:00
|
|
|
#include "providers/twitch/TwitchCommon.hpp"
|
2019-09-18 13:03:16 +02:00
|
|
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
2022-10-10 12:56:55 +02:00
|
|
|
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
2018-10-26 17:38:17 +02:00
|
|
|
#include "singletons/Emotes.hpp"
|
2018-06-28 19:46:45 +02:00
|
|
|
#include "singletons/Paths.hpp"
|
|
|
|
#include "singletons/Settings.hpp"
|
2019-05-05 16:05:29 +02:00
|
|
|
#include "singletons/Theme.hpp"
|
2020-10-31 16:42:48 +01:00
|
|
|
#include "singletons/WindowManager.hpp"
|
2022-05-23 00:42:52 +02:00
|
|
|
#include "util/Clipboard.hpp"
|
2018-08-02 14:23:27 +02:00
|
|
|
#include "util/CombinePath.hpp"
|
2021-01-30 15:39:01 +01:00
|
|
|
#include "util/FormatTime.hpp"
|
2021-03-06 15:03:33 +01:00
|
|
|
#include "util/Helpers.hpp"
|
2021-06-27 13:40:44 +02:00
|
|
|
#include "util/IncognitoBrowser.hpp"
|
2023-05-27 14:18:08 +02:00
|
|
|
#include "util/PostToThread.hpp"
|
2022-05-14 12:11:39 +02:00
|
|
|
#include "util/Qt.hpp"
|
2022-10-22 12:04:51 +02:00
|
|
|
#include "util/StreamerMode.hpp"
|
Sort and force grouping of includes (#4172)
This change enforces strict include grouping using IncludeCategories
In addition to adding this to the .clang-format file and applying it in the tests/src and src directories, I also did the following small changes:
In ChatterSet.hpp, I changed lrucache to a <>include
In Irc2.hpp, I change common/SignalVector.hpp to a "project-include"
In AttachedWindow.cpp, NativeMessaging.cpp, WindowsHelper.hpp, BaseWindow.cpp, and StreamerMode.cpp, I disabled clang-format for the windows-includes
In WindowDescriptors.hpp, I added the missing vector include. It was previously not needed because the include was handled by another file that was previously included first.
clang-format minimum version has been bumped, so Ubuntu version used in the check-formatting job has been bumped to 22.04 (which is the latest LTS)
2022-11-27 19:32:53 +01:00
|
|
|
#include "util/StreamLink.hpp"
|
2020-05-10 12:45:19 +02:00
|
|
|
#include "util/Twitch.hpp"
|
2022-07-31 12:45:25 +02:00
|
|
|
#include "widgets/dialogs/ReplyThreadPopup.hpp"
|
2020-01-25 12:59:31 +01:00
|
|
|
#include "widgets/dialogs/UserInfoPopup.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "widgets/helper/ChannelView.hpp"
|
2021-03-14 18:25:45 +01:00
|
|
|
#include "widgets/splits/Split.hpp"
|
2022-11-05 13:40:15 +01:00
|
|
|
#include "widgets/splits/SplitContainer.hpp"
|
Sort and force grouping of includes (#4172)
This change enforces strict include grouping using IncludeCategories
In addition to adding this to the .clang-format file and applying it in the tests/src and src directories, I also did the following small changes:
In ChatterSet.hpp, I changed lrucache to a <>include
In Irc2.hpp, I change common/SignalVector.hpp to a "project-include"
In AttachedWindow.cpp, NativeMessaging.cpp, WindowsHelper.hpp, BaseWindow.cpp, and StreamerMode.cpp, I disabled clang-format for the windows-includes
In WindowDescriptors.hpp, I added the missing vector include. It was previously not needed because the include was handled by another file that was previously included first.
clang-format minimum version has been bumped, so Ubuntu version used in the check-formatting job has been bumped to 22.04 (which is the latest LTS)
2022-11-27 19:32:53 +01:00
|
|
|
#include "widgets/Window.hpp"
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
#include <QApplication>
|
2021-05-24 00:24:49 +02:00
|
|
|
#include <QDesktopServices>
|
2018-04-30 23:30:05 +02:00
|
|
|
#include <QFile>
|
|
|
|
#include <QRegularExpression>
|
2021-05-24 00:24:49 +02:00
|
|
|
#include <QUrl>
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
namespace {
|
2022-10-02 15:27:55 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
using namespace chatterino;
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-10-02 15:27:55 +02:00
|
|
|
bool areIRCCommandsStillAvailable()
|
|
|
|
{
|
2022-10-02 23:53:22 +02:00
|
|
|
// 11th of February 2023, 06:00am UTC
|
|
|
|
const QDateTime migrationTime(QDate(2023, 2, 11), QTime(6, 0), Qt::UTC);
|
|
|
|
auto now = QDateTime::currentDateTimeUtc();
|
|
|
|
return now < migrationTime;
|
2022-10-02 15:27:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
QString useIRCCommand(const QStringList &words)
|
|
|
|
{
|
|
|
|
// Reform the original command
|
|
|
|
auto originalCommand = words.join(" ");
|
|
|
|
|
|
|
|
// Replace the / with a . to pass it along to TMI
|
|
|
|
auto newCommand = originalCommand;
|
|
|
|
newCommand.replace(0, 1, ".");
|
|
|
|
|
|
|
|
qCDebug(chatterinoTwitch)
|
|
|
|
<< "Forwarding command" << originalCommand << "as" << newCommand;
|
|
|
|
|
|
|
|
return newCommand;
|
|
|
|
}
|
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
void sendWhisperMessage(const QString &text)
|
|
|
|
{
|
2019-05-07 20:21:08 +02:00
|
|
|
// (hemirt) pajlada: "we should not be sending whispers through jtv, but
|
|
|
|
// rather to your own username"
|
2019-05-05 16:05:29 +02:00
|
|
|
auto app = getApp();
|
2022-01-11 01:18:02 +01:00
|
|
|
QString toSend = text.simplified();
|
|
|
|
|
2022-03-19 12:02:29 +01:00
|
|
|
app->twitch->sendMessage("jtv", toSend);
|
2019-05-05 16:05:29 +02:00
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-07 20:21:08 +02:00
|
|
|
bool appendWhisperMessageWordsLocally(const QStringList &words)
|
2019-05-05 16:05:29 +02:00
|
|
|
{
|
|
|
|
auto app = getApp();
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
MessageBuilder b;
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
b.emplace<TimestampElement>();
|
|
|
|
b.emplace<TextElement>(app->accounts->twitch.getCurrent()->getUserName(),
|
|
|
|
MessageElementFlag::Text, MessageColor::Text,
|
|
|
|
FontStyle::ChatMediumBold);
|
|
|
|
b.emplace<TextElement>("->", MessageElementFlag::Text,
|
|
|
|
getApp()->themes->messages.textColors.system);
|
|
|
|
b.emplace<TextElement>(words[1] + ":", MessageElementFlag::Text,
|
|
|
|
MessageColor::Text, FontStyle::ChatMediumBold);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
const auto &acc = app->accounts->twitch.getCurrent();
|
|
|
|
const auto &accemotes = *acc->accessEmotes();
|
2022-03-19 12:02:29 +01:00
|
|
|
const auto &bttvemotes = app->twitch->getBttvEmotes();
|
|
|
|
const auto &ffzemotes = app->twitch->getFfzEmotes();
|
2019-05-05 16:05:29 +02:00
|
|
|
auto flags = MessageElementFlags();
|
|
|
|
auto emote = boost::optional<EmotePtr>{};
|
|
|
|
for (int i = 2; i < words.length(); i++)
|
|
|
|
{
|
2021-10-17 15:06:58 +02:00
|
|
|
{ // Twitch emote
|
2019-05-05 16:05:29 +02:00
|
|
|
auto it = accemotes.emotes.find({words[i]});
|
|
|
|
if (it != accemotes.emotes.end())
|
|
|
|
{
|
|
|
|
b.emplace<EmoteElement>(it->second,
|
|
|
|
MessageElementFlag::TwitchEmote);
|
|
|
|
continue;
|
|
|
|
}
|
2021-10-17 15:06:58 +02:00
|
|
|
} // Twitch emote
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
{ // bttv/ffz emote
|
|
|
|
if ((emote = bttvemotes.emote({words[i]})))
|
|
|
|
{
|
|
|
|
flags = MessageElementFlag::BttvEmote;
|
|
|
|
}
|
|
|
|
else if ((emote = ffzemotes.emote({words[i]})))
|
|
|
|
{
|
|
|
|
flags = MessageElementFlag::FfzEmote;
|
|
|
|
}
|
|
|
|
if (emote)
|
|
|
|
{
|
|
|
|
b.emplace<EmoteElement>(emote.get(), flags);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} // bttv/ffz emote
|
|
|
|
{ // emoji/text
|
|
|
|
for (auto &variant : app->emotes->emojis.parse(words[i]))
|
|
|
|
{
|
|
|
|
constexpr const static struct {
|
|
|
|
void operator()(EmotePtr emote, MessageBuilder &b) const
|
|
|
|
{
|
|
|
|
b.emplace<EmoteElement>(emote,
|
|
|
|
MessageElementFlag::EmojiAll);
|
|
|
|
}
|
|
|
|
void operator()(const QString &string,
|
|
|
|
MessageBuilder &b) const
|
|
|
|
{
|
2023-04-23 00:58:37 +02:00
|
|
|
LinkParser parser(string);
|
|
|
|
if (parser.result())
|
2019-05-05 16:05:29 +02:00
|
|
|
{
|
2023-04-23 00:58:37 +02:00
|
|
|
b.addLink(*parser.result());
|
2019-05-05 16:05:29 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-04-23 00:58:37 +02:00
|
|
|
b.emplace<TextElement>(string,
|
|
|
|
MessageElementFlag::Text);
|
2019-05-05 16:05:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} visitor;
|
2020-11-08 12:02:19 +01:00
|
|
|
boost::apply_visitor(
|
|
|
|
[&b](auto &&arg) {
|
|
|
|
visitor(arg, b);
|
|
|
|
},
|
|
|
|
variant);
|
2019-05-05 16:05:29 +02:00
|
|
|
} // emoji/text
|
|
|
|
}
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
b->flags.set(MessageFlag::DoNotTriggerNotification);
|
|
|
|
b->flags.set(MessageFlag::Whisper);
|
|
|
|
auto messagexD = b.release();
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-03-19 12:02:29 +01:00
|
|
|
app->twitch->whispersChannel->addMessage(messagexD);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
auto overrideFlags = boost::optional<MessageFlags>(messagexD->flags);
|
|
|
|
overrideFlags->set(MessageFlag::DoNotLog);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-10-22 12:04:51 +02:00
|
|
|
if (getSettings()->inlineWhispers &&
|
|
|
|
!(getSettings()->streamerModeSuppressInlineWhispers &&
|
|
|
|
isInStreamerMode()))
|
2019-05-05 16:05:29 +02:00
|
|
|
{
|
2022-03-19 12:02:29 +01:00
|
|
|
app->twitch->forEachChannel(
|
2019-05-05 16:05:29 +02:00
|
|
|
[&messagexD, overrideFlags](ChannelPtr _channel) {
|
|
|
|
_channel->addMessage(messagexD, overrideFlags);
|
|
|
|
});
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
return true;
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-10-08 13:11:55 +02:00
|
|
|
bool useIrcForWhisperCommand()
|
2019-05-05 16:05:29 +02:00
|
|
|
{
|
2022-10-08 13:11:55 +02:00
|
|
|
switch (getSettings()->helixTimegateWhisper.getValue())
|
|
|
|
{
|
|
|
|
case HelixTimegateOverride::Timegate: {
|
|
|
|
if (areIRCCommandsStillAvailable())
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-10-08 13:11:55 +02:00
|
|
|
// fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseIRC: {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseHelix: {
|
|
|
|
// do nothing and fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString runWhisperCommand(const QStringList &words, const ChannelPtr &channel)
|
|
|
|
{
|
|
|
|
if (words.size() < 3)
|
2019-05-05 16:05:29 +02:00
|
|
|
{
|
2022-10-08 13:11:55 +02:00
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Usage: /w <username> <message>"));
|
|
|
|
return "";
|
2019-05-05 16:05:29 +02:00
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-10-08 13:11:55 +02:00
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to send a whisper!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
auto message = words.mid(2).join(' ');
|
2022-11-18 20:11:56 +01:00
|
|
|
if (channel->isTwitchChannel())
|
2019-05-05 16:05:29 +02:00
|
|
|
{
|
2022-11-18 20:11:56 +01:00
|
|
|
// this covers all twitch channels and twitch-like channels
|
|
|
|
if (useIrcForWhisperCommand())
|
2019-05-05 16:05:29 +02:00
|
|
|
{
|
2022-10-08 13:11:55 +02:00
|
|
|
appendWhisperMessageWordsLocally(words);
|
|
|
|
sendWhisperMessage(words.join(' '));
|
2022-11-18 20:11:56 +01:00
|
|
|
return "";
|
2019-05-05 16:05:29 +02:00
|
|
|
}
|
2022-11-18 20:11:56 +01:00
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[channel, currentUser, target, message,
|
|
|
|
words](const auto &targetUser) {
|
|
|
|
getHelix()->sendWhisper(
|
|
|
|
currentUser->getUserId(), targetUser.id, message,
|
|
|
|
[words] {
|
|
|
|
appendWhisperMessageWordsLocally(words);
|
|
|
|
},
|
|
|
|
[channel, target, targetUser](auto error, auto message) {
|
|
|
|
using Error = HelixWhisperError;
|
2022-10-08 13:11:55 +02:00
|
|
|
|
2022-11-18 20:11:56 +01:00
|
|
|
QString errorMessage = "Failed to send whisper - ";
|
2022-10-08 13:11:55 +02:00
|
|
|
|
2022-11-18 20:11:56 +01:00
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case Error::NoVerifiedPhone: {
|
|
|
|
errorMessage +=
|
|
|
|
"Due to Twitch restrictions, you are now "
|
|
|
|
"required to have a verified phone number "
|
|
|
|
"to send whispers. You can add a phone "
|
|
|
|
"number in Twitch settings. "
|
|
|
|
"https://www.twitch.tv/settings/security";
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::RecipientBlockedUser: {
|
|
|
|
errorMessage +=
|
|
|
|
"The recipient doesn't allow whispers "
|
|
|
|
"from strangers or you directly.";
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::WhisperSelf: {
|
|
|
|
errorMessage += "You cannot whisper yourself.";
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Ratelimited: {
|
|
|
|
errorMessage +=
|
|
|
|
"You may only whisper a maximum of 40 "
|
|
|
|
"unique recipients per day. Within the "
|
|
|
|
"per day limit, you may whisper a "
|
|
|
|
"maximum of 3 whispers per second and "
|
|
|
|
"a maximum of 100 whispers per minute.";
|
|
|
|
}
|
|
|
|
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;
|
2022-10-08 13:11:55 +02:00
|
|
|
}
|
2022-11-18 20:11:56 +01:00
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel] {
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("No user matching that username."));
|
|
|
|
});
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
// we must be on IRC
|
|
|
|
auto *ircChannel = dynamic_cast<IrcChannel *>(channel.get());
|
|
|
|
if (ircChannel == nullptr)
|
|
|
|
{
|
|
|
|
// give up
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
auto *server = ircChannel->server();
|
|
|
|
server->sendWhisper(target, message);
|
2022-10-08 13:11:55 +02:00
|
|
|
return "";
|
2019-05-05 16:05:29 +02:00
|
|
|
}
|
2021-09-11 14:35:26 +02:00
|
|
|
|
2022-06-25 14:06:16 +02:00
|
|
|
using VariableReplacer = std::function<QString(
|
|
|
|
const QString &, const ChannelPtr &, const Message *)>;
|
|
|
|
|
|
|
|
const VariableReplacer NO_OP_PLACEHOLDER =
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
2022-05-29 14:23:29 +02:00
|
|
|
return altText;
|
|
|
|
};
|
|
|
|
|
2022-06-25 14:06:16 +02:00
|
|
|
const std::unordered_map<QString, VariableReplacer> COMMAND_VARS{
|
|
|
|
{
|
|
|
|
"channel.name",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(altText); //unused
|
|
|
|
(void)(message); //unused
|
|
|
|
return channel->getName();
|
2021-09-11 14:35:26 +02:00
|
|
|
},
|
2022-06-25 14:06:16 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"channel.id",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(message); //unused
|
|
|
|
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (tc == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
2021-09-11 14:35:26 +02:00
|
|
|
|
2022-06-25 14:06:16 +02:00
|
|
|
return tc->roomId();
|
2021-09-11 14:35:26 +02:00
|
|
|
},
|
2022-06-25 14:06:16 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
// NOTE: The use of {channel} is deprecated and support for it will drop at some point
|
|
|
|
// Users should be encouraged to use {channel.name} instead.
|
|
|
|
"channel",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(altText); //unused
|
|
|
|
(void)(message); //unused
|
|
|
|
return channel->getName();
|
2021-09-11 14:35:26 +02:00
|
|
|
},
|
2022-06-25 14:06:16 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"stream.game",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(message); //unused
|
|
|
|
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (tc == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
const auto &status = tc->accessStreamStatus();
|
|
|
|
return status->live ? status->game : altText;
|
2021-09-11 14:35:26 +02:00
|
|
|
},
|
2022-06-25 14:06:16 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"stream.title",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(message); //unused
|
|
|
|
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (tc == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
const auto &status = tc->accessStreamStatus();
|
|
|
|
return status->live ? status->title : altText;
|
2021-09-11 14:35:26 +02:00
|
|
|
},
|
2022-06-25 14:06:16 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"my.id",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
(void)(message); //unused
|
|
|
|
auto uid = getApp()->accounts->twitch.getCurrent()->getUserId();
|
|
|
|
return uid.isEmpty() ? altText : uid;
|
2021-09-11 14:35:26 +02:00
|
|
|
},
|
2022-06-25 14:06:16 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"my.name",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
(void)(message); //unused
|
|
|
|
auto name = getApp()->accounts->twitch.getCurrent()->getUserName();
|
|
|
|
return name.isEmpty() ? altText : name;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"user.name",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
if (message == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &v = message->loginName;
|
|
|
|
|
|
|
|
if (v.isEmpty())
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
return v;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// NOTE: The use of {user} is deprecated and support for it will drop at some point
|
|
|
|
// Users should be encouraged to use {user.name} instead.
|
|
|
|
"user",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
if (message == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &v = message->loginName;
|
|
|
|
|
|
|
|
if (v.isEmpty())
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
return v;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"msg.id",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
if (message == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &v = message->id;
|
|
|
|
|
|
|
|
if (v.isEmpty())
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
return v;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// NOTE: The use of {msg-id} is deprecated and support for it will drop at some point
|
|
|
|
// Users should be encouraged to use {msg.id} instead.
|
|
|
|
"msg-id",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
if (message == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &v = message->id;
|
|
|
|
|
|
|
|
if (v.isEmpty())
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
return v;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"msg.text",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
if (message == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &v = message->messageText;
|
|
|
|
|
|
|
|
if (v.isEmpty())
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
return v;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// NOTE: The use of {message} is deprecated and support for it will drop at some point
|
|
|
|
// Users should be encouraged to use {msg.text} instead.
|
|
|
|
"message",
|
|
|
|
[](const auto &altText, const auto &channel, const auto *message) {
|
|
|
|
(void)(channel); //unused
|
|
|
|
if (message == nullptr)
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &v = message->messageText;
|
|
|
|
|
|
|
|
if (v.isEmpty())
|
|
|
|
{
|
|
|
|
return altText;
|
|
|
|
}
|
|
|
|
|
|
|
|
return v;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// variables used in mod buttons and the like, these make no sense in normal commands, so they are left empty
|
|
|
|
{"input.text", NO_OP_PLACEHOLDER},
|
|
|
|
};
|
2021-09-11 14:35:26 +02:00
|
|
|
|
2019-05-05 16:05:29 +02:00
|
|
|
} // namespace
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
namespace chatterino {
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 13:37:09 +01:00
|
|
|
void CommandController::initialize(Settings &, Paths &paths)
|
2018-04-30 23:30:05 +02:00
|
|
|
{
|
2018-11-03 13:37:09 +01:00
|
|
|
// Update commands map when the vector of commands has been updated
|
2018-04-30 23:30:05 +02:00
|
|
|
auto addFirstMatchToMap = [this](auto args) {
|
2020-09-26 10:01:00 +02:00
|
|
|
this->userCommands_.remove(args.item.name);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-12-26 14:21:52 +01:00
|
|
|
for (const Command &cmd : this->items)
|
2018-04-30 23:30:05 +02:00
|
|
|
{
|
|
|
|
if (cmd.name == args.item.name)
|
|
|
|
{
|
2020-09-26 10:01:00 +02:00
|
|
|
this->userCommands_[cmd.name] = cmd;
|
2018-04-30 23:30:05 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 14:52:38 +01:00
|
|
|
int maxSpaces = 0;
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-12-26 14:21:52 +01:00
|
|
|
for (const Command &cmd : this->items)
|
2018-11-03 14:52:38 +01:00
|
|
|
{
|
|
|
|
auto localMaxSpaces = cmd.name.count(' ');
|
|
|
|
if (localMaxSpaces > maxSpaces)
|
|
|
|
{
|
|
|
|
maxSpaces = localMaxSpaces;
|
|
|
|
}
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 14:52:38 +01:00
|
|
|
this->maxSpaces_ = maxSpaces;
|
2018-04-30 23:30:05 +02:00
|
|
|
};
|
2021-12-26 14:21:52 +01:00
|
|
|
this->items.itemInserted.connect(addFirstMatchToMap);
|
|
|
|
this->items.itemRemoved.connect(addFirstMatchToMap);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 13:37:09 +01:00
|
|
|
// Initialize setting manager for commands.json
|
|
|
|
auto path = combinePath(paths.settingsDirectory, "commands.json");
|
|
|
|
this->sm_ = std::make_shared<pajlada::Settings::SettingManager>();
|
2021-12-11 12:56:19 +01:00
|
|
|
this->sm_->setPath(qPrintable(path));
|
2021-08-15 14:33:31 +02:00
|
|
|
this->sm_->setBackupEnabled(true);
|
|
|
|
this->sm_->setBackupSlots(9);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 13:37:09 +01:00
|
|
|
// Delayed initialization of the setting storing all commands
|
|
|
|
this->commandsSetting_.reset(
|
|
|
|
new pajlada::Settings::Setting<std::vector<Command>>("/commands",
|
|
|
|
this->sm_));
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 13:37:09 +01:00
|
|
|
// Update the setting when the vector of commands has been updated (most
|
|
|
|
// likely from the settings dialog)
|
2021-12-26 14:21:52 +01:00
|
|
|
this->items.delayedItemsChanged.connect([this] {
|
|
|
|
this->commandsSetting_->setValue(this->items.raw());
|
2018-11-03 13:37:09 +01:00
|
|
|
});
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 13:37:09 +01:00
|
|
|
// Load commands from commands.json
|
|
|
|
this->sm_->load();
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-11-03 13:37:09 +01:00
|
|
|
// Add loaded commands to our vector of commands (which will update the map
|
|
|
|
// of commands)
|
|
|
|
for (const auto &command : this->commandsSetting_->getValue())
|
2018-04-30 23:30:05 +02:00
|
|
|
{
|
2021-12-26 14:21:52 +01:00
|
|
|
this->items.append(command);
|
2018-04-30 23:30:05 +02:00
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
/// Deprecated commands
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
auto blockLambda = [](const auto &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /block command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2021-02-14 14:01:13 +01:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
2021-08-21 14:41:06 +02:00
|
|
|
channel->addMessage(makeSystemMessage("Usage: /block <user>"));
|
2021-02-14 14:01:13 +01:00
|
|
|
return "";
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
if (currentUser->isAnon())
|
2020-09-26 10:01:00 +02:00
|
|
|
{
|
2021-02-14 14:01:13 +01:00
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to block someone!"));
|
2020-09-26 10:01:00 +02:00
|
|
|
return "";
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
auto target = words.at(1);
|
2022-06-18 13:18:51 +02:00
|
|
|
stripChannelName(target);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[currentUser, channel, target](const HelixUser &targetUser) {
|
|
|
|
getApp()->accounts->twitch.getCurrent()->blockUser(
|
2023-07-01 14:01:47 +02:00
|
|
|
targetUser.id, nullptr,
|
2021-02-14 14:01:13 +01:00
|
|
|
[channel, target, targetUser] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("You successfully blocked user %1")
|
|
|
|
.arg(target)));
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("User %1 couldn't be blocked, an unknown "
|
|
|
|
"error occurred!")
|
|
|
|
.arg(target)));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage(QString("User %1 couldn't be blocked, no "
|
|
|
|
"user with that name found!")
|
|
|
|
.arg(target)));
|
|
|
|
});
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
return "";
|
2021-02-14 14:01:13 +01:00
|
|
|
};
|
2020-09-26 10:01:00 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
auto unblockLambda = [](const auto &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /unblock command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2020-09-26 10:01:00 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
2021-08-21 14:41:06 +02:00
|
|
|
channel->addMessage(makeSystemMessage("Usage: /unblock <user>"));
|
2018-11-03 14:52:38 +01:00
|
|
|
return "";
|
2018-06-05 18:51:14 +02:00
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
if (currentUser->isAnon())
|
2018-04-30 23:30:05 +02:00
|
|
|
{
|
2020-09-26 10:01:00 +02:00
|
|
|
channel->addMessage(
|
2021-02-14 14:01:13 +01:00
|
|
|
makeSystemMessage("You must be logged in to unblock someone!"));
|
2018-11-03 14:52:38 +01:00
|
|
|
return "";
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
auto target = words.at(1);
|
2022-06-18 13:18:51 +02:00
|
|
|
stripChannelName(target);
|
2021-02-14 14:01:13 +01:00
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[currentUser, channel, target](const auto &targetUser) {
|
|
|
|
getApp()->accounts->twitch.getCurrent()->unblockUser(
|
2023-07-01 14:01:47 +02:00
|
|
|
targetUser.id, nullptr,
|
2021-02-14 14:01:13 +01:00
|
|
|
[channel, target, targetUser] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("You successfully unblocked user %1")
|
|
|
|
.arg(target)));
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("User %1 couldn't be unblocked, an unknown "
|
|
|
|
"error occurred!")
|
|
|
|
.arg(target)));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage(QString("User %1 couldn't be unblocked, "
|
|
|
|
"no user with that name found!")
|
|
|
|
.arg(target)));
|
|
|
|
});
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
return "";
|
2021-02-14 14:01:13 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
this->registerCommand(
|
|
|
|
"/ignore", [blockLambda](const auto &words, auto channel) {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Ignore command has been renamed to /block, please use it from "
|
|
|
|
"now on as /ignore is going to be removed soon."));
|
|
|
|
blockLambda(words, channel);
|
2018-11-03 14:52:38 +01:00
|
|
|
return "";
|
2021-02-14 14:01:13 +01:00
|
|
|
});
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
this->registerCommand(
|
|
|
|
"/unignore", [unblockLambda](const auto &words, auto channel) {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Unignore command has been renamed to /unblock, please use it "
|
|
|
|
"from now on as /unignore is going to be removed soon."));
|
|
|
|
unblockLambda(words, channel);
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2021-08-04 22:41:27 +02:00
|
|
|
this->registerCommand("/follow", [](const auto &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
return "";
|
|
|
|
}
|
2021-08-04 22:41:27 +02:00
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Twitch has removed the ability to follow users through "
|
|
|
|
"third-party applications. For more information, see "
|
|
|
|
"https://github.com/Chatterino/chatterino2/issues/3076"));
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand("/unfollow", [](const auto &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
return "";
|
|
|
|
}
|
2021-08-04 22:41:27 +02:00
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Twitch has removed the ability to unfollow users through "
|
|
|
|
"third-party applications. For more information, see "
|
|
|
|
"https://github.com/Chatterino/chatterino2/issues/3076"));
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
/// Supported commands
|
|
|
|
|
|
|
|
this->registerCommand(
|
|
|
|
"/debug-args", [](const auto & /*words*/, auto channel) {
|
|
|
|
QString msg = QApplication::instance()->arguments().join(' ');
|
|
|
|
|
|
|
|
channel->addMessage(makeSystemMessage(msg));
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2022-05-13 21:59:03 +02:00
|
|
|
this->registerCommand("/debug-env", [](const auto & /*words*/,
|
|
|
|
ChannelPtr channel) {
|
|
|
|
auto env = Env::get();
|
|
|
|
|
|
|
|
QStringList debugMessages{
|
|
|
|
"recentMessagesApiUrl: " + env.recentMessagesApiUrl,
|
|
|
|
"linkResolverUrl: " + env.linkResolverUrl,
|
|
|
|
"twitchServerHost: " + env.twitchServerHost,
|
|
|
|
"twitchServerPort: " + QString::number(env.twitchServerPort),
|
|
|
|
"twitchServerSecure: " + QString::number(env.twitchServerSecure),
|
|
|
|
};
|
|
|
|
|
|
|
|
for (QString &str : debugMessages)
|
|
|
|
{
|
|
|
|
MessageBuilder builder;
|
|
|
|
builder.emplace<TimestampElement>(QTime::currentTime());
|
|
|
|
builder.emplace<TextElement>(str, MessageElementFlag::Text,
|
|
|
|
MessageColor::System);
|
|
|
|
channel->addMessage(builder.release());
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
this->registerCommand("/uptime", [](const auto & /*words*/, auto channel) {
|
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
2020-09-26 10:01:00 +02:00
|
|
|
{
|
2021-02-14 14:01:13 +01:00
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /uptime command only works in Twitch Channels"));
|
2018-11-03 14:52:38 +01:00
|
|
|
return "";
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
const auto &streamStatus = twitchChannel->accessStreamStatus();
|
|
|
|
|
|
|
|
QString messageText =
|
|
|
|
streamStatus->live ? streamStatus->uptime : "Channel is not live.";
|
|
|
|
|
|
|
|
channel->addMessage(makeSystemMessage(messageText));
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
return "";
|
|
|
|
});
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
this->registerCommand("/block", blockLambda);
|
|
|
|
|
|
|
|
this->registerCommand("/unblock", unblockLambda);
|
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
this->registerCommand("/user", [](const auto &words, auto channel) {
|
|
|
|
if (words.size() < 2)
|
2018-11-03 14:52:38 +01:00
|
|
|
{
|
2020-09-26 10:01:00 +02:00
|
|
|
channel->addMessage(
|
2021-08-21 14:41:06 +02:00
|
|
|
makeSystemMessage("Usage: /user <user> [channel]"));
|
2018-11-03 14:52:38 +01:00
|
|
|
return "";
|
2018-04-30 23:30:05 +02:00
|
|
|
}
|
2021-08-08 13:23:54 +02:00
|
|
|
QString userName = words[1];
|
|
|
|
stripUserName(userName);
|
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
QString channelName = channel->getName();
|
2021-08-08 13:23:54 +02:00
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
if (words.size() > 2)
|
2019-06-03 17:56:54 +02:00
|
|
|
{
|
2020-09-26 10:01:00 +02:00
|
|
|
channelName = words[2];
|
2021-08-08 13:23:54 +02:00
|
|
|
stripChannelName(channelName);
|
2020-09-26 10:01:00 +02:00
|
|
|
}
|
2021-08-08 13:23:54 +02:00
|
|
|
openTwitchUsercard(channelName, userName);
|
2020-05-10 12:45:19 +02:00
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand("/usercard", [](const auto &words, auto channel) {
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
2021-08-21 13:37:57 +02:00
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Usage: /usercard <user> [channel]"));
|
2019-06-03 17:56:54 +02:00
|
|
|
return "";
|
|
|
|
}
|
2020-09-26 10:01:00 +02:00
|
|
|
|
2021-08-08 13:23:54 +02:00
|
|
|
QString userName = words[1];
|
|
|
|
stripUserName(userName);
|
2021-08-21 13:37:57 +02:00
|
|
|
|
|
|
|
if (words.size() > 2)
|
|
|
|
{
|
|
|
|
QString channelName = words[2];
|
|
|
|
stripChannelName(channelName);
|
|
|
|
|
|
|
|
ChannelPtr channelTemp =
|
2022-03-19 12:02:29 +01:00
|
|
|
getApp()->twitch->getChannelOrEmpty(channelName);
|
2021-08-21 13:37:57 +02:00
|
|
|
|
|
|
|
if (channelTemp->isEmpty())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"A usercard can only be displayed for a channel that is "
|
|
|
|
"currently opened in Chatterino."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
channel = channelTemp;
|
|
|
|
}
|
|
|
|
|
2022-11-05 13:40:15 +01:00
|
|
|
// try to link to current split if possible
|
|
|
|
Split *currentSplit = nullptr;
|
|
|
|
auto *currentPage = dynamic_cast<SplitContainer *>(
|
|
|
|
getApp()->windows->getMainWindow().getNotebook().getSelectedPage());
|
|
|
|
if (currentPage != nullptr)
|
|
|
|
{
|
|
|
|
currentSplit = currentPage->getSelectedSplit();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto differentChannel =
|
|
|
|
currentSplit != nullptr && currentSplit->getChannel() != channel;
|
|
|
|
if (differentChannel || currentSplit == nullptr)
|
|
|
|
{
|
|
|
|
// not possible to use current split, try searching for one
|
|
|
|
const auto ¬ebook =
|
|
|
|
getApp()->windows->getMainWindow().getNotebook();
|
|
|
|
auto count = notebook.getPageCount();
|
|
|
|
for (int i = 0; i < count; i++)
|
|
|
|
{
|
|
|
|
auto *page = notebook.getPageAt(i);
|
|
|
|
auto *container = dynamic_cast<SplitContainer *>(page);
|
|
|
|
assert(container != nullptr);
|
|
|
|
for (auto *split : container->getSplits())
|
|
|
|
{
|
|
|
|
if (split->getChannel() == channel)
|
|
|
|
{
|
|
|
|
currentSplit = split;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This would have crashed either way.
|
|
|
|
assert(currentSplit != nullptr &&
|
|
|
|
"something went HORRIBLY wrong with the /usercard "
|
|
|
|
"command. It couldn't find a split for a channel which "
|
|
|
|
"should be open.");
|
|
|
|
}
|
|
|
|
|
2020-10-31 16:42:48 +01:00
|
|
|
auto *userPopup = new UserInfoPopup(
|
|
|
|
getSettings()->autoCloseUserPopup,
|
2022-07-31 12:45:25 +02:00
|
|
|
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())),
|
2022-11-05 13:40:15 +01:00
|
|
|
currentSplit);
|
2021-08-08 13:23:54 +02:00
|
|
|
userPopup->setData(userName, channel);
|
2020-09-26 10:01:00 +02:00
|
|
|
userPopup->move(QCursor::pos());
|
|
|
|
userPopup->show();
|
|
|
|
return "";
|
|
|
|
});
|
2021-01-16 13:58:11 +01:00
|
|
|
|
2022-05-22 13:53:45 +02:00
|
|
|
this->registerCommand("/requests", [](const QStringList &words,
|
|
|
|
ChannelPtr channel) {
|
|
|
|
QString target(words.value(1));
|
|
|
|
|
|
|
|
if (target.isEmpty())
|
|
|
|
{
|
|
|
|
if (channel->getType() == Channel::Type::Twitch &&
|
|
|
|
!channel->isEmpty())
|
|
|
|
{
|
|
|
|
target = channel->getName();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: /requests [channel]. You can also use the command "
|
|
|
|
"without arguments in any Twitch channel to open its "
|
|
|
|
"channel points requests queue. Only the broadcaster and "
|
|
|
|
"moderators have permission to view the queue."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
stripChannelName(target);
|
|
|
|
QDesktopServices::openUrl(
|
|
|
|
QUrl(QString("https://www.twitch.tv/popout/%1/reward-queue")
|
|
|
|
.arg(target)));
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2023-04-15 12:13:26 +02:00
|
|
|
this->registerCommand("/lowtrust", [](const QStringList &words,
|
|
|
|
ChannelPtr channel) {
|
|
|
|
QString target(words.value(1));
|
|
|
|
|
|
|
|
if (target.isEmpty())
|
|
|
|
{
|
|
|
|
if (channel->getType() == Channel::Type::Twitch &&
|
|
|
|
!channel->isEmpty())
|
|
|
|
{
|
|
|
|
target = channel->getName();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: /lowtrust [channel]. You can also use the command "
|
|
|
|
"without arguments in any Twitch channel to open its "
|
|
|
|
"suspicious user activity feed. Only the broadcaster and "
|
|
|
|
"moderators have permission to view this feed."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
stripChannelName(target);
|
|
|
|
QDesktopServices::openUrl(QUrl(
|
|
|
|
QString("https://www.twitch.tv/popout/moderator/%1/low-trust-users")
|
|
|
|
.arg(target)));
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2022-11-05 12:56:17 +01:00
|
|
|
auto formatChattersError = [](HelixGetChattersError error,
|
|
|
|
QString message) {
|
|
|
|
using Error = HelixGetChattersError;
|
2022-11-01 23:18:57 +01:00
|
|
|
|
2023-02-14 22:27:33 +01:00
|
|
|
QString errorMessage = QString("Failed to get chatter count - ");
|
2021-01-16 13:58:11 +01:00
|
|
|
|
2022-11-05 12:56:17 +01:00
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
2022-11-01 23:18:57 +01:00
|
|
|
|
2022-11-05 12:56:17 +01:00
|
|
|
case Error::UserMissingScope: {
|
|
|
|
errorMessage += "Missing required scope. "
|
|
|
|
"Re-login with your "
|
|
|
|
"account and try again.";
|
|
|
|
}
|
|
|
|
break;
|
2022-11-01 23:18:57 +01:00
|
|
|
|
2022-11-05 12:56:17 +01:00
|
|
|
case Error::UserNotAuthorized: {
|
|
|
|
errorMessage += "You must have moderator permissions to "
|
|
|
|
"use this command.";
|
|
|
|
}
|
|
|
|
break;
|
2022-11-01 23:18:57 +01:00
|
|
|
|
2022-11-05 12:56:17 +01:00
|
|
|
case Error::Unknown: {
|
|
|
|
errorMessage += "An unknown error has occurred.";
|
2021-01-16 13:58:11 +01:00
|
|
|
}
|
2022-11-05 12:56:17 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
return errorMessage;
|
|
|
|
};
|
2021-01-16 13:58:11 +01:00
|
|
|
|
2022-11-05 12:56:17 +01:00
|
|
|
this->registerCommand(
|
|
|
|
"/chatters", [formatChattersError](const auto &words, auto channel) {
|
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /chatters command only works in Twitch Channels"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh chatter list via helix api for mods
|
|
|
|
getHelix()->getChatters(
|
|
|
|
twitchChannel->roomId(),
|
|
|
|
getApp()->accounts->twitch.getCurrent()->getUserId(), 1,
|
|
|
|
[channel](auto result) {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Chatter count: %1")
|
|
|
|
.arg(localizeNumbers(result.total))));
|
|
|
|
},
|
|
|
|
[channel, formatChattersError](auto error, auto message) {
|
|
|
|
auto errorMessage = formatChattersError(error, message);
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand("/test-chatters", [formatChattersError](
|
|
|
|
const auto & /*words*/,
|
|
|
|
auto channel) {
|
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
2021-01-16 13:58:11 +01:00
|
|
|
|
2022-11-01 23:18:57 +01:00
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2022-11-05 12:56:17 +01:00
|
|
|
"The /test-chatters command only works in Twitch Channels"));
|
2021-01-16 13:58:11 +01:00
|
|
|
return "";
|
2022-11-01 23:18:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
getHelix()->getChatters(
|
|
|
|
twitchChannel->roomId(),
|
2022-11-05 12:56:17 +01:00
|
|
|
getApp()->accounts->twitch.getCurrent()->getUserId(), 5000,
|
|
|
|
[channel, twitchChannel](auto result) {
|
|
|
|
QStringList entries;
|
|
|
|
for (const auto &username : result.chatters)
|
|
|
|
{
|
|
|
|
entries << username;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString prefix = "Chatters ";
|
|
|
|
|
|
|
|
if (result.total > 5000)
|
|
|
|
{
|
|
|
|
prefix += QString("(5000/%1):").arg(result.total);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
prefix += QString("(%1):").arg(result.total);
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageBuilder builder;
|
|
|
|
TwitchMessageBuilder::listOfUsersSystemMessage(
|
|
|
|
prefix, entries, twitchChannel, &builder);
|
|
|
|
|
|
|
|
channel->addMessage(builder.release());
|
2022-11-01 23:18:57 +01:00
|
|
|
},
|
2022-11-05 12:56:17 +01:00
|
|
|
[channel, formatChattersError](auto error, auto message) {
|
|
|
|
auto errorMessage = formatChattersError(error, message);
|
2022-11-01 23:18:57 +01:00
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2021-01-17 14:47:34 +01:00
|
|
|
|
2022-11-05 12:20:12 +01:00
|
|
|
auto formatModsError = [](HelixGetModeratorsError error, QString message) {
|
|
|
|
using Error = HelixGetModeratorsError;
|
|
|
|
|
2023-02-14 22:27:33 +01:00
|
|
|
QString errorMessage = QString("Failed to get moderators - ");
|
2022-11-05 12:20:12 +01:00
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::UserMissingScope: {
|
|
|
|
errorMessage += "Missing required scope. "
|
|
|
|
"Re-login with your "
|
|
|
|
"account and try again.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::UserNotAuthorized: {
|
|
|
|
errorMessage +=
|
|
|
|
"Due to Twitch restrictions, "
|
|
|
|
"this command can only be used by the broadcaster. "
|
|
|
|
"To see the list of mods you must use the Twitch website.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown: {
|
|
|
|
errorMessage += "An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return errorMessage;
|
|
|
|
};
|
|
|
|
|
|
|
|
this->registerCommand(
|
|
|
|
"/mods",
|
|
|
|
[formatModsError](const QStringList &words, auto channel) -> QString {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /mods command only works in Twitch Channels"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2022-11-05 12:20:12 +01:00
|
|
|
switch (getSettings()->helixTimegateModerators.getValue())
|
|
|
|
{
|
|
|
|
case HelixTimegateOverride::Timegate: {
|
|
|
|
if (areIRCCommandsStillAvailable())
|
|
|
|
{
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseIRC: {
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case HelixTimegateOverride::AlwaysUseHelix: {
|
|
|
|
// Fall through to helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
getHelix()->getModerators(
|
|
|
|
twitchChannel->roomId(), 500,
|
|
|
|
[channel, twitchChannel](auto result) {
|
2023-04-15 19:59:46 +02:00
|
|
|
if (result.empty())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"This channel does not have any moderators."));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-05 12:20:12 +01:00
|
|
|
// TODO: sort results?
|
|
|
|
|
|
|
|
MessageBuilder builder;
|
|
|
|
TwitchMessageBuilder::listOfUsersSystemMessage(
|
|
|
|
"The moderators of this channel are", result,
|
|
|
|
twitchChannel, &builder);
|
|
|
|
channel->addMessage(builder.release());
|
|
|
|
},
|
|
|
|
[channel, formatModsError](auto error, auto message) {
|
|
|
|
auto errorMessage = formatModsError(error, message);
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2021-02-14 14:01:13 +01:00
|
|
|
this->registerCommand("/clip", [](const auto & /*words*/, auto channel) {
|
2021-03-15 17:45:56 +01:00
|
|
|
if (const auto type = channel->getType();
|
|
|
|
type != Channel::Type::Twitch &&
|
|
|
|
type != Channel::Type::TwitchWatching)
|
2021-01-17 14:47:34 +01:00
|
|
|
{
|
2022-11-17 20:22:47 +01:00
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /clip command only works in Twitch Channels"));
|
2021-01-17 14:47:34 +01:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
|
|
|
|
twitchChannel->createClip();
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2021-01-30 15:39:01 +01:00
|
|
|
|
|
|
|
this->registerCommand("/marker", [](const QStringList &words,
|
|
|
|
auto channel) {
|
2021-10-17 12:45:54 +02:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
2021-01-30 15:39:01 +01:00
|
|
|
{
|
2021-10-17 12:45:54 +02:00
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /marker command only works in Twitch channels"));
|
2021-01-30 15:39:01 +01:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Avoid Helix calls without Client ID and/or OAuth Token
|
|
|
|
if (getApp()->accounts->twitch.getCurrent()->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You need to be logged in to create stream markers!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Exact same message as in webchat
|
|
|
|
if (!twitchChannel->isLive())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You can only add stream markers during live streams. Try "
|
|
|
|
"again when the channel is live streaming."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto arguments = words;
|
|
|
|
arguments.removeFirst();
|
|
|
|
|
|
|
|
getHelix()->createStreamMarker(
|
|
|
|
// Limit for description is 140 characters, webchat just crops description
|
|
|
|
// if it's >140 characters, so we're doing the same thing
|
|
|
|
twitchChannel->roomId(), arguments.join(" ").left(140),
|
|
|
|
[channel, arguments](const HelixStreamMarker &streamMarker) {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Successfully added a stream marker at %1%2")
|
|
|
|
.arg(formatTime(streamMarker.positionSeconds))
|
|
|
|
.arg(streamMarker.description.isEmpty()
|
|
|
|
? ""
|
|
|
|
: QString(": \"%1\"")
|
|
|
|
.arg(streamMarker.description))));
|
|
|
|
},
|
|
|
|
[channel](auto error) {
|
|
|
|
QString errorMessage("Failed to create stream marker - ");
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case HelixStreamMarkerError::UserNotAuthorized: {
|
|
|
|
errorMessage +=
|
|
|
|
"you don't have permission to perform that action.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixStreamMarkerError::UserNotAuthenticated: {
|
|
|
|
errorMessage += "you need to re-authenticate.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
// This would most likely happen if the service is down, or if the JSON payload returned has changed format
|
|
|
|
case HelixStreamMarkerError::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage += "an unknown error occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2021-02-12 14:16:44 +01:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
this->registerCommand("/streamlink", [](const QStringList &words,
|
|
|
|
ChannelPtr channel) {
|
|
|
|
QString target(words.value(1));
|
2021-05-24 00:24:49 +02:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
if (target.isEmpty())
|
|
|
|
{
|
|
|
|
if (channel->getType() == Channel::Type::Twitch &&
|
|
|
|
!channel->isEmpty())
|
|
|
|
{
|
|
|
|
target = channel->getName();
|
|
|
|
}
|
|
|
|
else
|
2021-02-12 14:16:44 +01:00
|
|
|
{
|
2021-05-24 00:24:49 +02:00
|
|
|
channel->addMessage(makeSystemMessage(
|
2022-01-22 21:07:19 +01:00
|
|
|
"/streamlink [channel]. Open specified Twitch channel in "
|
|
|
|
"streamlink. If no channel argument is specified, open the "
|
|
|
|
"current Twitch channel instead."));
|
2021-02-12 14:16:44 +01:00
|
|
|
return "";
|
|
|
|
}
|
2022-01-23 14:24:49 +01:00
|
|
|
}
|
2021-02-12 14:16:44 +01:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
stripChannelName(target);
|
|
|
|
openStreamlinkForChannel(target);
|
2021-05-24 00:24:49 +02:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
return "";
|
|
|
|
});
|
2021-05-24 00:24:49 +02:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
this->registerCommand("/popout", [](const QStringList &words,
|
|
|
|
ChannelPtr channel) {
|
|
|
|
QString target(words.value(1));
|
2021-05-24 00:24:49 +02:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
if (target.isEmpty())
|
|
|
|
{
|
|
|
|
if (channel->getType() == Channel::Type::Twitch &&
|
|
|
|
!channel->isEmpty())
|
|
|
|
{
|
|
|
|
target = channel->getName();
|
|
|
|
}
|
|
|
|
else
|
2021-05-24 00:24:49 +02:00
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2021-08-21 14:41:06 +02:00
|
|
|
"Usage: /popout <channel>. You can also use the command "
|
2021-05-24 00:24:49 +02:00
|
|
|
"without arguments in any Twitch channel to open its "
|
|
|
|
"popout chat."));
|
|
|
|
return "";
|
|
|
|
}
|
2022-01-23 14:24:49 +01:00
|
|
|
}
|
2021-05-24 00:24:49 +02:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
stripChannelName(target);
|
|
|
|
QDesktopServices::openUrl(
|
|
|
|
QUrl(QString("https://www.twitch.tv/popout/%1/chat?popout=")
|
|
|
|
.arg(target)));
|
2021-02-12 14:16:44 +01:00
|
|
|
|
2022-03-05 11:54:09 +01:00
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand("/popup", [](const QStringList &words,
|
2022-07-07 19:28:38 +02:00
|
|
|
ChannelPtr sourceChannel) {
|
2022-03-05 11:54:09 +01:00
|
|
|
static const auto *usageMessage =
|
|
|
|
"Usage: /popup [channel]. Open specified Twitch channel in "
|
|
|
|
"a new window. If no channel argument is specified, open "
|
|
|
|
"the currently selected split instead.";
|
|
|
|
|
|
|
|
QString target(words.value(1));
|
|
|
|
stripChannelName(target);
|
|
|
|
|
2022-07-07 19:28:38 +02:00
|
|
|
// Popup the current split
|
2022-03-05 11:54:09 +01:00
|
|
|
if (target.isEmpty())
|
|
|
|
{
|
|
|
|
auto *currentPage =
|
|
|
|
dynamic_cast<SplitContainer *>(getApp()
|
|
|
|
->windows->getMainWindow()
|
|
|
|
.getNotebook()
|
|
|
|
.getSelectedPage());
|
|
|
|
if (currentPage != nullptr)
|
|
|
|
{
|
|
|
|
auto *currentSplit = currentPage->getSelectedSplit();
|
|
|
|
if (currentSplit != nullptr)
|
|
|
|
{
|
|
|
|
currentSplit->popup();
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-07 19:28:38 +02:00
|
|
|
sourceChannel->addMessage(makeSystemMessage(usageMessage));
|
2022-03-05 11:54:09 +01:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2022-07-07 19:28:38 +02:00
|
|
|
// Open channel passed as argument in a popup
|
2022-03-05 11:54:09 +01:00
|
|
|
auto *app = getApp();
|
2022-07-07 19:28:38 +02:00
|
|
|
auto targetChannel = app->twitch->getOrAddChannel(target);
|
|
|
|
app->windows->openInPopup(targetChannel);
|
2022-03-05 11:54:09 +01:00
|
|
|
|
2022-01-23 14:24:49 +01:00
|
|
|
return "";
|
|
|
|
});
|
2021-03-14 18:25:45 +01:00
|
|
|
|
|
|
|
this->registerCommand("/clearmessages", [](const auto & /*words*/,
|
|
|
|
ChannelPtr channel) {
|
|
|
|
auto *currentPage = dynamic_cast<SplitContainer *>(
|
|
|
|
getApp()->windows->getMainWindow().getNotebook().getSelectedPage());
|
|
|
|
|
2022-07-10 15:08:20 +02:00
|
|
|
if (auto split = currentPage->getSelectedSplit())
|
|
|
|
{
|
|
|
|
split->getChannelView().clearMessages();
|
|
|
|
}
|
|
|
|
|
2021-04-04 17:31:08 +02:00
|
|
|
return "";
|
|
|
|
});
|
2021-03-14 18:25:45 +01:00
|
|
|
|
2021-04-04 17:31:08 +02:00
|
|
|
this->registerCommand("/settitle", [](const QStringList &words,
|
|
|
|
ChannelPtr channel) {
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
2021-08-21 14:41:06 +02:00
|
|
|
makeSystemMessage("Usage: /settitle <stream title>"));
|
2021-04-04 17:31:08 +02:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
|
|
|
{
|
|
|
|
auto status = twitchChannel->accessStreamStatus();
|
|
|
|
auto title = words.mid(1).join(" ");
|
|
|
|
getHelix()->updateChannel(
|
|
|
|
twitchChannel->roomId(), "", "", title,
|
|
|
|
[channel, title](NetworkResult) {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Updated title to %1").arg(title)));
|
|
|
|
},
|
|
|
|
[channel] {
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Title update failed! Are you "
|
|
|
|
"missing the required scope?"));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Unable to set title of non-Twitch channel."));
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
});
|
2022-05-23 00:42:52 +02:00
|
|
|
|
2021-04-04 17:31:08 +02:00
|
|
|
this->registerCommand("/setgame", [](const QStringList &words,
|
2021-05-29 12:01:19 +02:00
|
|
|
const ChannelPtr channel) {
|
2021-04-04 17:31:08 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
2021-08-21 14:41:06 +02:00
|
|
|
makeSystemMessage("Usage: /setgame <stream game>"));
|
2021-04-04 17:31:08 +02:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
|
|
|
{
|
2021-05-29 12:01:19 +02:00
|
|
|
const auto gameName = words.mid(1).join(" ");
|
|
|
|
|
2021-04-11 01:34:27 +02:00
|
|
|
getHelix()->searchGames(
|
2021-05-29 12:01:19 +02:00
|
|
|
gameName,
|
|
|
|
[channel, twitchChannel,
|
|
|
|
gameName](const std::vector<HelixGame> &games) {
|
2021-04-11 01:34:27 +02:00
|
|
|
if (games.empty())
|
2021-04-04 17:31:08 +02:00
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Game not found."));
|
2021-05-29 12:01:19 +02:00
|
|
|
return;
|
2021-04-04 17:31:08 +02:00
|
|
|
}
|
2021-05-29 12:01:19 +02:00
|
|
|
|
|
|
|
auto matchedGame = games.at(0);
|
|
|
|
|
|
|
|
if (games.size() > 1)
|
2021-04-04 17:31:08 +02:00
|
|
|
{
|
2021-05-29 12:01:19 +02:00
|
|
|
// NOTE: Improvements could be made with 'fuzzy string matching' code here
|
|
|
|
// attempt to find the best looking game by comparing exactly with lowercase values
|
|
|
|
for (const auto &game : games)
|
|
|
|
{
|
|
|
|
if (game.name.toLower() == gameName.toLower())
|
|
|
|
{
|
|
|
|
matchedGame = game;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-04-04 17:31:08 +02:00
|
|
|
}
|
2021-05-29 12:01:19 +02:00
|
|
|
|
|
|
|
auto status = twitchChannel->accessStreamStatus();
|
|
|
|
getHelix()->updateChannel(
|
|
|
|
twitchChannel->roomId(), matchedGame.id, "", "",
|
|
|
|
[channel, games, matchedGame](const NetworkResult &) {
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage(QString("Updated game to %1")
|
|
|
|
.arg(matchedGame.name)));
|
|
|
|
},
|
|
|
|
[channel] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Game update failed! Are you "
|
|
|
|
"missing the required scope?"));
|
|
|
|
});
|
2021-04-04 17:31:08 +02:00
|
|
|
},
|
|
|
|
[channel] {
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Failed to look up game."));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Unable to set game of non-Twitch channel."));
|
|
|
|
}
|
2021-03-14 18:25:45 +01:00
|
|
|
return "";
|
|
|
|
});
|
2021-06-27 13:40:44 +02:00
|
|
|
|
|
|
|
this->registerCommand("/openurl", [](const QStringList &words,
|
|
|
|
const ChannelPtr channel) {
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
2021-08-21 14:41:06 +02:00
|
|
|
channel->addMessage(makeSystemMessage("Usage: /openurl <URL>"));
|
2021-06-27 13:40:44 +02:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
QUrl url = QUrl::fromUserInput(words.mid(1).join(" "));
|
|
|
|
if (!url.isValid())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage("Invalid URL specified."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
bool res = false;
|
|
|
|
if (supportsIncognitoLinks() && getSettings()->openLinksIncognito)
|
|
|
|
{
|
|
|
|
res = openLinkIncognito(url.toString(QUrl::FullyEncoded));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
res = QDesktopServices::openUrl(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!res)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage("Could not open URL."));
|
|
|
|
}
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2022-05-23 00:42:52 +02:00
|
|
|
|
2022-11-17 20:22:47 +01:00
|
|
|
this->registerCommand(
|
|
|
|
"/raw", [](const QStringList &words, ChannelPtr channel) -> QString {
|
|
|
|
if (channel->isTwitchChannel())
|
|
|
|
{
|
|
|
|
getApp()->twitch->sendRawMessage(words.mid(1).join(" "));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// other code down the road handles this for IRC
|
|
|
|
return words.join(" ");
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
});
|
2022-05-23 00:42:52 +02:00
|
|
|
|
2022-07-31 12:45:25 +02:00
|
|
|
this->registerCommand(
|
|
|
|
"/reply", [](const QStringList &words, ChannelPtr channel) {
|
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /reply command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (words.size() < 3)
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Usage: /reply <username> <message>"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
QString username = words[1];
|
|
|
|
stripChannelName(username);
|
|
|
|
|
|
|
|
auto snapshot = twitchChannel->getMessageSnapshot();
|
|
|
|
for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it)
|
|
|
|
{
|
|
|
|
const auto &msg = *it;
|
|
|
|
if (msg->loginName.compare(username, Qt::CaseInsensitive) == 0)
|
|
|
|
{
|
|
|
|
std::shared_ptr<MessageThread> thread;
|
|
|
|
// found most recent message by user
|
|
|
|
if (msg->replyThread == nullptr)
|
|
|
|
{
|
|
|
|
thread = std::make_shared<MessageThread>(msg);
|
|
|
|
twitchChannel->addReplyThread(thread);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
thread = msg->replyThread;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString reply = words.mid(2).join(" ");
|
|
|
|
twitchChannel->sendReply(reply, thread->rootId());
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("A message from that user wasn't found"));
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2022-01-02 16:43:51 +01:00
|
|
|
#ifndef NDEBUG
|
|
|
|
this->registerCommand(
|
|
|
|
"/fakemsg",
|
|
|
|
[](const QStringList &words, ChannelPtr channel) -> QString {
|
2022-11-17 20:22:47 +01:00
|
|
|
if (!channel->isTwitchChannel())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /fakemsg command only works in Twitch channels."));
|
|
|
|
return "";
|
|
|
|
}
|
2022-01-02 16:43:51 +01:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: /fakemsg (raw irc text) - injects raw irc text as "
|
|
|
|
"if it was a message received from TMI"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
auto ircText = words.mid(1).join(" ");
|
2022-03-19 12:02:29 +01:00
|
|
|
getApp()->twitch->addFakeMessage(ircText);
|
2022-01-02 16:43:51 +01:00
|
|
|
return "";
|
|
|
|
});
|
|
|
|
#endif
|
2022-05-23 00:42:52 +02:00
|
|
|
|
|
|
|
this->registerCommand(
|
|
|
|
"/copy", [](const QStringList &words, ChannelPtr channel) -> QString {
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Usage: /copy <text> - copies provided "
|
|
|
|
"text to clipboard."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
crossPlatformCopy(words.mid(1).join(" "));
|
|
|
|
return "";
|
|
|
|
});
|
2022-09-16 23:15:28 +02:00
|
|
|
|
|
|
|
this->registerCommand("/color", [](const QStringList &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
if (!channel->isTwitchChannel())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /color command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-09-16 23:15:28 +02:00
|
|
|
auto user = getApp()->accounts->twitch.getCurrent();
|
|
|
|
|
|
|
|
// Avoid Helix calls without Client ID and/or OAuth Token
|
|
|
|
if (user->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You must be logged in to use the /color command"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto colorString = words.value(1);
|
|
|
|
|
|
|
|
if (colorString.isEmpty())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Usage: /color <color> - Color must be one of Twitch's "
|
|
|
|
"supported colors (%1) or a hex code (#000000) if you "
|
|
|
|
"have Turbo or Prime.")
|
|
|
|
.arg(VALID_HELIX_COLORS.join(", "))));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanHelixColorName(colorString);
|
|
|
|
|
|
|
|
getHelix()->updateUserChatColor(
|
|
|
|
user->getUserId(), colorString,
|
|
|
|
[colorString, channel] {
|
|
|
|
QString successMessage =
|
|
|
|
QString("Your color has been changed to %1.")
|
|
|
|
.arg(colorString);
|
|
|
|
channel->addMessage(makeSystemMessage(successMessage));
|
|
|
|
},
|
|
|
|
[colorString, channel](auto error, auto message) {
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to change color to %1 - ").arg(colorString);
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case HelixUpdateUserChatColorError::UserMissingScope: {
|
|
|
|
errorMessage +=
|
|
|
|
"Missing required scope. Re-login with your "
|
|
|
|
"account and try again.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixUpdateUserChatColorError::InvalidColor: {
|
|
|
|
errorMessage += QString("Color must be one of Twitch's "
|
|
|
|
"supported colors (%1) or a "
|
|
|
|
"hex code (#000000) if you "
|
|
|
|
"have Turbo or Prime.")
|
|
|
|
.arg(VALID_HELIX_COLORS.join(", "));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixUpdateUserChatColorError::Forwarded: {
|
|
|
|
errorMessage += message + ".";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixUpdateUserChatColorError::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage += "An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2022-09-18 13:19:22 +02:00
|
|
|
|
2022-11-17 20:22:47 +01:00
|
|
|
auto deleteMessages = [](TwitchChannel *twitchChannel,
|
|
|
|
const QString &messageID) {
|
2022-09-18 13:19:22 +02:00
|
|
|
const auto *commandName = messageID.isEmpty() ? "/clear" : "/delete";
|
|
|
|
|
|
|
|
auto user = getApp()->accounts->twitch.getCurrent();
|
|
|
|
|
|
|
|
// Avoid Helix calls without Client ID and/or OAuth Token
|
|
|
|
if (user->isAnon())
|
|
|
|
{
|
2022-11-17 20:22:47 +01:00
|
|
|
twitchChannel->addMessage(makeSystemMessage(
|
2022-09-18 13:19:22 +02:00
|
|
|
QString("You must be logged in to use the %1 command.")
|
|
|
|
.arg(commandName)));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
getHelix()->deleteChatMessages(
|
|
|
|
twitchChannel->roomId(), user->getUserId(), messageID,
|
|
|
|
[]() {
|
|
|
|
// Success handling, we do nothing: IRC/pubsub-edge will dispatch the correct
|
|
|
|
// events to update state for us.
|
|
|
|
},
|
2022-11-17 20:22:47 +01:00
|
|
|
[twitchChannel, messageID](auto error, auto message) {
|
2022-09-18 13:19:22 +02:00
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to delete chat messages - ");
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case HelixDeleteChatMessagesError::UserMissingScope: {
|
|
|
|
errorMessage +=
|
|
|
|
"Missing required scope. Re-login with your "
|
|
|
|
"account and try again.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixDeleteChatMessagesError::UserNotAuthorized: {
|
|
|
|
errorMessage +=
|
|
|
|
"you don't have permission to perform that action.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixDeleteChatMessagesError::MessageUnavailable: {
|
|
|
|
// Override default message prefix to match with IRC message format
|
|
|
|
errorMessage =
|
|
|
|
QString(
|
|
|
|
"The message %1 does not exist, was deleted, "
|
|
|
|
"or is too old to be deleted.")
|
|
|
|
.arg(messageID);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixDeleteChatMessagesError::UserNotAuthenticated: {
|
|
|
|
errorMessage += "you need to re-authenticate.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixDeleteChatMessagesError::Forwarded: {
|
2022-09-20 00:26:48 +02:00
|
|
|
errorMessage += message;
|
2022-09-18 13:19:22 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixDeleteChatMessagesError::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage += "An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-11-17 20:22:47 +01:00
|
|
|
twitchChannel->addMessage(makeSystemMessage(errorMessage));
|
2022-09-18 13:19:22 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
};
|
|
|
|
|
|
|
|
this->registerCommand(
|
|
|
|
"/clear", [deleteMessages](const QStringList &words, auto channel) {
|
|
|
|
(void)words; // unused
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /clear command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
return deleteMessages(twitchChannel, QString());
|
2022-09-18 13:19:22 +02:00
|
|
|
});
|
2022-09-20 00:26:48 +02:00
|
|
|
|
|
|
|
this->registerCommand("/delete", [deleteMessages](const QStringList &words,
|
|
|
|
auto channel) {
|
|
|
|
// This is a wrapper over the Helix delete messages endpoint
|
|
|
|
// We use this to ensure the user gets better error messages for missing or malformed arguments
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /delete command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-09-20 00:26:48 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Usage: /delete <msg-id> - Deletes the "
|
|
|
|
"specified message."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto messageID = words.at(1);
|
|
|
|
auto uuid = QUuid(messageID);
|
|
|
|
if (uuid.isNull())
|
|
|
|
{
|
|
|
|
// The message id must be a valid UUID
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid msg-id: \"%1\"").arg(messageID)));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto msg = channel->findMessage(messageID);
|
|
|
|
if (msg != nullptr)
|
|
|
|
{
|
|
|
|
if (msg->loginName == channel->getName() &&
|
|
|
|
!channel->isBroadcaster())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You cannot delete the broadcaster's messages unless "
|
|
|
|
"you are the broadcaster."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 20:22:47 +01:00
|
|
|
return deleteMessages(twitchChannel, messageID);
|
2022-09-20 00:26:48 +02:00
|
|
|
});
|
2022-09-23 18:12:34 +02:00
|
|
|
|
|
|
|
this->registerCommand("/mod", [](const QStringList &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /mod command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-09-23 18:12:34 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: \"/mod <username>\" - Grant moderator status to a "
|
|
|
|
"user. Use \"/mods\" to list the moderators of this channel."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to mod someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[twitchChannel, channel](const HelixUser &targetUser) {
|
|
|
|
getHelix()->addChannelModerator(
|
|
|
|
twitchChannel->roomId(), targetUser.id,
|
|
|
|
[channel, targetUser] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("You have added %1 as a moderator of this "
|
|
|
|
"channel.")
|
|
|
|
.arg(targetUser.displayName)));
|
|
|
|
},
|
|
|
|
[channel, targetUser](auto error, auto message) {
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to add channel moderator - ");
|
|
|
|
|
|
|
|
using Error = HelixAddChannelModeratorError;
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
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::Ratelimited: {
|
|
|
|
errorMessage +=
|
|
|
|
"You are being ratelimited by Twitch. Try "
|
|
|
|
"again in a few seconds.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::TargetIsVIP: {
|
|
|
|
errorMessage +=
|
|
|
|
QString("%1 is currently a VIP, \"/unvip\" "
|
|
|
|
"them and "
|
|
|
|
"retry this command.")
|
|
|
|
.arg(targetUser.displayName);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::TargetAlreadyModded: {
|
|
|
|
// Equivalent irc error
|
|
|
|
errorMessage =
|
|
|
|
QString("%1 is already a moderator of this "
|
|
|
|
"channel.")
|
|
|
|
.arg(targetUser.displayName);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage +=
|
|
|
|
"An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
2022-09-24 12:49:13 +02:00
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand("/unmod", [](const QStringList &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /unmod command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-09-24 12:49:13 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: \"/unmod <username>\" - Revoke moderator status from a "
|
|
|
|
"user. Use \"/mods\" to list the moderators of this channel."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to unmod someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[twitchChannel, channel](const HelixUser &targetUser) {
|
|
|
|
getHelix()->removeChannelModerator(
|
|
|
|
twitchChannel->roomId(), targetUser.id,
|
|
|
|
[channel, targetUser] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("You have removed %1 as a moderator of "
|
|
|
|
"this channel.")
|
|
|
|
.arg(targetUser.displayName)));
|
|
|
|
},
|
|
|
|
[channel, targetUser](auto error, auto message) {
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to remove channel moderator - ");
|
|
|
|
|
|
|
|
using Error = HelixRemoveChannelModeratorError;
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
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::Ratelimited: {
|
|
|
|
errorMessage +=
|
|
|
|
"You are being ratelimited by Twitch. Try "
|
|
|
|
"again in a few seconds.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::TargetNotModded: {
|
|
|
|
// Equivalent irc error
|
|
|
|
errorMessage +=
|
|
|
|
QString("%1 is not a moderator of this "
|
|
|
|
"channel.")
|
|
|
|
.arg(targetUser.displayName);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage +=
|
|
|
|
"An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
2022-09-23 18:12:34 +02:00
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2022-09-24 17:50:02 +02:00
|
|
|
|
|
|
|
this->registerCommand(
|
|
|
|
"/announce", [](const QStringList &words, auto channel) -> QString {
|
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"This command can only be used in Twitch channels."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: /announce <message> - Call attention to your "
|
|
|
|
"message with a highlight."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto user = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (user->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You must be logged in to use the /announce command"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
getHelix()->sendChatAnnouncement(
|
|
|
|
twitchChannel->roomId(), user->getUserId(),
|
|
|
|
words.mid(1).join(" "), HelixAnnouncementColor::Primary,
|
|
|
|
[]() {
|
|
|
|
// do nothing.
|
|
|
|
},
|
|
|
|
[channel](auto error, auto message) {
|
|
|
|
using Error = HelixSendChatAnnouncementError;
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to send announcement - ");
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case Error::UserMissingScope: {
|
|
|
|
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
|
|
|
|
errorMessage +=
|
|
|
|
"Missing required scope. Re-login with your "
|
|
|
|
"account and try again.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage += "An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
return "";
|
|
|
|
});
|
2022-09-25 11:45:46 +02:00
|
|
|
|
|
|
|
this->registerCommand("/vip", [](const QStringList &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /vip command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-09-25 11:45:46 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: \"/vip <username>\" - Grant VIP status to a user. Use "
|
|
|
|
"\"/vips\" to list the VIPs of this channel."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to VIP someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[twitchChannel, channel](const HelixUser &targetUser) {
|
|
|
|
getHelix()->addChannelVIP(
|
|
|
|
twitchChannel->roomId(), targetUser.id,
|
|
|
|
[channel, targetUser] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString(
|
|
|
|
"You have added %1 as a VIP of this channel.")
|
|
|
|
.arg(targetUser.displayName)));
|
|
|
|
},
|
|
|
|
[channel, targetUser](auto error, auto message) {
|
|
|
|
QString errorMessage = QString("Failed to add VIP - ");
|
|
|
|
|
|
|
|
using Error = HelixAddChannelVIPError;
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
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;
|
2022-10-01 16:00:45 +02:00
|
|
|
|
|
|
|
case Error::Ratelimited: {
|
|
|
|
errorMessage +=
|
|
|
|
"You are being ratelimited by Twitch. Try "
|
|
|
|
"again in a few seconds.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
// These are actually the IRC equivalents, so we can ditch the prefix
|
|
|
|
errorMessage = message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage +=
|
|
|
|
"An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand("/unvip", [](const QStringList &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /unvip command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-10-01 16:00:45 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: \"/unvip <username>\" - Revoke VIP status from a user. "
|
|
|
|
"Use \"/vips\" to list the VIPs of this channel."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to UnVIP someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[twitchChannel, channel](const HelixUser &targetUser) {
|
|
|
|
getHelix()->removeChannelVIP(
|
|
|
|
twitchChannel->roomId(), targetUser.id,
|
|
|
|
[channel, targetUser] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString(
|
|
|
|
"You have removed %1 as a VIP of this channel.")
|
|
|
|
.arg(targetUser.displayName)));
|
|
|
|
},
|
|
|
|
[channel, targetUser](auto error, auto message) {
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to remove VIP - ");
|
|
|
|
|
|
|
|
using Error = HelixRemoveChannelVIPError;
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
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;
|
2022-09-25 11:45:46 +02:00
|
|
|
|
|
|
|
case Error::Ratelimited: {
|
|
|
|
errorMessage +=
|
|
|
|
"You are being ratelimited by Twitch. Try "
|
|
|
|
"again in a few seconds.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
// These are actually the IRC equivalents, so we can ditch the prefix
|
|
|
|
errorMessage = message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage +=
|
|
|
|
"An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2022-10-01 17:10:06 +02:00
|
|
|
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
auto unbanLambda = [](auto words, auto channel) {
|
|
|
|
auto commandName = words.at(0).toLower();
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("The %1 command only works in Twitch channels")
|
|
|
|
.arg(commandName)));
|
|
|
|
return "";
|
|
|
|
}
|
2022-10-01 17:10:06 +02:00
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Usage: \"%1 <username>\" - Removes a ban on a user.")
|
|
|
|
.arg(commandName)));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to unban someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[channel, currentUser, twitchChannel,
|
|
|
|
target](const auto &targetUser) {
|
|
|
|
getHelix()->unbanUser(
|
|
|
|
twitchChannel->roomId(), currentUser->getUserId(),
|
|
|
|
targetUser.id,
|
|
|
|
[] {
|
|
|
|
// No response for unbans, they're emitted over pubsub/IRC instead
|
|
|
|
},
|
|
|
|
[channel, target, targetUser](auto error, auto message) {
|
|
|
|
using Error = HelixUnbanUserError;
|
|
|
|
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to unban user - ");
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case Error::ConflictingOperation: {
|
|
|
|
errorMessage +=
|
|
|
|
"There was a conflicting ban 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::TargetNotBanned: {
|
|
|
|
// Equivalent IRC error
|
|
|
|
errorMessage =
|
|
|
|
QString(
|
|
|
|
"%1 is not banned from this channel.")
|
|
|
|
.arg(targetUser.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));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}; // These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
|
|
|
|
this->registerCommand("/unban", [unbanLambda](const QStringList &words,
|
|
|
|
auto channel) {
|
|
|
|
return unbanLambda(words, channel);
|
|
|
|
}); // These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
|
|
|
|
this->registerCommand("/untimeout", [unbanLambda](const QStringList &words,
|
|
|
|
auto channel) {
|
|
|
|
return unbanLambda(words, channel);
|
|
|
|
}); // These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
|
|
|
// These changes are from the helix-command-migration/unban-untimeout branch
|
2022-10-02 15:27:55 +02:00
|
|
|
|
|
|
|
this->registerCommand( // /raid
|
|
|
|
"/raid", [](const QStringList &words, auto channel) -> QString {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /raid command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-10-02 15:27:55 +02:00
|
|
|
switch (getSettings()->helixTimegateRaid.getValue())
|
|
|
|
{
|
|
|
|
case HelixTimegateOverride::Timegate: {
|
|
|
|
if (areIRCCommandsStillAvailable())
|
|
|
|
{
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
|
|
|
|
// fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseIRC: {
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseHelix: {
|
|
|
|
// do nothing and fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: \"/raid <username>\" - Raid a user. "
|
|
|
|
"Only the broadcaster can start a raid."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You must be logged in to start a raid!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[twitchChannel, channel](const HelixUser &targetUser) {
|
|
|
|
getHelix()->startRaid(
|
|
|
|
twitchChannel->roomId(), targetUser.id,
|
|
|
|
[channel, targetUser] {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("You started to raid %1.")
|
|
|
|
.arg(targetUser.displayName)));
|
|
|
|
},
|
|
|
|
[channel, targetUser](auto error, auto message) {
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to start a raid - ");
|
|
|
|
|
|
|
|
using Error = HelixStartRaidError;
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
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: {
|
|
|
|
errorMessage +=
|
|
|
|
"You must be the broadcaster "
|
|
|
|
"to start a raid.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::CantRaidYourself: {
|
|
|
|
errorMessage +=
|
|
|
|
"A channel cannot raid itself.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Ratelimited: {
|
|
|
|
errorMessage += "You are being ratelimited "
|
|
|
|
"by Twitch. Try "
|
|
|
|
"again in a few seconds.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage +=
|
|
|
|
"An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}); // /raid
|
2022-10-02 16:18:10 +02:00
|
|
|
|
2022-10-08 14:10:38 +02:00
|
|
|
this->registerCommand( // /unraid
|
|
|
|
"/unraid", [](const QStringList &words, auto channel) -> QString {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /unraid command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-10-08 14:10:38 +02:00
|
|
|
switch (getSettings()->helixTimegateRaid.getValue())
|
|
|
|
{
|
|
|
|
case HelixTimegateOverride::Timegate: {
|
|
|
|
if (areIRCCommandsStillAvailable())
|
|
|
|
{
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
|
|
|
|
// fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseIRC: {
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseHelix: {
|
|
|
|
// do nothing and fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (words.size() != 1)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Usage: \"/unraid\" - Cancel the current raid. "
|
|
|
|
"Only the broadcaster can cancel the raid."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You must be logged in to cancel the raid!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
getHelix()->cancelRaid(
|
|
|
|
twitchChannel->roomId(),
|
|
|
|
[channel] {
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage(QString("You cancelled the raid.")));
|
|
|
|
},
|
|
|
|
[channel](auto error, auto message) {
|
|
|
|
QString errorMessage =
|
|
|
|
QString("Failed to cancel the raid - ");
|
|
|
|
|
|
|
|
using Error = HelixCancelRaidError;
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
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: {
|
|
|
|
errorMessage += "You must be the broadcaster "
|
|
|
|
"to cancel the raid.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::NoRaidPending: {
|
|
|
|
errorMessage += "You don't have an active raid.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Ratelimited: {
|
|
|
|
errorMessage +=
|
|
|
|
"You are being ratelimited by Twitch. Try "
|
|
|
|
"again in a few seconds.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage += "An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}); // unraid
|
|
|
|
|
2022-11-06 13:07:54 +01:00
|
|
|
this->registerCommand("/emoteonly", &commands::emoteOnly);
|
|
|
|
this->registerCommand("/emoteonlyoff", &commands::emoteOnlyOff);
|
2022-10-03 19:42:02 +02:00
|
|
|
|
2022-11-06 13:07:54 +01:00
|
|
|
this->registerCommand("/subscribers", &commands::subscribers);
|
|
|
|
this->registerCommand("/subscribersoff", &commands::subscribersOff);
|
2022-10-03 19:42:02 +02:00
|
|
|
|
2022-11-06 13:07:54 +01:00
|
|
|
this->registerCommand("/slow", &commands::slow);
|
|
|
|
this->registerCommand("/slowoff", &commands::slowOff);
|
2022-10-03 19:42:02 +02:00
|
|
|
|
2022-11-06 13:07:54 +01:00
|
|
|
this->registerCommand("/followers", &commands::followers);
|
|
|
|
this->registerCommand("/followersoff", &commands::followersOff);
|
2022-10-03 19:42:02 +02:00
|
|
|
|
2022-11-06 13:07:54 +01:00
|
|
|
this->registerCommand("/uniquechat", &commands::uniqueChat);
|
|
|
|
this->registerCommand("/r9kbeta", &commands::uniqueChat);
|
|
|
|
this->registerCommand("/uniquechatoff", &commands::uniqueChatOff);
|
|
|
|
this->registerCommand("/r9kbetaoff", &commands::uniqueChatOff);
|
2022-10-06 23:52:25 +02:00
|
|
|
|
|
|
|
auto formatBanTimeoutError =
|
|
|
|
[](const char *operation, HelixBanUserError error,
|
2023-02-26 21:03:14 +01:00
|
|
|
const QString &message, const QString &userTarget) -> QString {
|
2022-10-06 23:52:25 +02:00
|
|
|
using Error = HelixBanUserError;
|
|
|
|
|
|
|
|
QString errorMessage = QString("Failed to %1 user - ").arg(operation);
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case Error::ConflictingOperation: {
|
|
|
|
errorMessage += "There was a conflicting ban 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::TargetBanned: {
|
|
|
|
// Equivalent IRC error
|
2022-11-19 14:34:07 +01:00
|
|
|
errorMessage += QString("%1 is already banned in this channel.")
|
2023-02-26 21:03:14 +01:00
|
|
|
.arg(userTarget);
|
2022-11-19 14:34:07 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::CannotBanUser: {
|
|
|
|
// We can't provide the identical error as in IRC,
|
|
|
|
// because we don't have enough information about the user.
|
|
|
|
// The messages from IRC are formatted like this:
|
|
|
|
// "You cannot {op} moderator {mod} unless you are the owner of this channel."
|
|
|
|
// "You cannot {op} the broadcaster."
|
2023-02-26 21:03:14 +01:00
|
|
|
errorMessage +=
|
|
|
|
QString("You cannot %1 %2.").arg(operation, userTarget);
|
2022-10-06 23:52:25 +02:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
return errorMessage;
|
|
|
|
};
|
|
|
|
|
|
|
|
this->registerCommand("/timeout", [formatBanTimeoutError](
|
|
|
|
const QStringList &words,
|
|
|
|
auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("The /timeout command only works in Twitch channels")));
|
|
|
|
return "";
|
|
|
|
}
|
2022-10-06 23:52:25 +02:00
|
|
|
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 "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to timeout someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
int duration = 10 * 60; // 10min
|
|
|
|
if (words.size() >= 3)
|
|
|
|
{
|
|
|
|
duration = (int)parseDurationToSeconds(words.at(2));
|
|
|
|
if (duration <= 0)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(usageStr));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
auto reason = words.mid(3).join(' ');
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[channel, currentUser, twitchChannel, target, duration, reason,
|
|
|
|
formatBanTimeoutError](const auto &targetUser) {
|
|
|
|
getHelix()->banUser(
|
|
|
|
twitchChannel->roomId(), currentUser->getUserId(),
|
|
|
|
targetUser.id, duration, reason,
|
|
|
|
[] {
|
|
|
|
// No response for timeouts, they're emitted over pubsub/IRC instead
|
|
|
|
},
|
|
|
|
[channel, target, targetUser, formatBanTimeoutError](
|
|
|
|
auto error, auto message) {
|
|
|
|
auto errorMessage = formatBanTimeoutError(
|
|
|
|
"timeout", error, message, targetUser.displayName);
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand("/ban", [formatBanTimeoutError](
|
|
|
|
const QStringList &words, auto channel) {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("The /ban command only works in Twitch channels")));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2022-10-06 23:52:25 +02:00
|
|
|
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 "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to ban someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
stripChannelName(target);
|
|
|
|
|
|
|
|
auto reason = words.mid(2).join(' ');
|
|
|
|
|
|
|
|
getHelix()->getUserByName(
|
|
|
|
target,
|
|
|
|
[channel, currentUser, twitchChannel, target, reason,
|
|
|
|
formatBanTimeoutError](const auto &targetUser) {
|
|
|
|
getHelix()->banUser(
|
|
|
|
twitchChannel->roomId(), currentUser->getUserId(),
|
|
|
|
targetUser.id, boost::none, reason,
|
|
|
|
[] {
|
|
|
|
// No response for bans, they're emitted over pubsub/IRC instead
|
|
|
|
},
|
|
|
|
[channel, target, targetUser, formatBanTimeoutError](
|
|
|
|
auto error, auto message) {
|
|
|
|
auto errorMessage = formatBanTimeoutError(
|
|
|
|
"ban", error, message, targetUser.displayName);
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[channel, target] {
|
|
|
|
// Equivalent error from IRC
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Invalid username: %1").arg(target)));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2022-10-08 13:11:55 +02:00
|
|
|
|
2023-02-26 21:03:14 +01:00
|
|
|
this->registerCommand("/banid", [formatBanTimeoutError](
|
|
|
|
const QStringList &words,
|
|
|
|
auto channel) {
|
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("The /banid command only works in Twitch channels")));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto *usageStr =
|
|
|
|
"Usage: \"/banid <userID> [reason]\" - Permanently prevent a user "
|
|
|
|
"from chatting via their user ID. Reason is optional and will be "
|
|
|
|
"shown to the target user and other moderators.";
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(usageStr));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("You must be logged in to ban someone!"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto target = words.at(1);
|
|
|
|
auto reason = words.mid(2).join(' ');
|
|
|
|
|
|
|
|
getHelix()->banUser(
|
|
|
|
twitchChannel->roomId(), currentUser->getUserId(), target,
|
|
|
|
boost::none, reason,
|
|
|
|
[] {
|
|
|
|
// No response for bans, they're emitted over pubsub/IRC instead
|
|
|
|
},
|
|
|
|
[channel, target, formatBanTimeoutError](auto error, auto message) {
|
|
|
|
auto errorMessage =
|
|
|
|
formatBanTimeoutError("ban", error, message, "#" + target);
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2022-10-08 13:11:55 +02:00
|
|
|
for (const auto &cmd : TWITCH_WHISPER_COMMANDS)
|
|
|
|
{
|
|
|
|
this->registerCommand(cmd, [](const QStringList &words, auto channel) {
|
|
|
|
return runWhisperCommand(words, channel);
|
|
|
|
});
|
|
|
|
}
|
2022-10-10 12:56:55 +02:00
|
|
|
|
|
|
|
auto formatVIPListError = [](HelixListVIPsError error,
|
|
|
|
const QString &message) -> QString {
|
|
|
|
using Error = HelixListVIPsError;
|
|
|
|
|
|
|
|
QString errorMessage = QString("Failed to list VIPs - ");
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
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::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::UserNotBroadcaster: {
|
|
|
|
errorMessage +=
|
|
|
|
"Due to Twitch restrictions, "
|
|
|
|
"this command can only be used by the broadcaster. "
|
|
|
|
"To see the list of VIPs you must use the Twitch website.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown: {
|
|
|
|
errorMessage += "An unknown error has occurred.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return errorMessage;
|
|
|
|
};
|
|
|
|
|
2022-11-05 10:43:31 +01:00
|
|
|
auto formatStartCommercialError = [](HelixStartCommercialError error,
|
|
|
|
const QString &message) -> QString {
|
|
|
|
using Error = HelixStartCommercialError;
|
|
|
|
|
|
|
|
QString errorMessage = "Failed to start commercial - ";
|
|
|
|
|
|
|
|
switch (error)
|
|
|
|
{
|
|
|
|
case Error::UserMissingScope: {
|
|
|
|
errorMessage += "Missing required scope. Re-login with your "
|
|
|
|
"account and try again.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::TokenMustMatchBroadcaster: {
|
|
|
|
errorMessage += "Only the broadcaster of the channel can run "
|
|
|
|
"commercials.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::BroadcasterNotStreaming: {
|
|
|
|
errorMessage += "You must be streaming live to run "
|
|
|
|
"commercials.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2022-11-12 00:17:50 +01:00
|
|
|
case Error::MissingLengthParameter: {
|
|
|
|
errorMessage +=
|
|
|
|
"Command must include a desired commercial break "
|
|
|
|
"length that is greater than zero.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2022-11-05 10:43:31 +01:00
|
|
|
case Error::Ratelimited: {
|
|
|
|
errorMessage += "You must wait until your cooldown period "
|
|
|
|
"expires before you can run another "
|
|
|
|
"commercial.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Forwarded: {
|
|
|
|
errorMessage += message;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Error::Unknown:
|
|
|
|
default: {
|
|
|
|
errorMessage +=
|
|
|
|
QString("An unknown error has occurred (%1).").arg(message);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return errorMessage;
|
|
|
|
};
|
|
|
|
|
2022-10-10 12:56:55 +02:00
|
|
|
this->registerCommand(
|
|
|
|
"/vips",
|
|
|
|
[formatVIPListError](const QStringList &words,
|
|
|
|
auto channel) -> QString {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /vips command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2022-10-10 12:56:55 +02:00
|
|
|
switch (getSettings()->helixTimegateVIPs.getValue())
|
|
|
|
{
|
|
|
|
case HelixTimegateOverride::Timegate: {
|
|
|
|
if (areIRCCommandsStillAvailable())
|
|
|
|
{
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
|
|
|
|
// fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseIRC: {
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseHelix: {
|
|
|
|
// do nothing and fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
if (currentUser->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"Due to Twitch restrictions, " //
|
|
|
|
"this command can only be used by the broadcaster. "
|
|
|
|
"To see the list of VIPs you must use the "
|
|
|
|
"Twitch website."));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
getHelix()->getChannelVIPs(
|
|
|
|
twitchChannel->roomId(),
|
|
|
|
[channel, twitchChannel](const std::vector<HelixVip> &vipList) {
|
|
|
|
if (vipList.empty())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"This channel does not have any VIPs."));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto messagePrefix =
|
|
|
|
QString("The VIPs of this channel are");
|
|
|
|
|
2022-11-21 18:58:32 +01:00
|
|
|
// TODO: sort results?
|
2022-10-10 12:56:55 +02:00
|
|
|
MessageBuilder builder;
|
|
|
|
TwitchMessageBuilder::listOfUsersSystemMessage(
|
2022-11-21 18:58:32 +01:00
|
|
|
messagePrefix, vipList, twitchChannel, &builder);
|
2022-10-10 12:56:55 +02:00
|
|
|
|
|
|
|
channel->addMessage(builder.release());
|
|
|
|
},
|
|
|
|
[channel, formatVIPListError](auto error, auto message) {
|
|
|
|
auto errorMessage = formatVIPListError(error, message);
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2022-10-15 12:36:49 +02:00
|
|
|
|
2022-11-05 10:43:31 +01:00
|
|
|
this->registerCommand(
|
|
|
|
"/commercial",
|
|
|
|
[formatStartCommercialError](const QStringList &words,
|
|
|
|
auto channel) -> QString {
|
2022-11-17 20:22:47 +01:00
|
|
|
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
|
|
|
|
if (tc == nullptr)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"The /commercial command only works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2022-11-05 10:43:31 +01:00
|
|
|
const auto *usageStr = "Usage: \"/commercial <length>\" - Starts a "
|
|
|
|
"commercial with the "
|
|
|
|
"specified duration for the current "
|
|
|
|
"channel. Valid length options "
|
|
|
|
"are 30, 60, 90, 120, 150, and 180 seconds.";
|
|
|
|
|
|
|
|
switch (getSettings()->helixTimegateCommercial.getValue())
|
|
|
|
{
|
|
|
|
case HelixTimegateOverride::Timegate: {
|
|
|
|
if (areIRCCommandsStillAvailable())
|
|
|
|
{
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
|
|
|
|
// fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseIRC: {
|
|
|
|
return useIRCCommand(words);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case HelixTimegateOverride::AlwaysUseHelix: {
|
|
|
|
// do nothing and fall through to Helix logic
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (words.size() < 2)
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(usageStr));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto user = getApp()->accounts->twitch.getCurrent();
|
|
|
|
|
|
|
|
// Avoid Helix calls without Client ID and/or OAuth Token
|
|
|
|
if (user->isAnon())
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
"You must be logged in to use the /commercial command"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
auto broadcasterID = tc->roomId();
|
|
|
|
auto length = words.at(1).toInt();
|
|
|
|
|
|
|
|
getHelix()->startCommercial(
|
|
|
|
broadcasterID, length,
|
|
|
|
[channel](auto response) {
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2022-11-12 00:17:50 +01:00
|
|
|
QString("Starting %1 second long commercial break. "
|
|
|
|
"Keep in mind you are still "
|
2022-11-05 10:43:31 +01:00
|
|
|
"live and not all viewers will receive a "
|
|
|
|
"commercial. "
|
2022-11-12 00:17:50 +01:00
|
|
|
"You may run another commercial in %2 seconds.")
|
|
|
|
.arg(response.length)
|
2022-11-05 10:43:31 +01:00
|
|
|
.arg(response.retryAfter)));
|
|
|
|
},
|
|
|
|
[channel, formatStartCommercialError](auto error,
|
|
|
|
auto message) {
|
|
|
|
auto errorMessage =
|
|
|
|
formatStartCommercialError(error, message);
|
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
});
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2022-11-13 18:21:21 +01:00
|
|
|
|
|
|
|
this->registerCommand("/unstable-set-user-color", [](const auto &ctx) {
|
2022-11-17 20:22:47 +01:00
|
|
|
if (ctx.twitchChannel == nullptr)
|
|
|
|
{
|
|
|
|
ctx.channel->addMessage(
|
|
|
|
makeSystemMessage("The /unstable-set-user-color command only "
|
|
|
|
"works in Twitch channels"));
|
|
|
|
return "";
|
|
|
|
}
|
2022-11-13 18:21:21 +01:00
|
|
|
if (ctx.words.size() < 2)
|
|
|
|
{
|
|
|
|
ctx.channel->addMessage(
|
|
|
|
makeSystemMessage(QString("Usage: %1 <TwitchUserID> [color]")
|
|
|
|
.arg(ctx.words.at(0))));
|
2023-04-22 19:44:23 +02:00
|
|
|
return "";
|
2022-11-13 18:21:21 +01:00
|
|
|
}
|
|
|
|
|
2023-04-22 19:44:23 +02:00
|
|
|
auto userID = ctx.words.at(1);
|
|
|
|
|
2022-11-13 18:21:21 +01:00
|
|
|
auto color = ctx.words.value(2);
|
|
|
|
|
|
|
|
getIApp()->getUserData()->setUserColor(userID, color);
|
|
|
|
|
|
|
|
return "";
|
|
|
|
});
|
2023-05-07 12:52:05 +02:00
|
|
|
|
2023-05-27 14:18:08 +02:00
|
|
|
this->registerCommand(
|
|
|
|
"/debug-force-image-gc",
|
|
|
|
[](const QStringList & /*words*/, auto /*channel*/) -> QString {
|
|
|
|
runInGuiThread([] {
|
|
|
|
using namespace chatterino::detail;
|
|
|
|
auto &iep = ImageExpirationPool::instance();
|
|
|
|
iep.freeOld();
|
|
|
|
});
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
|
|
|
this->registerCommand(
|
|
|
|
"/debug-force-image-unload",
|
|
|
|
[](const QStringList & /*words*/, auto /*channel*/) -> QString {
|
|
|
|
runInGuiThread([] {
|
|
|
|
using namespace chatterino::detail;
|
|
|
|
auto &iep = ImageExpirationPool::instance();
|
|
|
|
iep.freeAll();
|
|
|
|
});
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2023-05-07 12:52:05 +02:00
|
|
|
this->registerCommand("/shield", &commands::shieldModeOn);
|
|
|
|
this->registerCommand("/shieldoff", &commands::shieldModeOff);
|
2023-05-17 23:32:50 +02:00
|
|
|
|
2023-05-20 18:32:06 +02:00
|
|
|
this->registerCommand("/shoutout", &commands::sendShoutout);
|
|
|
|
|
2023-05-17 23:32:50 +02:00
|
|
|
this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules);
|
2023-07-23 14:13:21 +02:00
|
|
|
this->registerCommand("/c2-theme-autoreload", &commands::toggleThemeReload);
|
2020-09-26 10:01:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void CommandController::save()
|
|
|
|
{
|
|
|
|
this->sm_->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
CommandModel *CommandController::createModel(QObject *parent)
|
|
|
|
{
|
|
|
|
CommandModel *model = new CommandModel(parent);
|
2021-12-26 14:21:52 +01:00
|
|
|
model->initialize(&this->items);
|
2020-09-26 10:01:00 +02:00
|
|
|
|
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString CommandController::execCommand(const QString &textNoEmoji,
|
|
|
|
ChannelPtr channel, bool dryRun)
|
|
|
|
{
|
|
|
|
QString text = getApp()->emotes->emojis.replaceShortCodes(textNoEmoji);
|
2022-05-14 12:11:39 +02:00
|
|
|
QStringList words = text.split(' ', Qt::SkipEmptyParts);
|
2020-09-26 10:01:00 +02:00
|
|
|
|
|
|
|
if (words.length() == 0)
|
|
|
|
{
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString commandName = words[0];
|
|
|
|
|
2021-04-17 12:33:18 +02:00
|
|
|
{
|
|
|
|
// check if user command exists
|
|
|
|
const auto it = this->userCommands_.find(commandName);
|
|
|
|
if (it != this->userCommands_.end())
|
|
|
|
{
|
|
|
|
text = getApp()->emotes->emojis.replaceShortCodes(
|
2021-09-11 14:35:26 +02:00
|
|
|
this->execCustomCommand(words, it.value(), dryRun, channel));
|
2021-04-17 12:33:18 +02:00
|
|
|
|
2022-05-14 12:11:39 +02:00
|
|
|
words = text.split(' ', Qt::SkipEmptyParts);
|
2021-04-17 12:33:18 +02:00
|
|
|
|
|
|
|
if (words.length() == 0)
|
|
|
|
{
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
commandName = words[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 20:22:47 +01:00
|
|
|
if (!dryRun)
|
2018-11-03 14:52:38 +01:00
|
|
|
{
|
2020-09-26 10:01:00 +02:00
|
|
|
// check if command exists
|
|
|
|
const auto it = this->commands_.find(commandName);
|
|
|
|
if (it != this->commands_.end())
|
|
|
|
{
|
2022-11-06 13:07:54 +01:00
|
|
|
if (auto *command = std::get_if<CommandFunction>(&it->second))
|
|
|
|
{
|
|
|
|
return (*command)(words, channel);
|
|
|
|
}
|
|
|
|
if (auto *command =
|
|
|
|
std::get_if<CommandFunctionWithContext>(&it->second))
|
|
|
|
{
|
|
|
|
CommandContext ctx{
|
|
|
|
words,
|
|
|
|
channel,
|
|
|
|
dynamic_cast<TwitchChannel *>(channel.get()),
|
|
|
|
};
|
|
|
|
return (*command)(ctx);
|
|
|
|
}
|
|
|
|
|
|
|
|
return "";
|
2020-09-26 10:01:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 20:19:18 +01:00
|
|
|
// We have checks to ensure words cannot be empty, so this can never wrap around
|
|
|
|
auto maxSpaces = std::min(this->maxSpaces_, (qsizetype)words.length() - 1);
|
2018-11-03 14:52:38 +01:00
|
|
|
for (int i = 0; i < maxSpaces; ++i)
|
|
|
|
{
|
|
|
|
commandName += ' ' + words[i + 1];
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2020-09-26 10:01:00 +02:00
|
|
|
const auto it = this->userCommands_.find(commandName);
|
|
|
|
if (it != this->userCommands_.end())
|
2018-07-06 18:10:21 +02:00
|
|
|
{
|
2021-09-11 14:35:26 +02:00
|
|
|
return this->execCustomCommand(words, it.value(), dryRun, channel);
|
2018-04-30 23:30:05 +02:00
|
|
|
}
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-01-15 12:50:03 +01:00
|
|
|
if (!dryRun && channel->getType() == Channel::Type::TwitchWhispers)
|
|
|
|
{
|
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage("Use /w <username> <message> to whisper"));
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2018-11-03 14:52:38 +01:00
|
|
|
return text;
|
2018-04-30 23:30:05 +02:00
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2023-04-02 15:31:53 +02:00
|
|
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
|
|
|
bool CommandController::registerPluginCommand(const QString &commandName)
|
|
|
|
{
|
|
|
|
if (this->commands_.contains(commandName))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this->commands_[commandName] = [commandName](const CommandContext &ctx) {
|
|
|
|
return getApp()->plugins->tryExecPluginCommand(commandName, ctx);
|
|
|
|
};
|
|
|
|
this->pluginCommands_.append(commandName);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool CommandController::unregisterPluginCommand(const QString &commandName)
|
|
|
|
{
|
|
|
|
if (!this->pluginCommands_.contains(commandName))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
this->pluginCommands_.removeAll(commandName);
|
|
|
|
return this->commands_.erase(commandName) != 0;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2022-11-06 13:07:54 +01:00
|
|
|
void CommandController::registerCommand(const QString &commandName,
|
|
|
|
CommandFunctionVariants commandFunction)
|
2020-09-26 10:01:00 +02:00
|
|
|
{
|
2022-11-06 13:07:54 +01:00
|
|
|
assert(this->commands_.count(commandName) == 0);
|
2020-09-26 10:01:00 +02:00
|
|
|
|
2022-11-06 13:07:54 +01:00
|
|
|
this->commands_[commandName] = std::move(commandFunction);
|
2020-09-26 10:01:00 +02:00
|
|
|
|
2021-12-26 14:21:52 +01:00
|
|
|
this->defaultChatterinoCommandAutoCompletions_.append(commandName);
|
2020-09-26 10:01:00 +02:00
|
|
|
}
|
|
|
|
|
2022-06-25 14:06:16 +02:00
|
|
|
QString CommandController::execCustomCommand(
|
2022-10-08 13:11:55 +02:00
|
|
|
const QStringList &words, const Command &command, bool /* dryRun */,
|
2022-06-25 14:06:16 +02:00
|
|
|
ChannelPtr channel, const Message *message,
|
|
|
|
std::unordered_map<QString, QString> context)
|
2018-04-30 23:30:05 +02:00
|
|
|
{
|
|
|
|
QString result;
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-09-11 14:35:26 +02:00
|
|
|
static QRegularExpression parseCommand(
|
|
|
|
R"((^|[^{])({{)*{(\d+\+?|([a-zA-Z.-]+)(?:;(.+?))?)})");
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
int lastCaptureEnd = 0;
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
auto globalMatch = parseCommand.globalMatch(command.func);
|
|
|
|
int matchOffset = 0;
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
QRegularExpressionMatch match =
|
|
|
|
parseCommand.match(command.func, matchOffset);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
if (!match.hasMatch())
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
result += command.func.mid(lastCaptureEnd,
|
|
|
|
match.capturedStart() - lastCaptureEnd + 1);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
lastCaptureEnd = match.capturedEnd();
|
|
|
|
matchOffset = lastCaptureEnd - 1;
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
QString wordIndexMatch = match.captured(3);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
bool plus = wordIndexMatch.at(wordIndexMatch.size() - 1) == '+';
|
|
|
|
wordIndexMatch = wordIndexMatch.replace("+", "");
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
bool ok;
|
|
|
|
int wordIndex = wordIndexMatch.replace("=", "").toInt(&ok);
|
|
|
|
if (!ok || wordIndex == 0)
|
|
|
|
{
|
2021-09-11 14:35:26 +02:00
|
|
|
auto varName = match.captured(4);
|
|
|
|
auto altText = match.captured(5); // alt text or empty string
|
|
|
|
|
2022-05-29 14:23:29 +02:00
|
|
|
auto var = context.find(varName);
|
2021-09-11 14:35:26 +02:00
|
|
|
|
2022-05-29 14:23:29 +02:00
|
|
|
if (var != context.end())
|
2021-09-11 14:35:26 +02:00
|
|
|
{
|
2022-06-25 14:06:16 +02:00
|
|
|
// Found variable in `context`
|
2022-05-29 14:23:29 +02:00
|
|
|
result += var->second.isEmpty() ? altText : var->second;
|
2022-06-25 14:06:16 +02:00
|
|
|
continue;
|
2021-09-11 14:35:26 +02:00
|
|
|
}
|
2022-06-25 14:06:16 +02:00
|
|
|
|
|
|
|
auto it = COMMAND_VARS.find(varName);
|
|
|
|
if (it != COMMAND_VARS.end())
|
2021-09-11 14:35:26 +02:00
|
|
|
{
|
2022-06-25 14:06:16 +02:00
|
|
|
// Found variable in `COMMAND_VARS`
|
|
|
|
result += it->second(altText, channel, message);
|
|
|
|
continue;
|
2021-09-11 14:35:26 +02:00
|
|
|
}
|
2022-06-25 14:06:16 +02:00
|
|
|
|
|
|
|
// Fall back to replacing it with the actual matched string
|
|
|
|
result += "{" + match.captured(3) + "}";
|
2018-04-30 23:30:05 +02:00
|
|
|
continue;
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
if (words.length() <= wordIndex)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
if (plus)
|
|
|
|
{
|
|
|
|
bool first = true;
|
|
|
|
for (int i = wordIndex; i < words.length(); i++)
|
|
|
|
{
|
|
|
|
if (!first)
|
|
|
|
{
|
|
|
|
result += " ";
|
|
|
|
}
|
|
|
|
result += words[i];
|
|
|
|
first = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
result += words[wordIndex];
|
|
|
|
}
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
result += command.func.mid(lastCaptureEnd);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
if (result.size() > 0 && result.at(0) == '{')
|
|
|
|
{
|
|
|
|
result = result.mid(1);
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2022-10-08 13:11:55 +02:00
|
|
|
return result.replace("{{", "{");
|
2018-04-30 23:30:05 +02:00
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-12-26 14:21:52 +01:00
|
|
|
QStringList CommandController::getDefaultChatterinoCommandList()
|
2018-06-24 13:56:56 +02:00
|
|
|
{
|
2021-12-26 14:21:52 +01:00
|
|
|
return this->defaultChatterinoCommandAutoCompletions_;
|
2018-06-24 13:56:56 +02:00
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2018-04-30 23:30:05 +02:00
|
|
|
} // namespace chatterino
|