#include "CommandController.hpp" #include "Application.hpp" #include "common/SignalVector.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/Command.hpp" #include "controllers/commands/CommandModel.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/TwitchApi.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchServer.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "util/CombinePath.hpp" #include "widgets/dialogs/LogsPopup.hpp" #include #include #include #define TWITCH_DEFAULT_COMMANDS \ { \ "/help", "/w", "/me", "/disconnect", "/mods", "/color", "/ban", \ "/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \ "/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \ "/clear", "/subscribers", "/subscribersoff", "/followers", \ "/followersoff" \ } namespace chatterino { CommandController::CommandController() { auto addFirstMatchToMap = [this](auto args) { this->commandsMap_.remove(args.item.name); for (const Command &cmd : this->items.getVector()) { if (cmd.name == args.item.name) { this->commandsMap_[cmd.name] = cmd; break; } } }; this->items.itemInserted.connect(addFirstMatchToMap); this->items.itemRemoved.connect(addFirstMatchToMap); } void CommandController::initialize(Settings &, Paths &paths) { this->load(paths); } void CommandController::load(Paths &paths) { this->filePath_ = combinePath(paths.settingsDirectory, "commands.txt"); QFile textFile(this->filePath_); if (!textFile.open(QIODevice::ReadOnly)) { // No commands file created yet return; } QList test = textFile.readAll().split('\n'); for (const auto &command : test) { if (command.isEmpty()) { continue; } this->items.appendItem(Command(command)); } textFile.close(); } void CommandController::save() { QFile textFile(this->filePath_); if (!textFile.open(QIODevice::WriteOnly)) { log("[CommandController::saveCommands] Unable to open {} for writing", this->filePath_); return; } for (const Command &cmd : this->items.getVector()) { textFile.write((cmd.toString() + "\n").toUtf8()); } textFile.close(); } CommandModel *CommandController::createModel(QObject *parent) { CommandModel *model = new CommandModel(parent); model->init(&this->items); return model; } QString CommandController::execCommand(const QString &text, ChannelPtr channel, bool dryRun) { QStringList words = text.split(' ', QString::SkipEmptyParts); Command command; { std::lock_guard lock(this->mutex_); if (words.length() == 0) { return text; } QString commandName = words[0]; // works in a valid twitch channel and /whispers, etc... if (!dryRun && channel->isTwitchChannel()) { if (commandName == "/w") { if (words.length() <= 2) { return ""; } auto app = getApp(); MessageBuilder b; b.emplace(); b.emplace( app->accounts->twitch.getCurrent()->getUserName(), MessageElementFlag::Text, MessageColor::Text, FontStyle::ChatMediumBold); b.emplace("->", MessageElementFlag::Text); b.emplace(words[1] + ":", MessageElementFlag::Text, MessageColor::Text, FontStyle::ChatMediumBold); QString rest = ""; for (int i = 2; i < words.length(); i++) { rest += words[i] + " "; } b.emplace(rest, MessageElementFlag::Text); b->flags.set(MessageFlag::DoNotTriggerNotification); auto messagexD = b.release(); app->twitch.server->whispersChannel->addMessage(messagexD); app->twitch.server->sendMessage("jtv", text); if (getSettings()->inlineWhispers) { app->twitch.server->forEachChannel( [&messagexD](ChannelPtr _channel) { _channel->addMessage(messagexD); }); } return ""; } } // check if default command exists auto *twitchChannel = dynamic_cast(channel.get()); // works only in a valid twitch channel if (!dryRun && twitchChannel != nullptr) { if (commandName == "/debug-args") { QString msg = QApplication::instance()->arguments().join(' '); channel->addMessage(makeSystemMessage(msg)); return ""; } else if (commandName == "/uptime") { const auto &streamStatus = twitchChannel->accessStreamStatus(); QString messageText = streamStatus->live ? streamStatus->uptime : "Channel is not live."; channel->addMessage(makeSystemMessage(messageText)); return ""; } else if (commandName == "/ignore") { if (words.size() < 2) { channel->addMessage( makeSystemMessage("Usage: /ignore [user]")); return ""; } auto app = getApp(); auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { channel->addMessage(makeSystemMessage( "You must be logged in to ignore someone")); return ""; } user->ignore( target, [channel](auto resultCode, const QString &message) { channel->addMessage(makeSystemMessage(message)); }); return ""; } else if (commandName == "/unignore") { if (words.size() < 2) { channel->addMessage( makeSystemMessage("Usage: /unignore [user]")); return ""; } auto app = getApp(); auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { channel->addMessage(makeSystemMessage( "You must be logged in to ignore someone")); return ""; } user->unignore( target, [channel](auto resultCode, const QString &message) { channel->addMessage(makeSystemMessage(message)); }); return ""; } else if (commandName == "/follow") { if (words.size() < 2) { channel->addMessage( makeSystemMessage("Usage: /follow [user]")); return ""; } auto app = getApp(); auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { channel->addMessage(makeSystemMessage( "You must be logged in to follow someone")); return ""; } TwitchApi::findUserId( target, [user, channel, target](QString userId) { if (userId.isEmpty()) { channel->addMessage(makeSystemMessage( "User " + target + " could not be followed!")); return; } user->followUser(userId, [channel, target]() { channel->addMessage(makeSystemMessage( "You successfully followed " + target)); }); }); return ""; } else if (commandName == "/unfollow") { if (words.size() < 2) { channel->addMessage( makeSystemMessage("Usage: /unfollow [user]")); return ""; } auto app = getApp(); auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { channel->addMessage(makeSystemMessage( "You must be logged in to follow someone")); return ""; } TwitchApi::findUserId( target, [user, channel, target](QString userId) { if (userId.isEmpty()) { channel->addMessage(makeSystemMessage( "User " + target + " could not be followed!")); return; } user->unfollowUser(userId, [channel, target]() { channel->addMessage(makeSystemMessage( "You successfully unfollowed " + target)); }); }); return ""; } else if (commandName == "/logs") { if (words.size() < 2) { channel->addMessage( makeSystemMessage("Usage: /logs [user] (channel)")); return ""; } auto app = getApp(); auto logs = new LogsPopup(); QString target; if (words.at(1).at(0) == "@") { target = words.at(1).mid(1); } else { target = words.at(1); } if (words.size() == 3) { QString channelName = words.at(2); if (words.at(2).at(0) == "#") { channelName = words.at(2).mid(1); } auto logsChannel = app->twitch.server->getChannelOrEmpty(channelName); if (logsChannel == nullptr) { } else { logs->setInfo(logsChannel, target); } } else { logs->setInfo(channel, target); } logs->setAttribute(Qt::WA_DeleteOnClose); logs->show(); return ""; } } // check if custom command exists auto it = this->commandsMap_.find(commandName); if (it == this->commandsMap_.end()) { return text; } command = it.value(); } return this->execCustomCommand(words, command); } QString CommandController::execCustomCommand(const QStringList &words, const Command &command) { QString result; static QRegularExpression parseCommand("(^|[^{])({{)*{(\\d+\\+?)}"); int lastCaptureEnd = 0; auto globalMatch = parseCommand.globalMatch(command.func); int matchOffset = 0; while (true) { QRegularExpressionMatch match = parseCommand.match(command.func, matchOffset); if (!match.hasMatch()) { break; } result += command.func.mid(lastCaptureEnd, match.capturedStart() - lastCaptureEnd + 1); lastCaptureEnd = match.capturedEnd(); matchOffset = lastCaptureEnd - 1; QString wordIndexMatch = match.captured(3); bool plus = wordIndexMatch.at(wordIndexMatch.size() - 1) == '+'; wordIndexMatch = wordIndexMatch.replace("+", ""); bool ok; int wordIndex = wordIndexMatch.replace("=", "").toInt(&ok); if (!ok || wordIndex == 0) { result += "{" + match.captured(3) + "}"; continue; } if (words.length() <= wordIndex) { continue; } 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]; } } result += command.func.mid(lastCaptureEnd); if (result.size() > 0 && result.at(0) == '{') { result = result.mid(1); } return result.replace("{{", "{"); } QStringList CommandController::getDefaultTwitchCommandList() { QStringList l = TWITCH_DEFAULT_COMMANDS; l += "/uptime"; return l; } } // namespace chatterino