refactor: some Application & style things (#5561)
This commit is contained in:
+117
-124
@@ -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 ¤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<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 ¤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 + "<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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/ThreadGuard.hpp"
|
||||
|
||||
#include <QColor>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "providers/ffz/FfzUtil.hpp"
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
Url parseFfzUrl(const QString &ffzUrl)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "common/Aliases.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1320,8 +1320,6 @@ void TwitchChannel::refreshPubSub()
|
||||
|
||||
auto currentAccount = getApp()->getAccounts()->twitch.getCurrent();
|
||||
|
||||
getApp()->getTwitchPubSub()->setAccount(currentAccount);
|
||||
|
||||
getApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId);
|
||||
if (this->hasModRights())
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user