refactor: some Application & style things (#5561)

This commit is contained in:
pajlada
2024-08-25 15:33:07 +02:00
committed by GitHub
parent ac88730563
commit 627c735524
41 changed files with 733 additions and 678 deletions
+117 -124
View File
@@ -15,82 +15,57 @@
#include <QJsonArray>
#include <QThread>
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<EmoteId, std::weak_ptr<const Emote>> cache;
static std::mutex mutex;
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
}
std::pair<Outcome, EmoteMap> parseGlobalEmotes(const QJsonArray &jsonEmotes,
const EmoteMap &currentEmotes)
{
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<EmoteId, std::weak_ptr<const Emote>> cache;
static std::mutex mutex;
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
}
std::pair<Outcome, EmoteMap> parseGlobalEmotes(
const QJsonArray &jsonEmotes, const EmoteMap &currentEmotes)
{
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 + "<br>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<br>%2 BetterTTV Emote<br>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 + "<br>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<br>%2 BetterTTV Emote<br>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<br>%2 BetterTTV Emote<br>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<br>%2 BetterTTV Emote<br>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<EmoteMap>())
{
getSettings()->enableBTTVGlobalEmotes.connect(
[this] {
this->loadEmotes();
},
this->managedConnections, false);
}
std::shared_ptr<const EmoteMap> BttvEmotes::emotes() const
@@ -360,15 +364,4 @@ std::optional<EmotePtr> 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
+8
View File
@@ -3,10 +3,15 @@
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
#include <pajlada/signals/scoped-connection.hpp>
#include <QJsonObject>
#include <QString>
#include <functional>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
namespace chatterino {
@@ -81,6 +86,9 @@ public:
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
managedConnections;
};
} // namespace chatterino
@@ -1,7 +1,6 @@
#pragma once
#include "common/Aliases.hpp"
#include "util/QStringHash.hpp"
#include <memory>
#include <optional>
+2 -2
View File
@@ -299,7 +299,7 @@ std::vector<boost::variant<EmotePtr, QString>> Emojis::parse(
auto result = std::vector<boost::variant<EmotePtr, QString>>();
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())
{
-3
View File
@@ -12,9 +12,6 @@
#include <QThread>
#include <QUrl>
#include <map>
#include <shared_mutex>
namespace chatterino {
std::vector<FfzBadges::Badge> FfzBadges::getUserBadges(const UserId &id)
-1
View File
@@ -1,7 +1,6 @@
#pragma once
#include "common/Aliases.hpp"
#include "util/QStringHash.hpp"
#include "util/ThreadGuard.hpp"
#include <QColor>
+5
View File
@@ -206,6 +206,11 @@ FfzChannelBadgeMap ffz::detail::parseChannelBadges(const QJsonObject &badgeRoot)
FfzEmotes::FfzEmotes()
: global_(std::make_shared<EmoteMap>())
{
getSettings()->enableFFZGlobalEmotes.connect(
[this] {
this->loadEmotes();
},
this->managedConnections, false);
}
std::shared_ptr<const EmoteMap> FfzEmotes::emotes() const
+7
View File
@@ -5,10 +5,14 @@
#include "util/QStringHash.hpp"
#include <boost/unordered/unordered_flat_map.hpp>
#include <pajlada/signals/scoped-connection.hpp>
#include <QJsonObject>
#include <QString>
#include <functional>
#include <memory>
#include <optional>
#include <vector>
namespace chatterino {
@@ -51,6 +55,9 @@ public:
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
managedConnections;
};
} // namespace chatterino
+2
View File
@@ -1,5 +1,7 @@
#include "providers/ffz/FfzUtil.hpp"
#include <QUrl>
namespace chatterino {
Url parseFfzUrl(const QString &ffzUrl)
-1
View File
@@ -3,7 +3,6 @@
#include "common/Aliases.hpp"
#include <QString>
#include <QUrl>
namespace chatterino {
+5
View File
@@ -188,6 +188,11 @@ EmoteMap seventv::detail::parseEmotes(const QJsonArray &emoteSetEmotes,
SeventvEmotes::SeventvEmotes()
: global_(std::make_shared<EmoteMap>())
{
getSettings()->enableSevenTVGlobalEmotes.connect(
[this] {
this->loadGlobalEmotes();
},
this->managedConnections, false);
}
std::shared_ptr<const EmoteMap> SeventvEmotes::globalEmotes() const
+10 -1
View File
@@ -4,11 +4,17 @@
#include "common/Atomic.hpp"
#include "common/FlagsEnum.hpp"
#include <pajlada/signals/scoped-connection.hpp>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <vector>
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<std::shared_ptr<const EmoteMap>> global_;
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
managedConnections;
};
} // namespace chatterino
+19
View File
@@ -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<TwitchAccount> account)
{
this->token_ = account->getOAuthToken();
+34 -5
View File
@@ -3,15 +3,21 @@
#include "providers/twitch/PubSubClientOptions.hpp"
#include "providers/twitch/PubSubWebsocket.hpp"
#include "util/ExponentialBackoff.hpp"
#include "util/QStringHash.hpp"
#include <boost/asio/io_service.hpp>
#include <boost/asio/ssl/context.hpp>
#include <pajlada/signals/signal.hpp>
#include <QJsonObject>
#include <QString>
#include <websocketpp/client.hpp>
#include <websocketpp/common/connection_hdl.hpp>
#include <websocketpp/common/memory.hpp>
#include <websocketpp/config/asio_client.hpp>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
@@ -19,6 +25,10 @@
#include <unordered_map>
#include <vector>
#if __has_include(<gtest/gtest_prod.h>)
# include <gtest/gtest_prod.h>
#endif
namespace chatterino {
class TwitchAccount;
@@ -85,10 +95,8 @@ public:
PubSub &operator=(const PubSub &) = delete;
PubSub &operator=(PubSub &&) = delete;
void setAccount(std::shared_ptr<TwitchAccount> account);
void start();
void stop();
/// Set up connections between itself & other parts of the application
void initialize();
struct {
Signal<ClearChatAction> chatCleared;
@@ -192,6 +200,11 @@ public:
} diag;
private:
void setAccount(std::shared_ptr<TwitchAccount> 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
-2
View File
@@ -1320,8 +1320,6 @@ void TwitchChannel::refreshPubSub()
auto currentAccount = getApp()->getAccounts()->twitch.getCurrent();
getApp()->getTwitchPubSub()->setAccount(currentAccount);
getApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId);
if (this->hasModRights())
{
+439
View File
@@ -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<MessagePtr> 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<TwitchChannel>(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<TwitchChannel *>(chan.get()))
{
channel->addChannelPointReward(reward);
}
});
});
}
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
-2
View File
@@ -202,8 +202,6 @@ private:
std::queue<std::chrono::steady_clock::time_point> lastMessageMod_;
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
pajlada::Signals::SignalHolder signalHolder_;
};
} // namespace chatterino