diff --git a/CHANGELOG.md b/CHANGELOG.md index 220d48e80..b971a6597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Add `{channel.name}`, `{channel.id}`, `{stream.game}`, `{stream.title}`, `{my.id}`, `{my.name}` placeholders for commands (#3155) - Minor: Remove TwitchEmotes.com attribution and the open/copy options when right-clicking a Twitch Emote. (#2214, #3136) - Minor: Strip leading @ and trailing , from username in /user and /usercard commands. (#3143) - Minor: Display a system message when reloading subscription emotes to match BTTV/FFZ behavior (#3135) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index f34ec525a..cf71b3942 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -226,6 +226,72 @@ bool appendWhisperMessageStringLocally(const QString &textNoEmoji) } return false; } + +const std::map> + COMMAND_VARS{ + { + "channel.name", + [](const auto &altText, const auto &channel) { + (void)(altText); //unused + return channel->getName(); + }, + }, + { + "channel.id", + [](const auto &altText, const auto &channel) { + auto *tc = dynamic_cast(channel.get()); + if (tc == nullptr) + { + return altText; + } + + return tc->roomId(); + }, + }, + { + "stream.game", + [](const auto &altText, const auto &channel) { + auto *tc = dynamic_cast(channel.get()); + if (tc == nullptr) + { + return altText; + } + const auto &status = tc->accessStreamStatus(); + return status->live ? status->game : altText; + }, + }, + { + "stream.title", + [](const auto &altText, const auto &channel) { + auto *tc = dynamic_cast(channel.get()); + if (tc == nullptr) + { + return altText; + } + const auto &status = tc->accessStreamStatus(); + return status->live ? status->title : altText; + }, + }, + { + "my.id", + [](const auto &altText, const auto &channel) { + (void)(channel); //unused + auto uid = getApp()->accounts->twitch.getCurrent()->getUserId(); + return uid.isEmpty() ? altText : uid; + }, + }, + { + "my.name", + [](const auto &altText, const auto &channel) { + (void)(channel); //unused + auto name = + getApp()->accounts->twitch.getCurrent()->getUserName(); + return name.isEmpty() ? altText : name; + }, + }, + }; + } // namespace namespace chatterino { @@ -855,7 +921,7 @@ QString CommandController::execCommand(const QString &textNoEmoji, if (it != this->userCommands_.end()) { text = getApp()->emotes->emojis.replaceShortCodes( - this->execCustomCommand(words, it.value(), dryRun)); + this->execCustomCommand(words, it.value(), dryRun, channel)); words = text.split(' ', QString::SkipEmptyParts); @@ -887,7 +953,7 @@ QString CommandController::execCommand(const QString &textNoEmoji, const auto it = this->userCommands_.find(commandName); if (it != this->userCommands_.end()) { - return this->execCustomCommand(words, it.value(), dryRun); + return this->execCustomCommand(words, it.value(), dryRun, channel); } } @@ -906,11 +972,13 @@ void CommandController::registerCommand(QString commandName, QString CommandController::execCustomCommand(const QStringList &words, const Command &command, - bool dryRun) + bool dryRun, ChannelPtr channel, + std::map context) { QString result; - static QRegularExpression parseCommand("(^|[^{])({{)*{(\\d+\\+?)}"); + static QRegularExpression parseCommand( + R"((^|[^{])({{)*{(\d+\+?|([a-zA-Z.-]+)(?:;(.+?))?)})"); int lastCaptureEnd = 0; @@ -942,7 +1010,27 @@ QString CommandController::execCustomCommand(const QStringList &words, int wordIndex = wordIndexMatch.replace("=", "").toInt(&ok); if (!ok || wordIndex == 0) { - result += "{" + match.captured(3) + "}"; + auto varName = match.captured(4); + auto altText = match.captured(5); // alt text or empty string + + auto var = COMMAND_VARS.find(varName); + + if (var != COMMAND_VARS.end()) + { + result += var->second(altText, channel); + } + else + { + auto it = context.find(varName); + if (it != context.end()) + { + result += it->second.isEmpty() ? altText : it->second; + } + else + { + result += "{" + match.captured(3) + "}"; + } + } continue; } diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index 1d3c25557..427c146e8 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -34,6 +34,10 @@ public: CommandModel *createModel(QObject *parent); + QString execCustomCommand(const QStringList &words, const Command &command, + bool dryRun, ChannelPtr channel, + std::map context = {}); + private: void load(Paths &paths); @@ -57,9 +61,6 @@ private: std::unique_ptr>> commandsSetting_; - QString execCustomCommand(const QStringList &words, const Command &command, - bool dryRun); - QStringList commandAutoCompletions_; }; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 064a25cd9..35606508c 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -2101,12 +2101,24 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link, } } - value.replace("{user}", layout->getMessage()->loginName) - .replace("{channel}", this->channel_->getName()) - .replace("{msg-id}", layout->getMessage()->id) - .replace("{message}", layout->getMessage()->messageText); + value = getApp()->commands->execCustomCommand( + QStringList(), Command{"(modaction)", value}, true, channel, + { + {"user.name", layout->getMessage()->loginName}, + {"msg.id", layout->getMessage()->id}, + {"msg.text", layout->getMessage()->messageText}, + + // old placeholders + {"user", layout->getMessage()->loginName}, + {"msg-id", layout->getMessage()->id}, + {"message", layout->getMessage()->messageText}, + + // new version of this is inside execCustomCommand + {"channel", this->channel()->getName()}, + }); value = getApp()->commands->execCommand(value, channel, false); + channel->sendMessage(value); } break; diff --git a/src/widgets/settingspages/ModerationPage.cpp b/src/widgets/settingspages/ModerationPage.cpp index 020732b92..2b7e41ab7 100644 --- a/src/widgets/settingspages/ModerationPage.cpp +++ b/src/widgets/settingspages/ModerationPage.cpp @@ -157,8 +157,8 @@ ModerationPage::ModerationPage() // clang-format off auto label = modMode.emplace( "Moderation mode is enabled by clicking in a channel that you moderate.

" - "Moderation buttons can be bound to chat commands such as \"/ban {user}\", \"/timeout {user} 1000\", \"/w someusername !report {user} was bad in channel {channel}\" or any other custom text commands.
" - "For deleting messages use /delete {msg-id}.

" + "Moderation buttons can be bound to chat commands such as \"/ban {user.name}\", \"/timeout {user.name} 1000\", \"/w someusername !report {user.name} was bad in channel {channel.name}\" or any other custom text commands.
" + "For deleting messages use /delete {msg.id}.

" "More information can be found here."); label->setOpenExternalLinks(true); label->setWordWrap(true); @@ -188,7 +188,7 @@ ModerationPage::ModerationPage() view->addButtonPressed.connect([] { getSettings()->moderationActions.append( - ModerationAction("/timeout {user} 300")); + ModerationAction("/timeout {user.name} 300")); }); }