diff --git a/.clang-tidy b/.clang-tidy index 42cca83f6..b5e8bef8c 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -73,3 +73,6 @@ CheckOptions: - key: misc-const-correctness.AnalyzeValues value: false + + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: true diff --git a/mocks/include/mocks/BaseApplication.hpp b/mocks/include/mocks/BaseApplication.hpp index 3f9df4bb5..2ba9f949c 100644 --- a/mocks/include/mocks/BaseApplication.hpp +++ b/mocks/include/mocks/BaseApplication.hpp @@ -20,6 +20,7 @@ class BaseApplication : public EmptyApplication public: BaseApplication() : settings(this->args, this->settingsDir.path()) + , updates(this->paths_, this->settings) , theme(this->paths_) , fonts(this->settings) { @@ -28,11 +29,17 @@ public: explicit BaseApplication(const QString &settingsData) : EmptyApplication(settingsData) , settings(this->args, this->settingsDir.path()) + , updates(this->paths_, this->settings) , theme(this->paths_) , fonts(this->settings) { } + Updates &getUpdates() override + { + return this->updates; + } + IStreamerMode *getStreamerMode() override { return &this->streamerMode; @@ -60,6 +67,7 @@ public: Args args; Settings settings; + Updates updates; DisabledStreamerMode streamerMode; Theme theme; Fonts fonts; diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 63f928537..947a55f3d 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -12,13 +12,9 @@ namespace chatterino::mock { class EmptyApplication : public IApplication { public: - EmptyApplication() - : updates_(this->paths_) - { - } + EmptyApplication() = default; explicit EmptyApplication(const QString &settingsData) - : EmptyApplication() { QFile settingsFile(this->settingsDir.filePath("settings.json")); settingsFile.open(QIODevice::WriteOnly | QIODevice::Text); @@ -212,11 +208,6 @@ public: } #endif - Updates &getUpdates() override - { - return this->updates_; - } - BttvEmotes *getBttvEmotes() override { assert(false && "EmptyApplication::getBttvEmotes was called without " @@ -269,7 +260,6 @@ public: QTemporaryDir settingsDir; Paths paths_; Args args_; - Updates updates_; }; } // namespace chatterino::mock diff --git a/src/Application.cpp b/src/Application.cpp index c8c1bf5d6..735c226ea 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -61,10 +61,9 @@ #include "widgets/Window.hpp" #include +#include #include -#include - namespace { using namespace chatterino; @@ -128,8 +127,6 @@ IApplication *INSTANCE = nullptr; namespace chatterino { -static std::atomic isAppInitialized{false}; - IApplication::IApplication() { INSTANCE = this; @@ -194,8 +191,7 @@ Application::~Application() void Application::initialize(Settings &settings, const Paths &paths) { - assert(isAppInitialized == false); - isAppInitialized = true; + assert(!this->initialized); // Show changelog if (!this->args_.isFramelessEmbed && @@ -271,17 +267,19 @@ void Application::initialize(Settings &settings, const Paths &paths) { this->initNm(paths); } - this->initPubSub(); + this->twitchPubSub->initialize(); this->initBttvLiveUpdates(); this->initSeventvEventAPI(); this->streamerMode->start(); + + this->initialized = true; } -int Application::run(QApplication &qtApp) +int Application::run() { - assert(isAppInitialized); + assert(this->initialized); this->twitch->connect(); @@ -290,44 +288,23 @@ int Application::run(QApplication &qtApp) this->windows->getMainWindow().show(); } - getSettings()->betaUpdates.connect( - [this] { - this->updates.checkForUpdates(); - }, - false); - - getSettings()->enableBTTVGlobalEmotes.connect( - [this] { - this->bttvEmotes->loadEmotes(); - }, - false); getSettings()->enableBTTVChannelEmotes.connect( [this] { this->twitch->reloadAllBTTVChannelEmotes(); }, false); - getSettings()->enableFFZGlobalEmotes.connect( - [this] { - this->ffzEmotes->loadEmotes(); - }, - false); getSettings()->enableFFZChannelEmotes.connect( [this] { this->twitch->reloadAllFFZChannelEmotes(); }, false); - getSettings()->enableSevenTVGlobalEmotes.connect( - [this] { - this->seventvEmotes->loadGlobalEmotes(); - }, - false); getSettings()->enableSevenTVChannelEmotes.connect( [this] { this->twitch->reloadAllSevenTVChannelEmotes(); }, false); - return qtApp.exec(); + return QApplication::exec(); } Theme *Application::getThemes() @@ -597,455 +574,6 @@ void Application::initNm(const Paths &paths) #endif } -void Application::initPubSub() -{ - // We can safely ignore these signal connections since the twitch object will always - // be destroyed before the Application - std::ignore = this->twitchPubSub->moderation.chatCleared.connect( - [this](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - QString text = - QString("%1 cleared the chat.").arg(action.source.login); - - postToThread([chan, text] { - chan->addSystemMessage(text); - }); - }); - - std::ignore = this->twitchPubSub->moderation.modeChanged.connect( - [this](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - QString text = - QString("%1 turned %2 %3 mode.") - .arg(action.source.login) - .arg(action.state == ModeChangedAction::State::On ? "on" - : "off") - .arg(action.getModeName()); - - if (action.duration > 0) - { - text += QString(" (%1 seconds)").arg(action.duration); - } - - postToThread([chan, text] { - chan->addSystemMessage(text); - }); - }); - - std::ignore = this->twitchPubSub->moderation.moderationStateChanged.connect( - [this](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - QString text; - - text = QString("%1 %2 %3.") - .arg(action.source.login, - (action.modded ? "modded" : "unmodded"), - action.target.login); - - postToThread([chan, text] { - chan->addSystemMessage(text); - }); - }); - - std::ignore = this->twitchPubSub->moderation.userBanned.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - MessageBuilder msg(action); - msg->flags.set(MessageFlag::PubSub); - chan->addOrReplaceTimeout(msg.release()); - }); - }); - - std::ignore = this->twitchPubSub->moderation.userWarned.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - // TODO: Resolve the moderator's user ID into a full user here, so message can look better - postToThread([chan, action] { - MessageBuilder msg(action); - msg->flags.set(MessageFlag::PubSub); - chan->addMessage(msg.release(), MessageContext::Original); - }); - }); - - std::ignore = this->twitchPubSub->moderation.messageDeleted.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty() || getSettings()->hideDeletionActions) - { - return; - } - - auto msg = MessageBuilder::makeDeletionMessageFromPubSub(action); - - postToThread([chan, msg] { - auto replaced = false; - LimitedQueueSnapshot snapshot = - chan->getMessageSnapshot(); - int snapshotLength = snapshot.size(); - - // without parens it doesn't build on windows - int end = (std::max)(0, snapshotLength - 200); - - for (int i = snapshotLength - 1; i >= end; --i) - { - const auto &s = snapshot[i]; - if (!s->flags.has(MessageFlag::PubSub) && - s->timeoutUser == msg->timeoutUser) - { - chan->replaceMessage(s, msg); - replaced = true; - break; - } - } - if (!replaced) - { - chan->addMessage(msg, MessageContext::Original); - } - }); - }); - - std::ignore = this->twitchPubSub->moderation.userUnbanned.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - auto msg = MessageBuilder(action).release(); - - postToThread([chan, msg] { - chan->addMessage(msg, MessageContext::Original); - }); - }); - - std::ignore = - this->twitchPubSub->moderation.suspiciousMessageReceived.connect( - [&](const auto &action) { - if (action.treatment == - PubSubLowTrustUsersMessage::Treatment::INVALID) - { - qCWarning(chatterinoTwitch) - << "Received suspicious message with unknown " - "treatment:" - << action.treatmentString; - return; - } - - // monitored chats are received over irc; in the future, we will use pubsub instead - if (action.treatment != - PubSubLowTrustUsersMessage::Treatment::Restricted) - { - return; - } - - if (getSettings()->streamerModeHideModActions && - this->getStreamerMode()->isEnabled()) - { - return; - } - - auto chan = - this->twitch->getChannelOrEmptyByID(action.channelID); - - if (chan->isEmpty()) - { - return; - } - - auto twitchChannel = - std::dynamic_pointer_cast(chan); - if (!twitchChannel) - { - return; - } - - postToThread([twitchChannel, action] { - const auto p = MessageBuilder::makeLowTrustUserMessage( - action, twitchChannel->getName(), twitchChannel.get()); - twitchChannel->addMessage(p.first, - MessageContext::Original); - twitchChannel->addMessage(p.second, - MessageContext::Original); - }); - }); - - std::ignore = - this->twitchPubSub->moderation.suspiciousTreatmentUpdated.connect( - [&](const auto &action) { - if (action.treatment == - PubSubLowTrustUsersMessage::Treatment::INVALID) - { - qCWarning(chatterinoTwitch) - << "Received suspicious user update with unknown " - "treatment:" - << action.treatmentString; - return; - } - - if (action.updatedByUserLogin.isEmpty()) - { - return; - } - - if (getSettings()->streamerModeHideModActions && - this->getStreamerMode()->isEnabled()) - { - return; - } - - auto chan = - this->twitch->getChannelOrEmptyByID(action.channelID); - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - auto msg = - MessageBuilder::makeLowTrustUpdateMessage(action); - chan->addMessage(msg, MessageContext::Original); - }); - }); - - std::ignore = this->twitchPubSub->moderation.autoModMessageCaught.connect( - [&](const auto &msg, const QString &channelID) { - auto chan = this->twitch->getChannelOrEmptyByID(channelID); - if (chan->isEmpty()) - { - return; - } - - switch (msg.type) - { - case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: { - if (msg.status == "PENDING") - { - AutomodAction action(msg.data, channelID); - action.reason = QString("%1 level %2") - .arg(msg.contentCategory) - .arg(msg.contentLevel); - - action.msgID = msg.messageID; - action.message = msg.messageText; - - // this message also contains per-word automod data, which could be implemented - - // extract sender data manually because Twitch loves not being consistent - QString senderDisplayName = - msg.senderUserDisplayName; // Might be transformed later - bool hasLocalizedName = false; - if (!msg.senderUserDisplayName.isEmpty()) - { - // check for non-ascii display names - if (QString::compare(msg.senderUserDisplayName, - msg.senderUserLogin, - Qt::CaseInsensitive) != 0) - { - hasLocalizedName = true; - } - } - QColor senderColor = msg.senderUserChatColor; - QString senderColor_; - if (!senderColor.isValid() && - getSettings()->colorizeNicknames) - { - // color may be not present if user is a grey-name - senderColor = getRandomColor(msg.senderUserID); - } - - // handle username style based on prefered setting - switch (getSettings()->usernameDisplayMode.getValue()) - { - case UsernameDisplayMode::Username: { - if (hasLocalizedName) - { - senderDisplayName = msg.senderUserLogin; - } - break; - } - case UsernameDisplayMode::LocalizedName: { - break; - } - case UsernameDisplayMode:: - UsernameAndLocalizedName: { - if (hasLocalizedName) - { - senderDisplayName = QString("%1(%2)").arg( - msg.senderUserLogin, - msg.senderUserDisplayName); - } - break; - } - } - - action.target = - ActionUser{msg.senderUserID, msg.senderUserLogin, - senderDisplayName, senderColor}; - postToThread([chan, action] { - const auto p = MessageBuilder::makeAutomodMessage( - action, chan->getName()); - chan->addMessage(p.first, MessageContext::Original); - chan->addMessage(p.second, - MessageContext::Original); - - getApp() - ->getTwitch() - ->getAutomodChannel() - ->addMessage(p.first, MessageContext::Original); - getApp() - ->getTwitch() - ->getAutomodChannel() - ->addMessage(p.second, - MessageContext::Original); - - if (getSettings()->showAutomodInMentions) - { - getApp() - ->getTwitch() - ->getMentionsChannel() - ->addMessage(p.first, - MessageContext::Original); - getApp() - ->getTwitch() - ->getMentionsChannel() - ->addMessage(p.second, - MessageContext::Original); - } - }); - } - // "ALLOWED" and "DENIED" statuses remain unimplemented - // They are versions of automod_message_(denied|approved) but for mods. - } - break; - - case PubSubAutoModQueueMessage::Type::INVALID: - default: { - } - break; - } - }); - - std::ignore = this->twitchPubSub->moderation.autoModMessageBlocked.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - const auto p = - MessageBuilder::makeAutomodMessage(action, chan->getName()); - chan->addMessage(p.first, MessageContext::Original); - chan->addMessage(p.second, MessageContext::Original); - }); - }); - - std::ignore = this->twitchPubSub->moderation.automodUserMessage.connect( - [&](const auto &action) { - if (getSettings()->streamerModeHideModActions && - this->getStreamerMode()->isEnabled()) - { - return; - } - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - auto msg = MessageBuilder(action).release(); - - postToThread([chan, msg] { - chan->addMessage(msg, MessageContext::Original); - }); - chan->deleteMessage(msg->id); - }); - - std::ignore = this->twitchPubSub->moderation.automodInfoMessage.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - const auto p = MessageBuilder::makeAutomodInfoMessage(action); - chan->addMessage(p, MessageContext::Original); - }); - }); - - std::ignore = - this->twitchPubSub->pointReward.redeemed.connect([&](auto &data) { - QString channelId = data.value("channel_id").toString(); - if (channelId.isEmpty()) - { - qCDebug(chatterinoApp) - << "Couldn't find channel id of point reward"; - return; - } - - auto chan = this->twitch->getChannelOrEmptyByID(channelId); - - auto reward = ChannelPointReward(data); - - postToThread([chan, reward] { - if (auto *channel = dynamic_cast(chan.get())) - { - channel->addChannelPointReward(reward); - } - }); - }); - - this->twitchPubSub->start(); - this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent()); - - this->accounts->twitch.currentUserChanged.connect( - [this] { - this->twitchPubSub->unlistenChannelModerationActions(); - this->twitchPubSub->unlistenAutomod(); - this->twitchPubSub->unlistenLowTrustUsers(); - this->twitchPubSub->unlistenChannelPointRewards(); - - this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent()); - }, - boost::signals2::at_front); -} - void Application::initBttvLiveUpdates() { if (!this->bttvLiveUpdates) diff --git a/src/Application.hpp b/src/Application.hpp index 7fe804ae2..c44e1c63e 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -2,8 +2,6 @@ #include "singletons/NativeMessaging.hpp" -#include - #include #include @@ -133,7 +131,7 @@ public: void load(); void save(); - int run(QApplication &qtApp); + int run(); friend void test(); @@ -219,13 +217,14 @@ public: IStreamerMode *getStreamerMode() override; private: - void initPubSub(); void initBttvLiveUpdates(); void initSeventvEventAPI(); void initNm(const Paths &paths); NativeMessagingServer nmServer; Updates &updates; + + bool initialized{false}; }; IApplication *getApp(); diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 62e010d9f..92da3dbf4 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -41,7 +41,7 @@ namespace { { // borrowed from // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows - auto dark = qApp->palette(); + auto dark = QApplication::palette(); dark.setColor(QPalette::Window, QColor(22, 22, 22)); dark.setColor(QPalette::WindowText, Qt::white); @@ -71,7 +71,7 @@ namespace { dark.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127)); - qApp->setPalette(dark); + QApplication::setPalette(dark); } void initQt() @@ -263,7 +263,7 @@ void runGui(QApplication &a, const Paths &paths, Settings &settings, Application app(settings, paths, args, updates); app.initialize(settings, paths); - app.run(a); + app.run(); app.save(); settings.requestSave(); diff --git a/src/common/ChatterSet.cpp b/src/common/ChatterSet.cpp index aa45b2367..e54ae6947 100644 --- a/src/common/ChatterSet.cpp +++ b/src/common/ChatterSet.cpp @@ -2,12 +2,10 @@ #include "debug/Benchmark.hpp" -#include - namespace chatterino { ChatterSet::ChatterSet() - : items(chatterLimit) + : items(ChatterSet::CHATTER_LIMIT) { } @@ -22,7 +20,7 @@ void ChatterSet::updateOnlineChatters( BenchmarkGuard bench("update online chatters"); // Create a new lru cache without the users that are not present anymore. - cache::lru_cache tmp(chatterLimit); + cache::lru_cache tmp(ChatterSet::CHATTER_LIMIT); for (auto &&chatter : lowerCaseUsernames) { @@ -32,7 +30,7 @@ void ChatterSet::updateOnlineChatters( // Less chatters than the limit => try to preserve as many as possible. } - else if (lowerCaseUsernames.size() < chatterLimit) + else if (lowerCaseUsernames.size() < ChatterSet::CHATTER_LIMIT) { tmp.put(chatter, chatter); } diff --git a/src/common/ChatterSet.hpp b/src/common/ChatterSet.hpp index f1345f435..c06d866ec 100644 --- a/src/common/ChatterSet.hpp +++ b/src/common/ChatterSet.hpp @@ -5,9 +5,6 @@ #include #include -#include -#include -#include #include #include @@ -19,7 +16,7 @@ class ChatterSet { public: /// The limit of how many chatters can be saved for a channel. - static constexpr size_t chatterLimit = 2000; + static constexpr size_t CHATTER_LIMIT = 2000; ChatterSet(); diff --git a/src/common/Credentials.cpp b/src/common/Credentials.cpp index 3c1c0ce04..1effa8f9b 100644 --- a/src/common/Credentials.cpp +++ b/src/common/Credentials.cpp @@ -8,6 +8,7 @@ #include "util/CombinePath.hpp" #include "util/Variant.hpp" +#include #include #include #include @@ -89,7 +90,7 @@ void queueInsecureSave() if (!isQueued) { isQueued = true; - QTimer::singleShot(200, qApp, [] { + QTimer::singleShot(200, QApplication::instance(), [] { storeInsecure(insecureInstance()); isQueued = false; }); @@ -133,8 +134,8 @@ void runNextJob() job->setAutoDelete(true); job->setKey(set.name); job->setTextData(set.credential); - QObject::connect(job, &QKeychain::Job::finished, qApp, - [](auto) { + QObject::connect(job, &QKeychain::Job::finished, + QApplication::instance(), [](auto) { runNextJob(); }); job->start(); @@ -143,8 +144,8 @@ void runNextJob() auto *job = new QKeychain::DeletePasswordJob("chatterino"); job->setAutoDelete(true); job->setKey(erase.name); - QObject::connect(job, &QKeychain::Job::finished, qApp, - [](auto) { + QObject::connect(job, &QKeychain::Job::finished, + QApplication::instance(), [](auto) { runNextJob(); }); job->start(); diff --git a/src/common/network/NetworkRequest.cpp b/src/common/network/NetworkRequest.cpp index f46da079a..9413943b4 100644 --- a/src/common/network/NetworkRequest.cpp +++ b/src/common/network/NetworkRequest.cpp @@ -4,6 +4,7 @@ #include "common/QLogging.hpp" #include "common/Version.hpp" +#include #include #include #include @@ -44,7 +45,7 @@ NetworkRequest NetworkRequest::caller(const QObject *caller) && if (caller) { // Caller must be in gui thread - assert(caller->thread() == qApp->thread()); + assert(caller->thread() == QApplication::instance()->thread()); this->data->caller = const_cast(caller); this->data->hasCaller = true; diff --git a/src/main.cpp b/src/main.cpp index 02dc4623e..4ece0deb8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,15 +110,15 @@ int main(int argc, char **argv) << QSslSocket::supportedProtocols(); #endif - Updates updates(*paths); + Settings settings(args, paths->settingsDirectory); + + Updates updates(*paths, settings); NetworkConfigurationProvider::applyFromEnv(Env::get()); IvrApi::initialize(); Helix::initialize(); - Settings settings(args, paths->settingsDirectory); - runGui(a, *paths, settings, args, updates); } return 0; diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 5966802c4..db3ecc13b 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -44,6 +44,7 @@ #include "widgets/Window.hpp" #include +#include #include #include #include diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 37cda37b8..60bd07b3c 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -15,82 +15,57 @@ #include #include -namespace chatterino { namespace { - const QString CHANNEL_HAS_NO_EMOTES( - "This channel has no BetterTTV channel emotes."); +using namespace chatterino; - QString emoteLinkFormat("https://betterttv.com/emotes/%1"); - // BTTV doesn't provide any data on the size, so we assume an emote is 28x28 - constexpr QSize EMOTE_BASE_SIZE(28, 28); +const QString CHANNEL_HAS_NO_EMOTES( + "This channel has no BetterTTV channel emotes."); - struct CreateEmoteResult { - EmoteId id; - EmoteName name; - Emote emote; - }; +/// The emote page template. +/// +/// %1 being the emote ID (e.g. 566ca04265dbbdab32ec054a) +constexpr QStringView EMOTE_LINK_FORMAT = u"https://betterttv.com/emotes/%1"; - Url getEmoteLink(QString urlTemplate, const EmoteId &id, - const QString &emoteScale) +/// The emote CDN link template. +/// +/// %1 being the emote ID (e.g. 566ca04265dbbdab32ec054a) +/// +/// %2 being the emote size (e.g. 3x) +constexpr QStringView EMOTE_CDN_FORMAT = + u"https://cdn.betterttv.net/emote/%1/%2"; + +// BTTV doesn't provide any data on the size, so we assume an emote is 28x28 +constexpr QSize EMOTE_BASE_SIZE(28, 28); + +struct CreateEmoteResult { + EmoteId id; + EmoteName name; + Emote emote; +}; + +Url getEmoteLinkV3(const EmoteId &id, const QString &emoteScale) +{ + return {EMOTE_CDN_FORMAT.arg(id.string, emoteScale)}; +} + +EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) +{ + static std::unordered_map> cache; + static std::mutex mutex; + + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); +} + +std::pair parseGlobalEmotes(const QJsonArray &jsonEmotes, + const EmoteMap ¤tEmotes) +{ + auto emotes = EmoteMap(); + + for (auto jsonEmote : jsonEmotes) { - urlTemplate.detach(); - - return {urlTemplate.replace("{{id}}", id.string) - .replace("{{image}}", emoteScale)}; - } - - Url getEmoteLinkV3(const EmoteId &id, const QString &emoteScale) - { - static const QString urlTemplate( - "https://cdn.betterttv.net/emote/%1/%2"); - - return {urlTemplate.arg(id.string, emoteScale)}; - } - EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) - { - static std::unordered_map> cache; - static std::mutex mutex; - - return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); - } - std::pair parseGlobalEmotes( - const QJsonArray &jsonEmotes, const EmoteMap ¤tEmotes) - { - auto emotes = EmoteMap(); - - for (auto jsonEmote : jsonEmotes) - { - auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; - auto name = - EmoteName{jsonEmote.toObject().value("code").toString()}; - - auto emote = Emote({ - name, - ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1, - EMOTE_BASE_SIZE), - Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5, - EMOTE_BASE_SIZE * 2), - Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25, - EMOTE_BASE_SIZE * 4)}, - Tooltip{name.string + "
Global BetterTTV Emote"}, - Url{emoteLinkFormat.arg(id.string)}, - }); - - emotes[name] = - cachedOrMakeEmotePtr(std::move(emote), currentEmotes); - } - - return {Success, std::move(emotes)}; - } - - CreateEmoteResult createChannelEmote(const QString &channelDisplayName, - const QJsonObject &jsonEmote) - { - auto id = EmoteId{jsonEmote.value("id").toString()}; - auto name = EmoteName{jsonEmote.value("code").toString()}; - auto author = EmoteAuthor{ - jsonEmote.value("user").toObject().value("displayName").toString()}; + auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; + auto name = EmoteName{jsonEmote.toObject().value("code").toString()}; auto emote = Emote({ name, @@ -99,58 +74,82 @@ namespace { Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5, EMOTE_BASE_SIZE * 2), Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25, - EMOTE_BASE_SIZE * 4), - }, - Tooltip{ - QString("%1
%2 BetterTTV Emote
By: %3") - .arg(name.string) - // when author is empty, it is a channel emote created by the broadcaster - .arg(author.string.isEmpty() ? "Channel" : "Shared") - .arg(author.string.isEmpty() ? channelDisplayName - : author.string)}, - Url{emoteLinkFormat.arg(id.string)}, - false, - id, + EMOTE_BASE_SIZE * 4)}, + Tooltip{name.string + "
Global BetterTTV Emote"}, + Url{EMOTE_LINK_FORMAT.arg(id.string)}, }); - return {id, name, emote}; + emotes[name] = cachedOrMakeEmotePtr(std::move(emote), currentEmotes); } - bool updateChannelEmote(Emote &emote, const QString &channelDisplayName, - const QJsonObject &jsonEmote) + return {Success, std::move(emotes)}; +} + +CreateEmoteResult createChannelEmote(const QString &channelDisplayName, + const QJsonObject &jsonEmote) +{ + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto name = EmoteName{jsonEmote.value("code").toString()}; + auto author = EmoteAuthor{ + jsonEmote.value("user").toObject().value("displayName").toString()}; + + auto emote = Emote({ + name, + ImageSet{ + Image::fromUrl(getEmoteLinkV3(id, "1x"), 1, EMOTE_BASE_SIZE), + Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5, EMOTE_BASE_SIZE * 2), + Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25, EMOTE_BASE_SIZE * 4), + }, + Tooltip{ + QString("%1
%2 BetterTTV Emote
By: %3") + .arg(name.string) + // when author is empty, it is a channel emote created by the broadcaster + .arg(author.string.isEmpty() ? "Channel" : "Shared") + .arg(author.string.isEmpty() ? channelDisplayName + : author.string)}, + Url{EMOTE_LINK_FORMAT.arg(id.string)}, + false, + id, + }); + + return {id, name, emote}; +} + +bool updateChannelEmote(Emote &emote, const QString &channelDisplayName, + const QJsonObject &jsonEmote) +{ + bool anyModifications = false; + + if (jsonEmote.contains("code")) { - bool anyModifications = false; - - if (jsonEmote.contains("code")) - { - emote.name = EmoteName{jsonEmote.value("code").toString()}; - anyModifications = true; - } - if (jsonEmote.contains("user")) - { - emote.author = EmoteAuthor{jsonEmote.value("user") - .toObject() - .value("displayName") - .toString()}; - anyModifications = true; - } - - if (anyModifications) - { - emote.tooltip = Tooltip{ - QString("%1
%2 BetterTTV Emote
By: %3") - .arg(emote.name.string) - // when author is empty, it is a channel emote created by the broadcaster - .arg(emote.author.string.isEmpty() ? "Channel" : "Shared") - .arg(emote.author.string.isEmpty() ? channelDisplayName - : emote.author.string)}; - } - - return anyModifications; + emote.name = EmoteName{jsonEmote.value("code").toString()}; + anyModifications = true; } + if (jsonEmote.contains("user")) + { + emote.author = EmoteAuthor{ + jsonEmote.value("user").toObject().value("displayName").toString()}; + anyModifications = true; + } + + if (anyModifications) + { + emote.tooltip = Tooltip{ + QString("%1
%2 BetterTTV Emote
By: %3") + .arg(emote.name.string) + // when author is empty, it is a channel emote created by the broadcaster + .arg(emote.author.string.isEmpty() ? "Channel" : "Shared") + .arg(emote.author.string.isEmpty() ? channelDisplayName + : emote.author.string)}; + } + + return anyModifications; +} } // namespace +namespace chatterino { + using namespace bttv::detail; EmoteMap bttv::detail::parseChannelEmotes(const QJsonObject &jsonRoot, @@ -182,6 +181,11 @@ EmoteMap bttv::detail::parseChannelEmotes(const QJsonObject &jsonRoot, BttvEmotes::BttvEmotes() : global_(std::make_shared()) { + getSettings()->enableBTTVGlobalEmotes.connect( + [this] { + this->loadEmotes(); + }, + this->managedConnections, false); } std::shared_ptr BttvEmotes::emotes() const @@ -360,15 +364,4 @@ std::optional BttvEmotes::removeEmote( return emote; } -/* -static Url getEmoteLink(QString urlTemplate, const EmoteId &id, - const QString &emoteScale) -{ - urlTemplate.detach(); - - return {urlTemplate.replace("{{id}}", id.string) - .replace("{{image}}", emoteScale)}; -} -*/ - } // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index 163f9728f..788b1e430 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -3,10 +3,15 @@ #include "common/Aliases.hpp" #include "common/Atomic.hpp" +#include #include +#include +#include #include #include +#include +#include namespace chatterino { @@ -81,6 +86,9 @@ public: private: Atomic> global_; + + std::vector> + managedConnections; }; } // namespace chatterino diff --git a/src/providers/chatterino/ChatterinoBadges.hpp b/src/providers/chatterino/ChatterinoBadges.hpp index d1afcfd5b..b4e26e4f5 100644 --- a/src/providers/chatterino/ChatterinoBadges.hpp +++ b/src/providers/chatterino/ChatterinoBadges.hpp @@ -1,7 +1,6 @@ #pragma once #include "common/Aliases.hpp" -#include "util/QStringHash.hpp" #include #include diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index 91da8b307..eefdd827e 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -299,7 +299,7 @@ std::vector> Emojis::parse( auto result = std::vector>(); QString::size_type lastParsedEmojiEndIndex = 0; - for (auto i = 0; i < text.length(); ++i) + for (qsizetype i = 0; i < text.length(); ++i) { const QChar character = text.at(i); @@ -401,7 +401,7 @@ QString Emojis::replaceShortCodes(const QString &text) const QString ret(text); auto it = this->findShortCodesRegex_.globalMatch(text); - int32_t offset = 0; + qsizetype offset = 0; while (it.hasNext()) { diff --git a/src/providers/ffz/FfzBadges.cpp b/src/providers/ffz/FfzBadges.cpp index 574d27787..fb4358ab9 100644 --- a/src/providers/ffz/FfzBadges.cpp +++ b/src/providers/ffz/FfzBadges.cpp @@ -12,9 +12,6 @@ #include #include -#include -#include - namespace chatterino { std::vector FfzBadges::getUserBadges(const UserId &id) diff --git a/src/providers/ffz/FfzBadges.hpp b/src/providers/ffz/FfzBadges.hpp index d24579451..761111831 100644 --- a/src/providers/ffz/FfzBadges.hpp +++ b/src/providers/ffz/FfzBadges.hpp @@ -1,7 +1,6 @@ #pragma once #include "common/Aliases.hpp" -#include "util/QStringHash.hpp" #include "util/ThreadGuard.hpp" #include diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 283f8f4f6..84887186c 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -206,6 +206,11 @@ FfzChannelBadgeMap ffz::detail::parseChannelBadges(const QJsonObject &badgeRoot) FfzEmotes::FfzEmotes() : global_(std::make_shared()) { + getSettings()->enableFFZGlobalEmotes.connect( + [this] { + this->loadEmotes(); + }, + this->managedConnections, false); } std::shared_ptr FfzEmotes::emotes() const diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index 7d639d56f..4c42ecbe4 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -5,10 +5,14 @@ #include "util/QStringHash.hpp" #include +#include #include +#include +#include #include #include +#include namespace chatterino { @@ -51,6 +55,9 @@ public: private: Atomic> global_; + + std::vector> + managedConnections; }; } // namespace chatterino diff --git a/src/providers/ffz/FfzUtil.cpp b/src/providers/ffz/FfzUtil.cpp index 762683a28..b621edbaf 100644 --- a/src/providers/ffz/FfzUtil.cpp +++ b/src/providers/ffz/FfzUtil.cpp @@ -1,5 +1,7 @@ #include "providers/ffz/FfzUtil.hpp" +#include + namespace chatterino { Url parseFfzUrl(const QString &ffzUrl) diff --git a/src/providers/ffz/FfzUtil.hpp b/src/providers/ffz/FfzUtil.hpp index 1d4ef65c3..4afd6c818 100644 --- a/src/providers/ffz/FfzUtil.hpp +++ b/src/providers/ffz/FfzUtil.hpp @@ -3,7 +3,6 @@ #include "common/Aliases.hpp" #include -#include namespace chatterino { diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 896a50e1b..969b0e5f5 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -188,6 +188,11 @@ EmoteMap seventv::detail::parseEmotes(const QJsonArray &emoteSetEmotes, SeventvEmotes::SeventvEmotes() : global_(std::make_shared()) { + getSettings()->enableSevenTVGlobalEmotes.connect( + [this] { + this->loadGlobalEmotes(); + }, + this->managedConnections, false); } std::shared_ptr SeventvEmotes::globalEmotes() const diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index 7daf7acc0..03b966f9c 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -4,11 +4,17 @@ #include "common/Atomic.hpp" #include "common/FlagsEnum.hpp" +#include #include #include +#include +#include +#include +#include #include #include +#include namespace chatterino { @@ -21,7 +27,7 @@ namespace seventv::eventapi { } // namespace seventv::eventapi // https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L29-L36 -enum class SeventvActiveEmoteFlag : int64_t { +enum class SeventvActiveEmoteFlag : std::int64_t { None = 0LL, // Emote is zero-width @@ -152,6 +158,9 @@ public: private: Atomic> global_; + + std::vector> + managedConnections; }; } // namespace chatterino diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index 35a99a486..010cac052 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -1,6 +1,8 @@ #include "providers/twitch/PubSubManager.hpp" +#include "Application.hpp" #include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" #include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubClient.hpp" @@ -508,6 +510,23 @@ PubSub::~PubSub() this->stop(); } +void PubSub::initialize() +{ + this->start(); + this->setAccount(getApp()->getAccounts()->twitch.getCurrent()); + + getApp()->getAccounts()->twitch.currentUserChanged.connect( + [this] { + this->unlistenChannelModerationActions(); + this->unlistenAutomod(); + this->unlistenLowTrustUsers(); + this->unlistenChannelPointRewards(); + + this->setAccount(getApp()->getAccounts()->twitch.getCurrent()); + }, + boost::signals2::at_front); +} + void PubSub::setAccount(std::shared_ptr account) { this->token_ = account->getOAuthToken(); diff --git a/src/providers/twitch/PubSubManager.hpp b/src/providers/twitch/PubSubManager.hpp index eb27e32f8..186cd5c6a 100644 --- a/src/providers/twitch/PubSubManager.hpp +++ b/src/providers/twitch/PubSubManager.hpp @@ -3,15 +3,21 @@ #include "providers/twitch/PubSubClientOptions.hpp" #include "providers/twitch/PubSubWebsocket.hpp" #include "util/ExponentialBackoff.hpp" -#include "util/QStringHash.hpp" +#include +#include #include #include #include #include +#include +#include +#include #include #include +#include +#include #include #include #include @@ -19,6 +25,10 @@ #include #include +#if __has_include() +# include +#endif + namespace chatterino { class TwitchAccount; @@ -85,10 +95,8 @@ public: PubSub &operator=(const PubSub &) = delete; PubSub &operator=(PubSub &&) = delete; - void setAccount(std::shared_ptr account); - - void start(); - void stop(); + /// Set up connections between itself & other parts of the application + void initialize(); struct { Signal chatCleared; @@ -192,6 +200,11 @@ public: } diag; private: + void setAccount(std::shared_ptr account); + + void start(); + void stop(); + /** * Unlistens to all topics matching the prefix in all clients */ @@ -250,6 +263,22 @@ private: const PubSubClientOptions clientOptions_; bool stopping_{false}; + +#ifdef FRIEND_TEST + friend class FTest; + + FRIEND_TEST(TwitchPubSubClient, ServerRespondsToPings); + FRIEND_TEST(TwitchPubSubClient, ServerDoesntRespondToPings); + FRIEND_TEST(TwitchPubSubClient, DisconnectedAfter1s); + FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimit); + FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep); + FRIEND_TEST(TwitchPubSubClient, ReceivedWhisper); + FRIEND_TEST(TwitchPubSubClient, ModeratorActionsUserBanned); + FRIEND_TEST(TwitchPubSubClient, MissingToken); + FRIEND_TEST(TwitchPubSubClient, WrongToken); + FRIEND_TEST(TwitchPubSubClient, CorrectToken); + FRIEND_TEST(TwitchPubSubClient, AutoModMessageHeld); +#endif }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 52eea65b3..ba020f529 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1320,8 +1320,6 @@ void TwitchChannel::refreshPubSub() auto currentAccount = getApp()->getAccounts()->twitch.getCurrent(); - getApp()->getTwitchPubSub()->setAccount(currentAccount); - getApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId); if (this->hasModRights()) { diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 91286767b..b0bfcf69d 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -16,9 +16,13 @@ #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/IrcMessageHandler.hpp" +#include "providers/twitch/PubSubActions.hpp" +#include "providers/twitch/PubSubManager.hpp" +#include "providers/twitch/pubsubmessages/AutoMod.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "util/PostToThread.hpp" #include "util/RatelimitBucket.hpp" @@ -230,6 +234,441 @@ void TwitchIrcServer::initialize() this->connect(); }); }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.chatCleared, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } + + QString text = + QString("%1 cleared the chat.").arg(action.source.login); + + postToThread([chan, text] { + chan->addSystemMessage(text); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.modeChanged, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } + + QString text = + QString("%1 turned %2 %3 mode.") + .arg(action.source.login) + .arg(action.state == ModeChangedAction::State::On ? "on" + : "off") + .arg(action.getModeName()); + + if (action.duration > 0) + { + text += QString(" (%1 seconds)").arg(action.duration); + } + + postToThread([chan, text] { + chan->addSystemMessage(text); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.moderationStateChanged, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } + + QString text; + + text = QString("%1 %2 %3.") + .arg(action.source.login, + (action.modded ? "modded" : "unmodded"), + action.target.login); + + postToThread([chan, text] { + chan->addSystemMessage(text); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.userBanned, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + MessageBuilder msg(action); + msg->flags.set(MessageFlag::PubSub); + chan->addOrReplaceTimeout(msg.release()); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.userWarned, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + // TODO: Resolve the moderator's user ID into a full user here, so message can look better + postToThread([chan, action] { + MessageBuilder msg(action); + msg->flags.set(MessageFlag::PubSub); + chan->addMessage(msg.release(), MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.messageDeleted, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty() || getSettings()->hideDeletionActions) + { + return; + } + + auto msg = MessageBuilder::makeDeletionMessageFromPubSub(action); + + postToThread([chan, msg] { + auto replaced = false; + LimitedQueueSnapshot snapshot = + chan->getMessageSnapshot(); + int snapshotLength = snapshot.size(); + + // without parens it doesn't build on windows + int end = (std::max)(0, snapshotLength - 200); + + for (int i = snapshotLength - 1; i >= end; --i) + { + const auto &s = snapshot[i]; + if (!s->flags.has(MessageFlag::PubSub) && + s->timeoutUser == msg->timeoutUser) + { + chan->replaceMessage(s, msg); + replaced = true; + break; + } + } + if (!replaced) + { + chan->addMessage(msg, MessageContext::Original); + } + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.userUnbanned, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + auto msg = MessageBuilder(action).release(); + + postToThread([chan, msg] { + chan->addMessage(msg, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.suspiciousMessageReceived, + [this](const auto &action) { + if (action.treatment == + PubSubLowTrustUsersMessage::Treatment::INVALID) + { + qCWarning(chatterinoTwitch) + << "Received suspicious message with unknown " + "treatment:" + << action.treatmentString; + return; + } + + // monitored chats are received over irc; in the future, we will use pubsub instead + if (action.treatment != + PubSubLowTrustUsersMessage::Treatment::Restricted) + { + return; + } + + if (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled()) + { + return; + } + + auto chan = this->getChannelOrEmptyByID(action.channelID); + + if (chan->isEmpty()) + { + return; + } + + auto twitchChannel = std::dynamic_pointer_cast(chan); + if (!twitchChannel) + { + return; + } + + postToThread([twitchChannel, action] { + const auto p = MessageBuilder::makeLowTrustUserMessage( + action, twitchChannel->getName(), twitchChannel.get()); + twitchChannel->addMessage(p.first, MessageContext::Original); + twitchChannel->addMessage(p.second, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.suspiciousTreatmentUpdated, + [this](const auto &action) { + if (action.treatment == + PubSubLowTrustUsersMessage::Treatment::INVALID) + { + qCWarning(chatterinoTwitch) + << "Received suspicious user update with unknown " + "treatment:" + << action.treatmentString; + return; + } + + if (action.updatedByUserLogin.isEmpty()) + { + return; + } + + if (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled()) + { + return; + } + + auto chan = this->getChannelOrEmptyByID(action.channelID); + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + auto msg = MessageBuilder::makeLowTrustUpdateMessage(action); + chan->addMessage(msg, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.autoModMessageCaught, + [this](const auto &msg, const QString &channelID) { + auto chan = this->getChannelOrEmptyByID(channelID); + if (chan->isEmpty()) + { + return; + } + + switch (msg.type) + { + case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: { + if (msg.status == "PENDING") + { + AutomodAction action(msg.data, channelID); + action.reason = QString("%1 level %2") + .arg(msg.contentCategory) + .arg(msg.contentLevel); + + action.msgID = msg.messageID; + action.message = msg.messageText; + + // this message also contains per-word automod data, which could be implemented + + // extract sender data manually because Twitch loves not being consistent + QString senderDisplayName = + msg.senderUserDisplayName; // Might be transformed later + bool hasLocalizedName = false; + if (!msg.senderUserDisplayName.isEmpty()) + { + // check for non-ascii display names + if (QString::compare(msg.senderUserDisplayName, + msg.senderUserLogin, + Qt::CaseInsensitive) != 0) + { + hasLocalizedName = true; + } + } + QColor senderColor = msg.senderUserChatColor; + QString senderColor_; + if (!senderColor.isValid() && + getSettings()->colorizeNicknames) + { + // color may be not present if user is a grey-name + senderColor = getRandomColor(msg.senderUserID); + } + + // handle username style based on prefered setting + switch (getSettings()->usernameDisplayMode.getValue()) + { + case UsernameDisplayMode::Username: { + if (hasLocalizedName) + { + senderDisplayName = msg.senderUserLogin; + } + break; + } + case UsernameDisplayMode::LocalizedName: { + break; + } + case UsernameDisplayMode:: + UsernameAndLocalizedName: { + if (hasLocalizedName) + { + senderDisplayName = QString("%1(%2)").arg( + msg.senderUserLogin, + msg.senderUserDisplayName); + } + break; + } + } + + action.target = + ActionUser{msg.senderUserID, msg.senderUserLogin, + senderDisplayName, senderColor}; + postToThread([chan, action] { + const auto p = MessageBuilder::makeAutomodMessage( + action, chan->getName()); + chan->addMessage(p.first, MessageContext::Original); + chan->addMessage(p.second, + MessageContext::Original); + + getApp() + ->getTwitch() + ->getAutomodChannel() + ->addMessage(p.first, MessageContext::Original); + getApp() + ->getTwitch() + ->getAutomodChannel() + ->addMessage(p.second, + MessageContext::Original); + + if (getSettings()->showAutomodInMentions) + { + getApp() + ->getTwitch() + ->getMentionsChannel() + ->addMessage(p.first, + MessageContext::Original); + getApp() + ->getTwitch() + ->getMentionsChannel() + ->addMessage(p.second, + MessageContext::Original); + } + }); + } + // "ALLOWED" and "DENIED" statuses remain unimplemented + // They are versions of automod_message_(denied|approved) but for mods. + } + break; + + case PubSubAutoModQueueMessage::Type::INVALID: + default: { + } + break; + } + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.autoModMessageBlocked, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + const auto p = + MessageBuilder::makeAutomodMessage(action, chan->getName()); + chan->addMessage(p.first, MessageContext::Original); + chan->addMessage(p.second, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.automodUserMessage, + [this](const auto &action) { + if (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled()) + { + return; + } + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + auto msg = MessageBuilder(action).release(); + + postToThread([chan, msg] { + chan->addMessage(msg, MessageContext::Original); + }); + chan->deleteMessage(msg->id); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.automodInfoMessage, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + const auto p = MessageBuilder::makeAutomodInfoMessage(action); + chan->addMessage(p, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->pointReward.redeemed, [this](auto &data) { + QString channelId = data.value("channel_id").toString(); + if (channelId.isEmpty()) + { + qCDebug(chatterinoApp) + << "Couldn't find channel id of point reward"; + return; + } + + auto chan = this->getChannelOrEmptyByID(channelId); + + auto reward = ChannelPointReward(data); + + postToThread([chan, reward] { + if (auto *channel = dynamic_cast(chan.get())) + { + channel->addChannelPointReward(reward); + } + }); + }); } void TwitchIrcServer::initializeConnection(IrcConnection *connection, diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index f91105808..fc3b888ef 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -202,8 +202,6 @@ private: std::queue lastMessageMod_; std::chrono::steady_clock::time_point lastErrorTimeSpeed_; std::chrono::steady_clock::time_point lastErrorTimeAmount_; - - pajlada::Signals::SignalHolder signalHolder_; }; } // namespace chatterino diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 8f1045e0c..3680e9d94 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -16,6 +16,7 @@ #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) # include #endif +#include #include @@ -301,8 +302,9 @@ Theme::Theme(const Paths &paths) this->loadAvailableThemes(paths); #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - QObject::connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, - &this->lifetime_, [this] { + QObject::connect(QApplication::styleHints(), + &QStyleHints::colorSchemeChanged, &this->lifetime_, + [this] { if (this->isSystemTheme()) { this->update(); @@ -320,7 +322,7 @@ void Theme::update() #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) if (this->isSystemTheme()) { - switch (qApp->styleHints()->colorScheme()) + switch (QApplication::styleHints()->colorScheme()) { case Qt::ColorScheme::Light: return this->lightSystemThemeName; diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index 9bb121989..0aad6c220 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -46,12 +46,18 @@ const QString CHATTERINO_OS = u"unknown"_s; namespace chatterino { -Updates::Updates(const Paths &paths_) +Updates::Updates(const Paths &paths_, Settings &settings) : paths(paths_) , currentVersion_(CHATTERINO_VERSION) , updateGuideLink_("https://chatterino.com") { qCDebug(chatterinoUpdate) << "init UpdateManager"; + + settings.betaUpdates.connect( + [this] { + this->checkForUpdates(); + }, + this->managedConnections, false); } /// Checks if the online version is newer or older than the current version. diff --git a/src/singletons/Updates.hpp b/src/singletons/Updates.hpp index 167bfab2b..93160b9f9 100644 --- a/src/singletons/Updates.hpp +++ b/src/singletons/Updates.hpp @@ -1,11 +1,16 @@ #pragma once +#include #include #include +#include +#include + namespace chatterino { class Paths; +class Settings; /** * To check for updates, use the `checkForUpdates` method. @@ -16,7 +21,7 @@ class Updates const Paths &paths; public: - explicit Updates(const Paths &paths_); + Updates(const Paths &paths_, Settings &settings); enum Status { None, @@ -59,6 +64,9 @@ private: QString updateGuideLink_; void setStatus_(Status status); + + std::vector> + managedConnections; }; } // namespace chatterino diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 9581224e9..bf0192e73 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -664,8 +664,6 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) { assertInGuiThread(); - auto *app = getApp(); - if (descriptor.type_ == "twitch") { return getApp()->getTwitch()->getOrAddChannel(descriptor.channelName_); @@ -753,7 +751,7 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout) // get geometry { // out of bounds windows - auto screens = qApp->screens(); + auto screens = QApplication::screens(); bool outOfBounds = !qEnvironmentVariableIsSet("I3SOCK") && std::none_of(screens.begin(), screens.end(), diff --git a/src/singletons/helper/GifTimer.cpp b/src/singletons/helper/GifTimer.cpp index 564ec961c..07e255381 100644 --- a/src/singletons/helper/GifTimer.cpp +++ b/src/singletons/helper/GifTimer.cpp @@ -4,6 +4,8 @@ #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" +#include + namespace chatterino { void GIFTimer::initialize() @@ -24,7 +26,7 @@ void GIFTimer::initialize() QObject::connect(&this->timer, &QTimer::timeout, [this] { if (getSettings()->animationsWhenFocused && - qApp->activeWindow() == nullptr) + QApplication::activeWindow() == nullptr) { return; } diff --git a/src/util/FormatTime.cpp b/src/util/FormatTime.cpp index 1bc0e07b6..13b64349c 100644 --- a/src/util/FormatTime.cpp +++ b/src/util/FormatTime.cpp @@ -48,7 +48,7 @@ QString formatTime(int totalSeconds) return res; } -QString formatTime(QString totalSecondsString) +QString formatTime(const QString &totalSecondsString) { bool ok = true; int totalSeconds(totalSecondsString.toInt(&ok)); diff --git a/src/util/FormatTime.hpp b/src/util/FormatTime.hpp index c9bb12cae..cb26cc9ca 100644 --- a/src/util/FormatTime.hpp +++ b/src/util/FormatTime.hpp @@ -8,7 +8,7 @@ namespace chatterino { // format: 1h 23m 42s QString formatTime(int totalSeconds); -QString formatTime(QString totalSecondsString); +QString formatTime(const QString &totalSecondsString); QString formatTime(std::chrono::seconds totalSeconds); } // namespace chatterino diff --git a/src/util/PostToThread.hpp b/src/util/PostToThread.hpp index fa4792941..0401091be 100644 --- a/src/util/PostToThread.hpp +++ b/src/util/PostToThread.hpp @@ -10,7 +10,7 @@ namespace chatterino { // https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style // Qt 5/4 - preferred, has least allocations template -static void postToThread(F &&fun, QObject *obj = qApp) +static void postToThread(F &&fun, QObject *obj = QCoreApplication::instance()) { struct Event : public QEvent { using Fun = typename std::decay::type; diff --git a/src/widgets/FramelessEmbedWindow.cpp b/src/widgets/FramelessEmbedWindow.cpp index 550cd9052..c054b1feb 100644 --- a/src/widgets/FramelessEmbedWindow.cpp +++ b/src/widgets/FramelessEmbedWindow.cpp @@ -5,6 +5,7 @@ #include "providers/twitch/TwitchIrcServer.hpp" #include "widgets/splits/Split.hpp" +#include #include #include #include @@ -75,7 +76,7 @@ void FramelessEmbedWindow::showEvent(QShowEvent *) auto handle = reinterpret_cast(this->winId()); if (!::SetParent(handle, parentHwnd)) { - qApp->exit(1); + QApplication::exit(1); } QJsonDocument doc; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 5c07be6b3..441d5b3c6 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -48,6 +48,7 @@ #include "widgets/Window.hpp" #include +#include #include #include #include diff --git a/tests/src/ChatterSet.cpp b/tests/src/ChatterSet.cpp index 57a67a771..7ef82fb43 100644 --- a/tests/src/ChatterSet.cpp +++ b/tests/src/ChatterSet.cpp @@ -4,9 +4,11 @@ #include +using namespace chatterino; + TEST(ChatterSet, insert) { - chatterino::ChatterSet set; + ChatterSet set; EXPECT_FALSE(set.contains("pajlada")); EXPECT_FALSE(set.contains("Pajlada")); @@ -26,7 +28,7 @@ TEST(ChatterSet, insert) TEST(ChatterSet, MaxSize) { - chatterino::ChatterSet set; + ChatterSet set; EXPECT_FALSE(set.contains("pajlada")); EXPECT_FALSE(set.contains("Pajlada")); @@ -36,7 +38,7 @@ TEST(ChatterSet, MaxSize) EXPECT_TRUE(set.contains("Pajlada")); // After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set - for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) + for (auto i = 0; i < ChatterSet::CHATTER_LIMIT - 1; ++i) { set.addRecentChatter(QString("%1").arg(i)); } @@ -53,7 +55,7 @@ TEST(ChatterSet, MaxSize) TEST(ChatterSet, MaxSizeLastUsed) { - chatterino::ChatterSet set; + ChatterSet set; EXPECT_FALSE(set.contains("pajlada")); EXPECT_FALSE(set.contains("Pajlada")); @@ -63,7 +65,7 @@ TEST(ChatterSet, MaxSizeLastUsed) EXPECT_TRUE(set.contains("Pajlada")); // After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set - for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) + for (auto i = 0; i < ChatterSet::CHATTER_LIMIT - 1; ++i) { set.addRecentChatter(QString("%1").arg(i)); } @@ -75,7 +77,7 @@ TEST(ChatterSet, MaxSizeLastUsed) set.addRecentChatter("pajlada"); // After another CHATTER_LIMIT-1 additional chatters, pajlada should still be there - for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) + for (auto i = 0; i < ChatterSet::CHATTER_LIMIT - 1; ++i) { set.addRecentChatter(QString("new-%1").arg(i)); } diff --git a/tests/src/TwitchPubSubClient.cpp b/tests/src/TwitchPubSubClient.cpp index dff88450f..e195d937b 100644 --- a/tests/src/TwitchPubSubClient.cpp +++ b/tests/src/TwitchPubSubClient.cpp @@ -10,7 +10,6 @@ #include #include -#include using namespace chatterino; using namespace std::chrono_literals; @@ -33,6 +32,8 @@ using namespace std::chrono_literals; #ifdef RUN_PUBSUB_TESTS +namespace chatterino { + template class ReceivedMessage { @@ -451,4 +452,6 @@ TEST(TwitchPubSubClient, AutoModMessageHeld) ASSERT_EQ(pubSub.diag.connectionsFailed, 0); } +} // namespace chatterino + #endif