#include "singletons/NativeMessaging.hpp" #include "Application.hpp" #include "common/Literals.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" #include "debug/AssertInGuiThread.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Paths.hpp" #include "util/IpcQueue.hpp" #include "util/PostToThread.hpp" #include #include #include #include #include #include #include #ifdef Q_OS_WIN # include "widgets/AttachedWindow.hpp" #endif namespace { using namespace chatterino::literals; const QString EXTENSION_ID = u"glknmaideaikkmemifbfkhnomoknepka"_s; constexpr const size_t MESSAGE_SIZE = 1024; } // namespace namespace chatterino { using namespace literals; void registerNmManifest(const Paths &paths, const QString &manifestFilename, const QString ®istryKeyName, const QJsonDocument &document); void registerNmHost(const Paths &paths) { if (Modes::instance().isPortable) { return; } auto getBaseDocument = [] { return QJsonObject{ {u"name"_s, "com.chatterino.chatterino"_L1}, {u"description"_s, "Browser interaction with chatterino."_L1}, {u"path"_s, QCoreApplication::applicationFilePath()}, {u"type"_s, "stdio"_L1}, }; }; // chrome { auto obj = getBaseDocument(); QJsonArray allowedOriginsArr = { u"chrome-extension://%1/"_s.arg(EXTENSION_ID)}; obj.insert("allowed_origins", allowedOriginsArr); registerNmManifest(paths, "/native-messaging-manifest-chrome.json", "HKCU\\Software\\Google\\Chrome\\NativeMessagingHost" "s\\com.chatterino.chatterino", QJsonDocument(obj)); } // firefox { auto obj = getBaseDocument(); QJsonArray allowedExtensions = {"chatterino_native@chatterino.com"}; obj.insert("allowed_extensions", allowedExtensions); registerNmManifest(paths, "/native-messaging-manifest-firefox.json", "HKCU\\Software\\Mozilla\\NativeMessagingHosts\\com." "chatterino.chatterino", QJsonDocument(obj)); } } void registerNmManifest(const Paths &paths, const QString &manifestFilename, const QString ®istryKeyName, const QJsonDocument &document) { (void)registryKeyName; // save the manifest QString manifestPath = paths.miscDirectory + manifestFilename; QFile file(manifestPath); file.open(QIODevice::WriteOnly | QIODevice::Truncate); file.write(document.toJson()); file.flush(); #ifdef Q_OS_WIN QSettings registry(registryKeyName, QSettings::NativeFormat); registry.setValue("Default", manifestPath); #endif } std::string &getNmQueueName(const Paths &paths) { static std::string name = "chatterino_gui" + paths.applicationFilePathHash.toStdString(); return name; } // CLIENT namespace nm::client { void sendMessage(const QByteArray &array) { ipc::sendMessage("chatterino_gui", array); } void writeToCout(const QByteArray &array) { const auto *data = array.data(); auto size = uint32_t(array.size()); // We're writing the raw bytes to cout. // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) std::cout.write(reinterpret_cast(&size), 4); std::cout.write(data, size); std::cout.flush(); } } // namespace nm::client // SERVER NativeMessagingServer::NativeMessagingServer() : thread(*this) { } void NativeMessagingServer::start() { this->thread.start(); } NativeMessagingServer::ReceiverThread::ReceiverThread( NativeMessagingServer &parent) : parent_(parent) { } void NativeMessagingServer::ReceiverThread::run() { auto [messageQueue, error] = ipc::IpcQueue::tryReplaceOrCreate("chatterino_gui", 100, MESSAGE_SIZE); if (!error.isEmpty()) { qCDebug(chatterinoNativeMessage) << "Failed to create message queue:" << error; nmIpcError().set(error); return; } while (true) { auto buf = messageQueue->receive(); if (buf.isEmpty()) { continue; } auto document = QJsonDocument::fromJson(buf); this->handleMessage(document.object()); } } void NativeMessagingServer::ReceiverThread::handleMessage( const QJsonObject &root) { QString action = root["action"_L1].toString(); if (action == "select") { this->handleSelect(root); return; } if (action == "detach") { this->handleDetach(root); return; } if (action == "sync") { this->handleSync(root); return; } qCDebug(chatterinoNativeMessage) << "NM unknown action" << action; } // NOLINTBEGIN(readability-convert-member-functions-to-static) void NativeMessagingServer::ReceiverThread::handleSelect( const QJsonObject &root) { QString type = root["type"_L1].toString(); bool attach = root["attach"_L1].toBool(); bool attachFullscreen = root["attach_fullscreen"_L1].toBool(); QString name = root["name"_L1].toString(); #ifdef USEWINSDK const auto sizeObject = root["size"_L1].toObject(); AttachedWindow::GetArgs args = { .winId = root["winId"_L1].toString(), .yOffset = root["yOffset"_L1].toInt(-1), .x = sizeObject["x"_L1].toDouble(-1.0), .pixelRatio = sizeObject["pixelRatio"_L1].toDouble(-1.0), .width = sizeObject["width"_L1].toInt(-1), .height = sizeObject["height"_L1].toInt(-1), .fullscreen = attachFullscreen, }; qCDebug(chatterinoNativeMessage) << args.x << args.pixelRatio << args.width << args.height << args.winId; if (args.winId.isNull()) { qCDebug(chatterinoNativeMessage) << "winId in select is missing"; return; } #endif if (type != u"twitch"_s) { qCDebug(chatterinoNativeMessage) << "NM unknown channel type"; return; } postToThread([=] { if (!name.isEmpty()) { auto channel = getApp()->getTwitchAbstract()->getOrAddChannel(name); if (getApp()->getTwitch()->getWatchingChannel().get() != channel) { getApp()->getTwitch()->setWatchingChannel(channel); } } if (attach || attachFullscreen) { #ifdef USEWINSDK auto *window = AttachedWindow::getForeground(args); if (!name.isEmpty()) { window->setChannel( getApp()->getTwitchAbstract()->getOrAddChannel(name)); } #endif } }); } void NativeMessagingServer::ReceiverThread::handleDetach( const QJsonObject &root) { QString winId = root["winId"_L1].toString(); if (winId.isNull()) { qCDebug(chatterinoNativeMessage) << "NM winId missing"; return; } #ifdef USEWINSDK postToThread([winId] { qCDebug(chatterinoNativeMessage) << "NW detach"; AttachedWindow::detach(winId); }); #endif } // NOLINTEND(readability-convert-member-functions-to-static) void NativeMessagingServer::ReceiverThread::handleSync(const QJsonObject &root) { // Structure: // { action: 'sync', twitchChannels?: string[] } postToThread([&parent = this->parent_, twitch = root["twitchChannels"_L1].toArray()] { parent.syncChannels(twitch); }); } void NativeMessagingServer::syncChannels(const QJsonArray &twitchChannels) { assertInGuiThread(); std::vector updated; updated.reserve(twitchChannels.size()); for (const auto &value : twitchChannels) { auto name = value.toString(); if (name.isEmpty()) { continue; } // the deduping is done on the extension side updated.emplace_back( getApp()->getTwitchAbstract()->getOrAddChannel(name)); } // This will destroy channels that aren't used anymore. this->channelWarmer_ = std::move(updated); } Atomic> &nmIpcError() { static Atomic> x; return x; } } // namespace chatterino