mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
refactor: load Twitch emotes from Helix (#5239)
This commit is contained in:
parent
03b0e4881f
commit
820aa12af6
40 changed files with 1251 additions and 528 deletions
|
@ -42,7 +42,7 @@ CheckOptions:
|
||||||
- key: readability-identifier-naming.FunctionCase
|
- key: readability-identifier-naming.FunctionCase
|
||||||
value: camelBack
|
value: camelBack
|
||||||
- key: readability-identifier-naming.FunctionIgnoredRegexp
|
- key: readability-identifier-naming.FunctionIgnoredRegexp
|
||||||
value: ^TEST$
|
value: ^(TEST|MOCK_METHOD)$
|
||||||
|
|
||||||
- key: readability-identifier-naming.MemberCase
|
- key: readability-identifier-naming.MemberCase
|
||||||
value: camelBack
|
value: camelBack
|
||||||
|
|
2
.github/workflows/clang-tidy.yml
vendored
2
.github/workflows/clang-tidy.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
||||||
build_dir: build-clang-tidy
|
build_dir: build-clang-tidy
|
||||||
config_file: ".clang-tidy"
|
config_file: ".clang-tidy"
|
||||||
split_workflow: true
|
split_workflow: true
|
||||||
exclude: "lib/*,tools/crash-handler/*"
|
exclude: "lib/*,tools/crash-handler/*,mocks/*"
|
||||||
cmake_command: >-
|
cmake_command: >-
|
||||||
./.CI/setup-clang-tidy.sh
|
./.CI/setup-clang-tidy.sh
|
||||||
apt_packages: >-
|
apt_packages: >-
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504)
|
- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504)
|
||||||
- Bugfix: Links with invalid characters in the domain are no longer detected. (#5509)
|
- Bugfix: Links with invalid characters in the domain are no longer detected. (#5509)
|
||||||
- Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525)
|
- Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525)
|
||||||
|
- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239)
|
||||||
- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530)
|
- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530)
|
||||||
- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558)
|
- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558)
|
||||||
- Bugfix: Fixed some tooltips not being readable. (#5578)
|
- Bugfix: Fixed some tooltips not being readable. (#5578)
|
||||||
|
|
|
@ -257,6 +257,13 @@ public:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ITwitchUsers *getTwitchUsers() override
|
||||||
|
{
|
||||||
|
assert(false && "EmptyApplication::getTwitchUsers was called without "
|
||||||
|
"being initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
QTemporaryDir settingsDir;
|
QTemporaryDir settingsDir;
|
||||||
Paths paths_;
|
Paths paths_;
|
||||||
Args args_;
|
Args args_;
|
||||||
|
|
|
@ -410,6 +410,23 @@ public:
|
||||||
(FailureCallback<HelixSendMessageError, QString> failureCallback)),
|
(FailureCallback<HelixSendMessageError, QString> failureCallback)),
|
||||||
(override));
|
(override));
|
||||||
|
|
||||||
|
// get user emotes
|
||||||
|
MOCK_METHOD(
|
||||||
|
void, getUserEmotes,
|
||||||
|
(QString userID, QString broadcasterID,
|
||||||
|
(ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||||
|
successCallback),
|
||||||
|
FailureCallback<QString> failureCallback, CancellationToken &&token),
|
||||||
|
(override));
|
||||||
|
|
||||||
|
// get followed channel
|
||||||
|
MOCK_METHOD(
|
||||||
|
void, getFollowedChannel,
|
||||||
|
(QString userID, QString broadcasterID,
|
||||||
|
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback),
|
||||||
|
(override));
|
||||||
|
|
||||||
MOCK_METHOD(void, update, (QString clientId, QString oauthToken),
|
MOCK_METHOD(void, update, (QString clientId, QString oauthToken),
|
||||||
(override));
|
(override));
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
#include "providers/twitch/PubSubMessages.hpp"
|
#include "providers/twitch/PubSubMessages.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
#include "providers/twitch/TwitchUsers.hpp"
|
||||||
#include "singletons/CrashHandler.hpp"
|
#include "singletons/CrashHandler.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
#include "singletons/Fonts.hpp"
|
#include "singletons/Fonts.hpp"
|
||||||
|
@ -176,6 +177,7 @@ Application::Application(Settings &_settings, const Paths &paths,
|
||||||
, logging(new Logging(_settings))
|
, logging(new Logging(_settings))
|
||||||
, linkResolver(new LinkResolver)
|
, linkResolver(new LinkResolver)
|
||||||
, streamerMode(new StreamerMode)
|
, streamerMode(new StreamerMode)
|
||||||
|
, twitchUsers(new TwitchUsers)
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
, plugins(new PluginController(paths))
|
, plugins(new PluginController(paths))
|
||||||
#endif
|
#endif
|
||||||
|
@ -515,6 +517,14 @@ IStreamerMode *Application::getStreamerMode()
|
||||||
return this->streamerMode.get();
|
return this->streamerMode.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ITwitchUsers *Application::getTwitchUsers()
|
||||||
|
{
|
||||||
|
assertInGuiThread();
|
||||||
|
assert(this->twitchUsers);
|
||||||
|
|
||||||
|
return this->twitchUsers.get();
|
||||||
|
}
|
||||||
|
|
||||||
BttvEmotes *Application::getBttvEmotes()
|
BttvEmotes *Application::getBttvEmotes()
|
||||||
{
|
{
|
||||||
assertInGuiThread();
|
assertInGuiThread();
|
||||||
|
|
|
@ -53,6 +53,7 @@ class SeventvEmotes;
|
||||||
class SeventvEventAPI;
|
class SeventvEventAPI;
|
||||||
class ILinkResolver;
|
class ILinkResolver;
|
||||||
class IStreamerMode;
|
class IStreamerMode;
|
||||||
|
class ITwitchUsers;
|
||||||
|
|
||||||
class IApplication
|
class IApplication
|
||||||
{
|
{
|
||||||
|
@ -103,6 +104,7 @@ public:
|
||||||
virtual SeventvEventAPI *getSeventvEventAPI() = 0;
|
virtual SeventvEventAPI *getSeventvEventAPI() = 0;
|
||||||
virtual ILinkResolver *getLinkResolver() = 0;
|
virtual ILinkResolver *getLinkResolver() = 0;
|
||||||
virtual IStreamerMode *getStreamerMode() = 0;
|
virtual IStreamerMode *getStreamerMode() = 0;
|
||||||
|
virtual ITwitchUsers *getTwitchUsers() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Application : public IApplication
|
class Application : public IApplication
|
||||||
|
@ -166,6 +168,7 @@ private:
|
||||||
const std::unique_ptr<Logging> logging;
|
const std::unique_ptr<Logging> logging;
|
||||||
std::unique_ptr<ILinkResolver> linkResolver;
|
std::unique_ptr<ILinkResolver> linkResolver;
|
||||||
std::unique_ptr<IStreamerMode> streamerMode;
|
std::unique_ptr<IStreamerMode> streamerMode;
|
||||||
|
std::unique_ptr<ITwitchUsers> twitchUsers;
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
std::unique_ptr<PluginController> plugins;
|
std::unique_ptr<PluginController> plugins;
|
||||||
#endif
|
#endif
|
||||||
|
@ -215,6 +218,7 @@ public:
|
||||||
|
|
||||||
ILinkResolver *getLinkResolver() override;
|
ILinkResolver *getLinkResolver() override;
|
||||||
IStreamerMode *getStreamerMode() override;
|
IStreamerMode *getStreamerMode() override;
|
||||||
|
ITwitchUsers *getTwitchUsers() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initBttvLiveUpdates();
|
void initBttvLiveUpdates();
|
||||||
|
|
|
@ -404,6 +404,8 @@ set(SOURCE_FILES
|
||||||
providers/twitch/TwitchIrcServer.hpp
|
providers/twitch/TwitchIrcServer.hpp
|
||||||
providers/twitch/TwitchUser.cpp
|
providers/twitch/TwitchUser.cpp
|
||||||
providers/twitch/TwitchUser.hpp
|
providers/twitch/TwitchUser.hpp
|
||||||
|
providers/twitch/TwitchUsers.cpp
|
||||||
|
providers/twitch/TwitchUsers.hpp
|
||||||
|
|
||||||
providers/twitch/pubsubmessages/AutoMod.cpp
|
providers/twitch/pubsubmessages/AutoMod.cpp
|
||||||
providers/twitch/pubsubmessages/AutoMod.hpp
|
providers/twitch/pubsubmessages/AutoMod.hpp
|
||||||
|
@ -471,6 +473,7 @@ set(SOURCE_FILES
|
||||||
util/DebugCount.hpp
|
util/DebugCount.hpp
|
||||||
util/DisplayBadge.cpp
|
util/DisplayBadge.cpp
|
||||||
util/DisplayBadge.hpp
|
util/DisplayBadge.hpp
|
||||||
|
util/Expected.hpp
|
||||||
util/FormatTime.cpp
|
util/FormatTime.cpp
|
||||||
util/FormatTime.hpp
|
util/FormatTime.hpp
|
||||||
util/FunctionEventFilter.cpp
|
util/FunctionEventFilter.cpp
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
# include <IrcCommand>
|
# include <IrcCommand>
|
||||||
# include <IrcConnection>
|
# include <IrcConnection>
|
||||||
# include <IrcMessage>
|
# include <IrcMessage>
|
||||||
|
# include <nonstd/expected.hpp>
|
||||||
# include <pajlada/serialize.hpp>
|
# include <pajlada/serialize.hpp>
|
||||||
# include <pajlada/settings/setting.hpp>
|
# include <pajlada/settings/setting.hpp>
|
||||||
# include <pajlada/settings/settinglistener.hpp>
|
# include <pajlada/settings/settinglistener.hpp>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/container_hash/hash_fwd.hpp>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||||
#define QStringAlias(name) \
|
#define QStringAlias(name) \
|
||||||
namespace chatterino { \
|
namespace chatterino { \
|
||||||
struct name { \
|
struct name { \
|
||||||
|
@ -27,12 +29,22 @@
|
||||||
return qHash(s.string); \
|
return qHash(s.string); \
|
||||||
} \
|
} \
|
||||||
}; \
|
}; \
|
||||||
} /* namespace std */
|
} /* namespace std */ \
|
||||||
|
namespace boost { \
|
||||||
|
template <> \
|
||||||
|
struct hash<chatterino::name> { \
|
||||||
|
std::size_t operator()(chatterino::name const &s) const \
|
||||||
|
{ \
|
||||||
|
return qHash(s.string); \
|
||||||
|
} \
|
||||||
|
}; \
|
||||||
|
} /* namespace boost */
|
||||||
|
|
||||||
QStringAlias(UserName);
|
QStringAlias(UserName);
|
||||||
QStringAlias(UserId);
|
QStringAlias(UserId);
|
||||||
QStringAlias(Url);
|
QStringAlias(Url);
|
||||||
QStringAlias(Tooltip);
|
QStringAlias(Tooltip);
|
||||||
QStringAlias(EmoteId);
|
QStringAlias(EmoteId);
|
||||||
|
QStringAlias(EmoteSetId);
|
||||||
QStringAlias(EmoteName);
|
QStringAlias(EmoteName);
|
||||||
QStringAlias(EmoteAuthor);
|
QStringAlias(EmoteAuthor);
|
||||||
|
|
|
@ -113,8 +113,8 @@ bool appendWhisperMessageWordsLocally(const QStringList &words)
|
||||||
for (int i = 2; i < words.length(); i++)
|
for (int i = 2; i < words.length(); i++)
|
||||||
{
|
{
|
||||||
{ // Twitch emote
|
{ // Twitch emote
|
||||||
auto it = accemotes.emotes.find({words[i]});
|
auto it = accemotes->find({words[i]});
|
||||||
if (it != accemotes.emotes.end())
|
if (it != accemotes->end())
|
||||||
{
|
{
|
||||||
b.emplace<EmoteElement>(it->second,
|
b.emplace<EmoteElement>(it->second,
|
||||||
MessageElementFlag::TwitchEmote);
|
MessageElementFlag::TwitchEmote);
|
||||||
|
|
|
@ -95,26 +95,16 @@ void EmoteSource::initializeFromChannel(const Channel *channel)
|
||||||
// returns true also for special Twitch channels (/live, /mentions, /whispers, etc.)
|
// returns true also for special Twitch channels (/live, /mentions, /whispers, etc.)
|
||||||
if (channel->isTwitchChannel())
|
if (channel->isTwitchChannel())
|
||||||
{
|
{
|
||||||
if (auto user = app->getAccounts()->twitch.getCurrent())
|
|
||||||
{
|
|
||||||
// Twitch Emotes available globally
|
|
||||||
auto emoteData = user->accessEmotes();
|
|
||||||
addEmotes(emotes, emoteData->emotes, "Twitch Emote");
|
|
||||||
|
|
||||||
// Twitch Emotes available locally
|
|
||||||
auto localEmoteData = user->accessLocalEmotes();
|
|
||||||
if ((tc != nullptr) &&
|
|
||||||
localEmoteData->find(tc->roomId()) != localEmoteData->end())
|
|
||||||
{
|
|
||||||
if (const auto *localEmotes = &localEmoteData->at(tc->roomId()))
|
|
||||||
{
|
|
||||||
addEmotes(emotes, *localEmotes, "Local Twitch Emotes");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tc)
|
if (tc)
|
||||||
{
|
{
|
||||||
|
if (auto twitch = tc->localTwitchEmotes())
|
||||||
|
{
|
||||||
|
addEmotes(emotes, *twitch, "Local Twitch Emotes");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto user = getApp()->getAccounts()->twitch.getCurrent();
|
||||||
|
addEmotes(emotes, **user->accessEmotes(), "Twitch Emote");
|
||||||
|
|
||||||
// TODO extract "Channel {BetterTTV,7TV,FrankerFaceZ}" text into a #define.
|
// TODO extract "Channel {BetterTTV,7TV,FrankerFaceZ}" text into a #define.
|
||||||
if (auto bttv = tc->bttvEmotes())
|
if (auto bttv = tc->bttvEmotes())
|
||||||
{
|
{
|
||||||
|
|
|
@ -99,7 +99,7 @@ bool IgnorePhrase::containsEmote() const
|
||||||
for (const auto &acc : accvec)
|
for (const auto &acc : accvec)
|
||||||
{
|
{
|
||||||
const auto &accemotes = *acc->accessEmotes();
|
const auto &accemotes = *acc->accessEmotes();
|
||||||
for (const auto &emote : accemotes.emotes)
|
for (const auto &emote : *accemotes)
|
||||||
{
|
{
|
||||||
if (this->replace_.contains(emote.first.string,
|
if (this->replace_.contains(emote.first.string,
|
||||||
Qt::CaseSensitive))
|
Qt::CaseSensitive))
|
||||||
|
|
|
@ -31,28 +31,6 @@ void IvrApi::getSubage(QString userName, QString channelName,
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
void IvrApi::getBulkEmoteSets(QString emoteSetList,
|
|
||||||
ResultCallback<QJsonArray> successCallback,
|
|
||||||
IvrFailureCallback failureCallback)
|
|
||||||
{
|
|
||||||
QUrlQuery urlQuery;
|
|
||||||
urlQuery.addQueryItem("set_id", emoteSetList);
|
|
||||||
|
|
||||||
this->makeRequest("twitch/emotes/sets", urlQuery)
|
|
||||||
.onSuccess([successCallback, failureCallback](auto result) {
|
|
||||||
auto root = result.parseJsonArray();
|
|
||||||
|
|
||||||
successCallback(root);
|
|
||||||
})
|
|
||||||
.onError([failureCallback](auto result) {
|
|
||||||
qCWarning(chatterinoIvr)
|
|
||||||
<< "Failed IVR API Call!" << result.formatError()
|
|
||||||
<< QString(result.getData());
|
|
||||||
failureCallback();
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkRequest IvrApi::makeRequest(QString url, QUrlQuery urlQuery)
|
NetworkRequest IvrApi::makeRequest(QString url, QUrlQuery urlQuery)
|
||||||
{
|
{
|
||||||
assert(!url.startsWith("/"));
|
assert(!url.startsWith("/"));
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "common/network/NetworkRequest.hpp"
|
#include "common/network/NetworkRequest.hpp"
|
||||||
#include "providers/twitch/TwitchEmotes.hpp"
|
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
@ -32,45 +31,6 @@ struct IvrSubage {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct IvrEmoteSet {
|
|
||||||
const QString setId;
|
|
||||||
const QString displayName;
|
|
||||||
const QString login;
|
|
||||||
const QString channelId;
|
|
||||||
const QString tier;
|
|
||||||
const QJsonArray emotes;
|
|
||||||
|
|
||||||
IvrEmoteSet(const QJsonObject &root)
|
|
||||||
: setId(root.value("setID").toString())
|
|
||||||
, displayName(root.value("channelName").toString())
|
|
||||||
, login(root.value("channelLogin").toString())
|
|
||||||
, channelId(root.value("channelID").toString())
|
|
||||||
, tier(root.value("tier").toString())
|
|
||||||
, emotes(root.value("emoteList").toArray())
|
|
||||||
|
|
||||||
{
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct IvrEmote {
|
|
||||||
const QString code;
|
|
||||||
const QString id;
|
|
||||||
const QString setId;
|
|
||||||
const QString url;
|
|
||||||
const QString emoteType;
|
|
||||||
const QString imageType;
|
|
||||||
|
|
||||||
explicit IvrEmote(const QJsonObject &root)
|
|
||||||
: code(root.value("code").toString())
|
|
||||||
, id(root.value("id").toString())
|
|
||||||
, setId(root.value("setID").toString())
|
|
||||||
, url(TWITCH_EMOTE_TEMPLATE.arg(this->id, u"3.0"))
|
|
||||||
, emoteType(root.value("type").toString())
|
|
||||||
, imageType(root.value("assetType").toString())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class IvrApi final
|
class IvrApi final
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -79,11 +39,6 @@ public:
|
||||||
ResultCallback<IvrSubage> resultCallback,
|
ResultCallback<IvrSubage> resultCallback,
|
||||||
IvrFailureCallback failureCallback);
|
IvrFailureCallback failureCallback);
|
||||||
|
|
||||||
// https://api.ivr.fi/v2/docs/static/index.html#/Twitch/get_twitch_emotes_sets
|
|
||||||
void getBulkEmoteSets(QString emoteSetList,
|
|
||||||
ResultCallback<QJsonArray> successCallback,
|
|
||||||
IvrFailureCallback failureCallback);
|
|
||||||
|
|
||||||
static void initialize();
|
static void initialize();
|
||||||
|
|
||||||
IvrApi() = default;
|
IvrApi() = default;
|
||||||
|
|
|
@ -870,17 +870,6 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
|
||||||
|
|
||||||
void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
|
void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
|
||||||
{
|
{
|
||||||
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
|
||||||
|
|
||||||
// set received emote-sets, used in TwitchAccount::loadUserstateEmotes
|
|
||||||
bool emoteSetsChanged = currentUser->setUserstateEmoteSets(
|
|
||||||
message->tag("emote-sets").toString().split(","));
|
|
||||||
|
|
||||||
if (emoteSetsChanged)
|
|
||||||
{
|
|
||||||
currentUser->loadUserstateEmotes();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString channelName;
|
QString channelName;
|
||||||
if (!trimChannelName(message->parameter(0), channelName))
|
if (!trimChannelName(message->parameter(0), channelName))
|
||||||
{
|
{
|
||||||
|
@ -918,24 +907,6 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will emit only once and right after user logs in to IRC - reset emote data and reload emotes
|
|
||||||
void IrcMessageHandler::handleGlobalUserStateMessage(
|
|
||||||
Communi::IrcMessage *message)
|
|
||||||
{
|
|
||||||
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
|
||||||
|
|
||||||
// set received emote-sets, this time used to initially load emotes
|
|
||||||
// NOTE: this should always return true unless we reconnect
|
|
||||||
auto emoteSetsChanged = currentUser->setUserstateEmoteSets(
|
|
||||||
message->tag("emote-sets").toString().split(","));
|
|
||||||
|
|
||||||
// We should always attempt to reload emotes even on reconnections where
|
|
||||||
// emoteSetsChanged, since we want to trigger emote reloads when
|
|
||||||
// "currentUserChanged" signal is emitted
|
|
||||||
qCDebug(chatterinoTwitch) << emoteSetsChanged << message->toData();
|
|
||||||
currentUser->loadEmotes();
|
|
||||||
}
|
|
||||||
|
|
||||||
void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage)
|
void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage)
|
||||||
{
|
{
|
||||||
MessageParseArgs args;
|
MessageParseArgs args;
|
||||||
|
|
|
@ -44,7 +44,6 @@ public:
|
||||||
void handleClearChatMessage(Communi::IrcMessage *message);
|
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||||
void handleClearMessageMessage(Communi::IrcMessage *message);
|
void handleClearMessageMessage(Communi::IrcMessage *message);
|
||||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||||
void handleGlobalUserStateMessage(Communi::IrcMessage *message);
|
|
||||||
void handleWhisperMessage(Communi::IrcMessage *ircMessage);
|
void handleWhisperMessage(Communi::IrcMessage *ircMessage);
|
||||||
|
|
||||||
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
||||||
|
|
|
@ -3,26 +3,33 @@
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "common/Channel.hpp"
|
#include "common/Channel.hpp"
|
||||||
#include "common/Env.hpp"
|
#include "common/Env.hpp"
|
||||||
|
#include "common/Literals.hpp"
|
||||||
#include "common/network/NetworkResult.hpp"
|
#include "common/network/NetworkResult.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
#include "debug/AssertInGuiThread.hpp"
|
#include "debug/AssertInGuiThread.hpp"
|
||||||
|
#include "messages/Emote.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
#include "providers/IvrApi.hpp"
|
#include "providers/IvrApi.hpp"
|
||||||
#include "providers/seventv/SeventvAPI.hpp"
|
#include "providers/seventv/SeventvAPI.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "providers/twitch/TwitchCommon.hpp"
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
|
#include "providers/twitch/TwitchUsers.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
#include "util/CancellationToken.hpp"
|
#include "util/CancellationToken.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
#include "util/QStringHash.hpp"
|
#include "util/QStringHash.hpp"
|
||||||
#include "util/RapidjsonHelpers.hpp"
|
#include "util/RapidjsonHelpers.hpp"
|
||||||
|
|
||||||
|
#include <boost/unordered/unordered_flat_map.hpp>
|
||||||
|
#include <QStringBuilder>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
using namespace literals;
|
||||||
|
|
||||||
TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
|
TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
|
||||||
const QString &oauthClient, const QString &userID)
|
const QString &oauthClient, const QString &userID)
|
||||||
: Account(ProviderId::Twitch)
|
: Account(ProviderId::Twitch)
|
||||||
|
@ -31,9 +38,13 @@ TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
|
||||||
, userName_(username)
|
, userName_(username)
|
||||||
, userId_(userID)
|
, userId_(userID)
|
||||||
, isAnon_(username == ANONYMOUS_USERNAME)
|
, isAnon_(username == ANONYMOUS_USERNAME)
|
||||||
|
, emoteSets_(std::make_shared<TwitchEmoteSetMap>())
|
||||||
|
, emotes_(std::make_shared<EmoteMap>())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TwitchAccount::~TwitchAccount() = default;
|
||||||
|
|
||||||
QString TwitchAccount::toString() const
|
QString TwitchAccount::toString() const
|
||||||
{
|
{
|
||||||
return this->getUserName();
|
return this->getUserName();
|
||||||
|
@ -175,214 +186,6 @@ const std::unordered_set<QString> &TwitchAccount::blockedUserIds() const
|
||||||
return this->ignoresUserIds_;
|
return this->ignoresUserIds_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchAccount::loadEmotes(std::weak_ptr<Channel> weakChannel)
|
|
||||||
{
|
|
||||||
qCDebug(chatterinoTwitch)
|
|
||||||
<< "Loading Twitch emotes for user" << this->getUserName();
|
|
||||||
|
|
||||||
if (this->getOAuthClient().isEmpty() || this->getOAuthToken().isEmpty())
|
|
||||||
{
|
|
||||||
qCDebug(chatterinoTwitch)
|
|
||||||
<< "Aborted loadEmotes due to missing Client ID and/or OAuth token";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto emoteData = this->emotes_.access();
|
|
||||||
emoteData->emoteSets.clear();
|
|
||||||
emoteData->emotes.clear();
|
|
||||||
qCDebug(chatterinoTwitch) << "Cleared emotes!";
|
|
||||||
}
|
|
||||||
|
|
||||||
this->loadUserstateEmotes(weakChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TwitchAccount::setUserstateEmoteSets(QStringList newEmoteSets)
|
|
||||||
{
|
|
||||||
newEmoteSets.sort();
|
|
||||||
|
|
||||||
if (this->userstateEmoteSets_ == newEmoteSets)
|
|
||||||
{
|
|
||||||
// Nothing has changed
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->userstateEmoteSets_ = newEmoteSets;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TwitchAccount::loadUserstateEmotes(std::weak_ptr<Channel> weakChannel)
|
|
||||||
{
|
|
||||||
if (this->userstateEmoteSets_.isEmpty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList newEmoteSetKeys, existingEmoteSetKeys;
|
|
||||||
|
|
||||||
auto emoteData = this->emotes_.access();
|
|
||||||
auto userEmoteSets = emoteData->emoteSets;
|
|
||||||
|
|
||||||
// get list of already fetched emote sets
|
|
||||||
for (const auto &userEmoteSet : userEmoteSets)
|
|
||||||
{
|
|
||||||
existingEmoteSetKeys.push_back(userEmoteSet->key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter out emote sets from userstate message, which are not in fetched emote set list
|
|
||||||
for (const auto &emoteSetKey : this->userstateEmoteSets_)
|
|
||||||
{
|
|
||||||
if (!existingEmoteSetKeys.contains(emoteSetKey))
|
|
||||||
{
|
|
||||||
newEmoteSetKeys.push_back(emoteSetKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return if there are no new emote sets
|
|
||||||
if (newEmoteSetKeys.isEmpty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// requesting emotes
|
|
||||||
auto batches = splitListIntoBatches(newEmoteSetKeys);
|
|
||||||
for (int i = 0; i < batches.size(); i++)
|
|
||||||
{
|
|
||||||
qCDebug(chatterinoTwitch)
|
|
||||||
<< QString(
|
|
||||||
"Loading %1 emotesets from IVR; batch %2/%3 (%4 sets): %5")
|
|
||||||
.arg(newEmoteSetKeys.size())
|
|
||||||
.arg(i + 1)
|
|
||||||
.arg(batches.size())
|
|
||||||
.arg(batches.at(i).size())
|
|
||||||
.arg(batches.at(i).join(","));
|
|
||||||
getIvr()->getBulkEmoteSets(
|
|
||||||
batches.at(i).join(","),
|
|
||||||
[this, weakChannel](QJsonArray emoteSetArray) {
|
|
||||||
auto emoteData = this->emotes_.access();
|
|
||||||
auto localEmoteData = this->localEmotes_.access();
|
|
||||||
|
|
||||||
std::unordered_set<QString> subscriberChannelIDs;
|
|
||||||
std::vector<IvrEmoteSet> ivrEmoteSets;
|
|
||||||
ivrEmoteSets.reserve(emoteSetArray.size());
|
|
||||||
|
|
||||||
for (auto emoteSet : emoteSetArray)
|
|
||||||
{
|
|
||||||
IvrEmoteSet ivrEmoteSet(emoteSet.toObject());
|
|
||||||
if (!ivrEmoteSet.tier.isNull())
|
|
||||||
{
|
|
||||||
subscriberChannelIDs.insert(ivrEmoteSet.channelId);
|
|
||||||
}
|
|
||||||
ivrEmoteSets.emplace_back(ivrEmoteSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto &emoteSet : emoteData->emoteSets)
|
|
||||||
{
|
|
||||||
if (emoteSet->subscriber)
|
|
||||||
{
|
|
||||||
subscriberChannelIDs.insert(emoteSet->channelID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto &ivrEmoteSet : ivrEmoteSets)
|
|
||||||
{
|
|
||||||
auto emoteSet = std::make_shared<EmoteSet>();
|
|
||||||
|
|
||||||
QString setKey = ivrEmoteSet.setId;
|
|
||||||
emoteSet->key = setKey;
|
|
||||||
|
|
||||||
// check if the emoteset is already in emoteData
|
|
||||||
auto isAlreadyFetched =
|
|
||||||
std::find_if(emoteData->emoteSets.begin(),
|
|
||||||
emoteData->emoteSets.end(),
|
|
||||||
[setKey](std::shared_ptr<EmoteSet> set) {
|
|
||||||
return (set->key == setKey);
|
|
||||||
});
|
|
||||||
if (isAlreadyFetched != emoteData->emoteSets.end())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
emoteSet->channelID = ivrEmoteSet.channelId;
|
|
||||||
emoteSet->channelName = ivrEmoteSet.login;
|
|
||||||
emoteSet->text = ivrEmoteSet.displayName;
|
|
||||||
emoteSet->subscriber = !ivrEmoteSet.tier.isNull();
|
|
||||||
|
|
||||||
// NOTE: If a user does not have a subscriber emote set, but a follower emote set, this logic will be wrong
|
|
||||||
// However, that's not a realistic problem.
|
|
||||||
bool haveSubscriberSetForChannel =
|
|
||||||
subscriberChannelIDs.contains(ivrEmoteSet.channelId);
|
|
||||||
|
|
||||||
for (const auto &emoteObj : ivrEmoteSet.emotes)
|
|
||||||
{
|
|
||||||
IvrEmote ivrEmote(emoteObj.toObject());
|
|
||||||
|
|
||||||
auto id = EmoteId{ivrEmote.id};
|
|
||||||
auto code = EmoteName{
|
|
||||||
TwitchEmotes::cleanUpEmoteCode(ivrEmote.code)};
|
|
||||||
|
|
||||||
emoteSet->emotes.push_back(TwitchEmote{id, code});
|
|
||||||
|
|
||||||
auto emote = getApp()
|
|
||||||
->getEmotes()
|
|
||||||
->getTwitchEmotes()
|
|
||||||
->getOrCreateEmote(id, code);
|
|
||||||
|
|
||||||
// Follower emotes can be only used in their origin channel
|
|
||||||
// unless the user is subscribed, then they can be used anywhere.
|
|
||||||
if (ivrEmote.emoteType == "FOLLOWER" &&
|
|
||||||
!haveSubscriberSetForChannel)
|
|
||||||
{
|
|
||||||
emoteSet->local = true;
|
|
||||||
|
|
||||||
// EmoteMap for target channel wasn't initialized yet, doing it now
|
|
||||||
if (localEmoteData->find(ivrEmoteSet.channelId) ==
|
|
||||||
localEmoteData->end())
|
|
||||||
{
|
|
||||||
localEmoteData->emplace(ivrEmoteSet.channelId,
|
|
||||||
EmoteMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
localEmoteData->at(ivrEmoteSet.channelId)
|
|
||||||
.emplace(code, emote);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
emoteData->emotes.emplace(code, emote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
|
|
||||||
[](const TwitchEmote &l, const TwitchEmote &r) {
|
|
||||||
return l.name.string < r.name.string;
|
|
||||||
});
|
|
||||||
emoteData->emoteSets.emplace_back(emoteSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto channel = weakChannel.lock(); channel != nullptr)
|
|
||||||
{
|
|
||||||
channel->addSystemMessage(
|
|
||||||
"Twitch subscriber emotes reloaded.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[] {
|
|
||||||
// fetching emotes failed, ivr API might be down
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedAccessGuard<const TwitchAccount::TwitchAccountEmoteData>
|
|
||||||
TwitchAccount::accessEmotes() const
|
|
||||||
{
|
|
||||||
return this->emotes_.accessConst();
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedAccessGuard<const std::unordered_map<QString, EmoteMap>>
|
|
||||||
TwitchAccount::accessLocalEmotes() const
|
|
||||||
{
|
|
||||||
return this->localEmotes_.accessConst();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoModActions
|
// AutoModActions
|
||||||
void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel)
|
void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel)
|
||||||
{
|
{
|
||||||
|
@ -515,4 +318,140 @@ void TwitchAccount::loadSeventvUserID()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TwitchAccount::hasEmoteSet(const EmoteSetId &id) const
|
||||||
|
{
|
||||||
|
auto emotes = this->emoteSets_.accessConst();
|
||||||
|
return emotes->get()->contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedAccessGuard<std::shared_ptr<const TwitchEmoteSetMap>>
|
||||||
|
TwitchAccount::accessEmoteSets() const
|
||||||
|
{
|
||||||
|
return this->emoteSets_.accessConst();
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedAccessGuard<std::shared_ptr<const EmoteMap>> TwitchAccount::accessEmotes()
|
||||||
|
const
|
||||||
|
{
|
||||||
|
return this->emotes_.accessConst();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<EmotePtr> TwitchAccount::twitchEmote(const EmoteName &name) const
|
||||||
|
{
|
||||||
|
auto emotes = this->emotes_.accessConst();
|
||||||
|
auto it = (*emotes)->find(name);
|
||||||
|
if (it != (*emotes)->end())
|
||||||
|
{
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchAccount::reloadEmotes(void *caller)
|
||||||
|
{
|
||||||
|
if (this->isAnon() || getApp()->isTest())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancellationToken token(false);
|
||||||
|
this->emoteToken_ = token;
|
||||||
|
|
||||||
|
auto sets = std::make_shared<TwitchEmoteSetMap>();
|
||||||
|
auto emoteMap = std::make_shared<EmoteMap>();
|
||||||
|
auto nCalls = std::make_shared<size_t>();
|
||||||
|
|
||||||
|
auto *twitchEmotes = getApp()->getEmotes()->getTwitchEmotes();
|
||||||
|
auto *twitchUsers = getApp()->getTwitchUsers();
|
||||||
|
|
||||||
|
auto addEmote = [sets, emoteMap, twitchEmotes,
|
||||||
|
twitchUsers](const HelixChannelEmote &emote) {
|
||||||
|
EmoteId id{emote.id};
|
||||||
|
EmoteName name{emote.name};
|
||||||
|
auto meta = getTwitchEmoteSetMeta(emote);
|
||||||
|
|
||||||
|
auto emotePtr = twitchEmotes->getOrCreateEmote(id, name);
|
||||||
|
if (!emoteMap->try_emplace(name, emotePtr).second)
|
||||||
|
{
|
||||||
|
// if the emote already exists, we don't want to add it to a set as
|
||||||
|
// those are assumed to be disjoint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto set = sets->find(EmoteSetId{meta.setID});
|
||||||
|
if (set == sets->end())
|
||||||
|
{
|
||||||
|
auto owner = [&]() {
|
||||||
|
if (meta.isSubLike)
|
||||||
|
{
|
||||||
|
return twitchUsers->resolveID({emote.ownerID});
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_shared<TwitchUser>(TwitchUser{
|
||||||
|
.id = u"[x-c2-global-owner]"_s,
|
||||||
|
.name = {},
|
||||||
|
.displayName = {},
|
||||||
|
});
|
||||||
|
}();
|
||||||
|
set = sets->emplace(EmoteSetId{meta.setID},
|
||||||
|
TwitchEmoteSet{
|
||||||
|
.owner = owner,
|
||||||
|
.emotes = {},
|
||||||
|
.isBits = meta.isBits,
|
||||||
|
.isSubLike = meta.isSubLike,
|
||||||
|
})
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
set->second.emotes.emplace_back(std::move(emotePtr));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto userID = this->getUserId();
|
||||||
|
qDebug(chatterinoTwitch).nospace()
|
||||||
|
<< "Loading Twitch emotes - userID: " << userID
|
||||||
|
<< ", broadcasterID: none, manualRefresh: " << (caller != nullptr);
|
||||||
|
|
||||||
|
getHelix()->getUserEmotes(
|
||||||
|
this->getUserId(), {},
|
||||||
|
[this, caller, emoteMap, sets, addEmote, nCalls](
|
||||||
|
const auto &emotes, const auto &state) mutable {
|
||||||
|
assert(emoteMap && sets);
|
||||||
|
(*nCalls)++;
|
||||||
|
qDebug(chatterinoTwitch).nospace()
|
||||||
|
<< "Got " << emotes.size() << " more emote(s)";
|
||||||
|
|
||||||
|
emoteMap->reserve(emoteMap->size() + emotes.size());
|
||||||
|
for (const auto &emote : emotes)
|
||||||
|
{
|
||||||
|
addEmote(emote);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.done)
|
||||||
|
{
|
||||||
|
qDebug(chatterinoTwitch).nospace()
|
||||||
|
<< "Loaded " << emoteMap->size() << " Twitch emotes ("
|
||||||
|
<< *nCalls << " requests)";
|
||||||
|
|
||||||
|
for (auto &[id, set] : *sets)
|
||||||
|
{
|
||||||
|
std::ranges::sort(
|
||||||
|
set.emotes, [](const auto &l, const auto &r) {
|
||||||
|
return l->name.string < r->name.string;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
*this->emotes_.access() = std::move(emoteMap);
|
||||||
|
*this->emoteSets_.access() = std::move(sets);
|
||||||
|
getApp()->getAccounts()->twitch.emotesReloaded.invoke(caller,
|
||||||
|
{});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[caller](const auto &error) {
|
||||||
|
qDebug(chatterinoTwitch)
|
||||||
|
<< "Failed to load Twitch emotes:" << error;
|
||||||
|
getApp()->getAccounts()->twitch.emotesReloaded.invoke(
|
||||||
|
caller, makeUnexpected(error));
|
||||||
|
},
|
||||||
|
std::move(token));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "common/Aliases.hpp"
|
|
||||||
#include "common/Atomic.hpp"
|
#include "common/Atomic.hpp"
|
||||||
#include "common/UniqueAccess.hpp"
|
#include "common/UniqueAccess.hpp"
|
||||||
#include "controllers/accounts/Account.hpp"
|
#include "controllers/accounts/Account.hpp"
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
|
#include "providers/twitch/TwitchEmotes.hpp"
|
||||||
#include "providers/twitch/TwitchUser.hpp"
|
#include "providers/twitch/TwitchUser.hpp"
|
||||||
#include "util/CancellationToken.hpp"
|
#include "util/CancellationToken.hpp"
|
||||||
#include "util/QStringHash.hpp"
|
#include "util/QStringHash.hpp"
|
||||||
|
|
||||||
|
#include <boost/unordered/unordered_flat_map_fwd.hpp>
|
||||||
|
#include <pajlada/signals.hpp>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
@ -18,7 +20,6 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
@ -28,31 +29,13 @@ using ChannelPtr = std::shared_ptr<Channel>;
|
||||||
class TwitchAccount : public Account
|
class TwitchAccount : public Account
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
struct TwitchEmote {
|
|
||||||
EmoteId id;
|
|
||||||
EmoteName name;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct EmoteSet {
|
|
||||||
QString key;
|
|
||||||
QString channelName;
|
|
||||||
QString channelID;
|
|
||||||
QString text;
|
|
||||||
bool subscriber{false};
|
|
||||||
bool local{false};
|
|
||||||
std::vector<TwitchEmote> emotes;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TwitchAccountEmoteData {
|
|
||||||
std::vector<std::shared_ptr<EmoteSet>> emoteSets;
|
|
||||||
|
|
||||||
// this EmoteMap should contain all emotes available globally
|
|
||||||
// excluding locally available emotes, such as follower ones
|
|
||||||
EmoteMap emotes;
|
|
||||||
};
|
|
||||||
|
|
||||||
TwitchAccount(const QString &username, const QString &oauthToken_,
|
TwitchAccount(const QString &username, const QString &oauthToken_,
|
||||||
const QString &oauthClient_, const QString &_userID);
|
const QString &oauthClient_, const QString &_userID);
|
||||||
|
~TwitchAccount() override;
|
||||||
|
TwitchAccount(const TwitchAccount &) = delete;
|
||||||
|
TwitchAccount(TwitchAccount &&) = delete;
|
||||||
|
TwitchAccount &operator=(const TwitchAccount &) = delete;
|
||||||
|
TwitchAccount &operator=(TwitchAccount &&) = delete;
|
||||||
|
|
||||||
QString toString() const override;
|
QString toString() const override;
|
||||||
|
|
||||||
|
@ -91,23 +74,32 @@ public:
|
||||||
[[nodiscard]] const std::unordered_set<TwitchUser> &blocks() const;
|
[[nodiscard]] const std::unordered_set<TwitchUser> &blocks() const;
|
||||||
[[nodiscard]] const std::unordered_set<QString> &blockedUserIds() const;
|
[[nodiscard]] const std::unordered_set<QString> &blockedUserIds() const;
|
||||||
|
|
||||||
void loadEmotes(std::weak_ptr<Channel> weakChannel = {});
|
|
||||||
// loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key
|
|
||||||
// this function makes sure not to load emote sets that have already been loaded
|
|
||||||
void loadUserstateEmotes(std::weak_ptr<Channel> weakChannel = {});
|
|
||||||
// setUserStateEmoteSets sets the emote sets that were parsed from the USERSTATE emote-sets key
|
|
||||||
// Returns true if the newly inserted emote sets differ from the ones previously saved
|
|
||||||
[[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets);
|
|
||||||
SharedAccessGuard<const TwitchAccountEmoteData> accessEmotes() const;
|
|
||||||
SharedAccessGuard<const std::unordered_map<QString, EmoteMap>>
|
|
||||||
accessLocalEmotes() const;
|
|
||||||
|
|
||||||
// Automod actions
|
// Automod actions
|
||||||
void autoModAllow(const QString msgID, ChannelPtr channel);
|
void autoModAllow(const QString msgID, ChannelPtr channel);
|
||||||
void autoModDeny(const QString msgID, ChannelPtr channel);
|
void autoModDeny(const QString msgID, ChannelPtr channel);
|
||||||
|
|
||||||
void loadSeventvUserID();
|
void loadSeventvUserID();
|
||||||
|
|
||||||
|
/// Returns true if the account has access to the given emote set
|
||||||
|
bool hasEmoteSet(const EmoteSetId &id) const;
|
||||||
|
|
||||||
|
/// Return a map of emote sets the account has access to
|
||||||
|
///
|
||||||
|
/// Key being the emote set ID, and contents being information about the emote set
|
||||||
|
/// and the emotes contained in the emote set
|
||||||
|
SharedAccessGuard<std::shared_ptr<const TwitchEmoteSetMap>>
|
||||||
|
accessEmoteSets() const;
|
||||||
|
|
||||||
|
/// Return a map of emotes the account has access to
|
||||||
|
SharedAccessGuard<std::shared_ptr<const EmoteMap>> accessEmotes() const;
|
||||||
|
|
||||||
|
/// Return the emote by emote name if the account has access to the emote
|
||||||
|
std::optional<EmotePtr> twitchEmote(const EmoteName &name) const;
|
||||||
|
|
||||||
|
/// Once emotes are reloaded, TwitchAccountManager::emotesReloaded is
|
||||||
|
/// invoked with @a caller and an optional error.
|
||||||
|
void reloadEmotes(void *caller = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString oauthClient_;
|
QString oauthClient_;
|
||||||
QString oauthToken_;
|
QString oauthToken_;
|
||||||
|
@ -122,9 +114,9 @@ private:
|
||||||
std::unordered_set<TwitchUser> ignores_;
|
std::unordered_set<TwitchUser> ignores_;
|
||||||
std::unordered_set<QString> ignoresUserIds_;
|
std::unordered_set<QString> ignoresUserIds_;
|
||||||
|
|
||||||
// std::map<UserId, TwitchAccountEmoteData> emotes;
|
ScopedCancellationToken emoteToken_;
|
||||||
UniqueAccess<TwitchAccountEmoteData> emotes_;
|
UniqueAccess<std::shared_ptr<const TwitchEmoteSetMap>> emoteSets_;
|
||||||
UniqueAccess<std::unordered_map<QString, EmoteMap>> localEmotes_;
|
UniqueAccess<std::shared_ptr<const EmoteMap>> emotes_;
|
||||||
|
|
||||||
QString seventvUserID_;
|
QString seventvUserID_;
|
||||||
};
|
};
|
||||||
|
|
|
@ -162,6 +162,7 @@ void TwitchAccountManager::load()
|
||||||
}
|
}
|
||||||
|
|
||||||
this->currentUserChanged();
|
this->currentUserChanged();
|
||||||
|
this->currentUser_->reloadEmotes();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "common/ChatterinoSetting.hpp"
|
#include "common/ChatterinoSetting.hpp"
|
||||||
#include "common/SignalVector.hpp"
|
#include "common/SignalVector.hpp"
|
||||||
|
#include "util/Expected.hpp"
|
||||||
#include "util/QStringHash.hpp"
|
#include "util/QStringHash.hpp"
|
||||||
#include "util/RapidJsonSerializeQString.hpp"
|
#include "util/RapidJsonSerializeQString.hpp"
|
||||||
|
|
||||||
|
@ -58,6 +59,10 @@ public:
|
||||||
|
|
||||||
SignalVector<std::shared_ptr<TwitchAccount>> accounts;
|
SignalVector<std::shared_ptr<TwitchAccount>> accounts;
|
||||||
|
|
||||||
|
/// The signal is invoked with (caller, error) where caller is the argument
|
||||||
|
/// passed to reloadEmotes() and error.
|
||||||
|
pajlada::Signals::Signal<void *, ExpectedStr<void>> emotesReloaded;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class AddUserResponse {
|
enum class AddUserResponse {
|
||||||
UserAlreadyExists,
|
UserAlreadyExists,
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "common/Common.hpp"
|
#include "common/Common.hpp"
|
||||||
|
#include "common/Env.hpp"
|
||||||
|
#include "common/Literals.hpp"
|
||||||
#include "common/network/NetworkRequest.hpp"
|
#include "common/network/NetworkRequest.hpp"
|
||||||
#include "common/network/NetworkResult.hpp"
|
#include "common/network/NetworkResult.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
#include "providers/twitch/TwitchCommon.hpp"
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
#include "providers/twitch/TwitchUsers.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
#include "singletons/StreamerMode.hpp"
|
#include "singletons/StreamerMode.hpp"
|
||||||
|
@ -47,11 +50,15 @@
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
|
#include <QStringBuilder>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <rapidjson/document.h>
|
#include <rapidjson/document.h>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
using namespace literals;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(6, 1, 0)
|
#if QT_VERSION < QT_VERSION_CHECK(6, 1, 0)
|
||||||
const QString MAGIC_MESSAGE_SUFFIX = QString((const char *)u8" \U000E0000");
|
const QString MAGIC_MESSAGE_SUFFIX = QString((const char *)u8" \U000E0000");
|
||||||
|
@ -84,6 +91,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
||||||
, subscriptionUrl_("https://www.twitch.tv/subs/" + name)
|
, subscriptionUrl_("https://www.twitch.tv/subs/" + name)
|
||||||
, channelUrl_("https://twitch.tv/" + name)
|
, channelUrl_("https://twitch.tv/" + name)
|
||||||
, popoutPlayerUrl_(TWITCH_PLAYER_URL.arg(name))
|
, popoutPlayerUrl_(TWITCH_PLAYER_URL.arg(name))
|
||||||
|
, localTwitchEmotes_(std::make_shared<EmoteMap>())
|
||||||
, bttvEmotes_(std::make_shared<EmoteMap>())
|
, bttvEmotes_(std::make_shared<EmoteMap>())
|
||||||
, ffzEmotes_(std::make_shared<EmoteMap>())
|
, ffzEmotes_(std::make_shared<EmoteMap>())
|
||||||
, seventvEmotes_(std::make_shared<EmoteMap>())
|
, seventvEmotes_(std::make_shared<EmoteMap>())
|
||||||
|
@ -94,6 +102,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
||||||
getApp()->getAccounts()->twitch.currentUserChanged.connect([this] {
|
getApp()->getAccounts()->twitch.currentUserChanged.connect([this] {
|
||||||
this->setMod(false);
|
this->setMod(false);
|
||||||
this->refreshPubSub();
|
this->refreshPubSub();
|
||||||
|
this->refreshTwitchChannelEmotes(false);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this->refreshPubSub();
|
this->refreshPubSub();
|
||||||
|
@ -133,6 +142,36 @@ TwitchChannel::TwitchChannel(const QString &name)
|
||||||
});
|
});
|
||||||
this->threadClearTimer_.start(5 * 60 * 1000);
|
this->threadClearTimer_.start(5 * 60 * 1000);
|
||||||
|
|
||||||
|
this->signalHolder_.managedConnect(
|
||||||
|
getApp()->getAccounts()->twitch.emotesReloaded,
|
||||||
|
[this](auto *caller, const auto &result) {
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
// emotes were reloaded - clear follower emotes if the user is
|
||||||
|
// now subscribed to the streamer
|
||||||
|
if (!this->localTwitchEmotes_.get()->empty() &&
|
||||||
|
getApp()->getAccounts()->twitch.getCurrent()->hasEmoteSet(
|
||||||
|
EmoteSetId{this->localTwitchEmoteSetID_.get()}))
|
||||||
|
{
|
||||||
|
this->localTwitchEmotes_.set(std::make_shared<EmoteMap>());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caller == this)
|
||||||
|
{
|
||||||
|
this->addSystemMessage(
|
||||||
|
"Twitch subscriber emotes reloaded.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caller == this || caller == nullptr)
|
||||||
|
{
|
||||||
|
this->addSystemMessage(
|
||||||
|
u"Failed to load Twitch subscriber emotes: " %
|
||||||
|
result.error());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// debugging
|
// debugging
|
||||||
#if 0
|
#if 0
|
||||||
for (int i = 0; i < 1000; i++) {
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
@ -194,6 +233,82 @@ void TwitchChannel::setLocalizedName(const QString &name)
|
||||||
this->nameOptions.localizedName = name;
|
this->nameOptions.localizedName = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::refreshTwitchChannelEmotes(bool manualRefresh)
|
||||||
|
{
|
||||||
|
if (getApp()->isTest())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitch's 'Get User Emotes' doesn't assigns a different set-ID to follower
|
||||||
|
// emotes compared to subscriber emotes.
|
||||||
|
QString setID = TWITCH_SUB_EMOTE_SET_PREFIX % this->roomId();
|
||||||
|
this->localTwitchEmoteSetID_.set(setID);
|
||||||
|
if (getApp()->getAccounts()->twitch.getCurrent()->hasEmoteSet(
|
||||||
|
EmoteSetId{setID}))
|
||||||
|
{
|
||||||
|
this->localTwitchEmotes_.set(std::make_shared<EmoteMap>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto makeEmotes = [](const auto &emotes) {
|
||||||
|
EmoteMap map;
|
||||||
|
for (const auto &emote : emotes)
|
||||||
|
{
|
||||||
|
if (emote.type != u"follower")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map.emplace(
|
||||||
|
EmoteName{emote.name},
|
||||||
|
getApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote(
|
||||||
|
EmoteId{emote.id}, EmoteName{emote.name}));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
getHelix()->getFollowedChannel(
|
||||||
|
getApp()->getAccounts()->twitch.getCurrent()->getUserId(),
|
||||||
|
this->roomId(),
|
||||||
|
[weak{this->weak_from_this()}, makeEmotes](const auto &chan) {
|
||||||
|
auto self = std::dynamic_pointer_cast<TwitchChannel>(weak.lock());
|
||||||
|
if (!self || !chan)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getHelix()->getChannelEmotes(
|
||||||
|
self->roomId(),
|
||||||
|
[weak, makeEmotes](const auto &emotes) {
|
||||||
|
auto self =
|
||||||
|
std::dynamic_pointer_cast<TwitchChannel>(weak.lock());
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->localTwitchEmotes_.set(
|
||||||
|
std::make_shared<EmoteMap>(makeEmotes(emotes)));
|
||||||
|
},
|
||||||
|
[weak] {
|
||||||
|
auto self = weak.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->addSystemMessage("Failed to load follower emotes.");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[](const auto &error) {
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Failed to get following status:" << error;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (manualRefresh)
|
||||||
|
{
|
||||||
|
getApp()->getAccounts()->twitch.getCurrent()->reloadEmotes(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh)
|
void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh)
|
||||||
{
|
{
|
||||||
if (!Settings::instance().enableBTTVChannelEmotes)
|
if (!Settings::instance().enableBTTVChannelEmotes)
|
||||||
|
@ -536,6 +651,7 @@ void TwitchChannel::roomIdChanged()
|
||||||
this->refreshPubSub();
|
this->refreshPubSub();
|
||||||
this->refreshBadges();
|
this->refreshBadges();
|
||||||
this->refreshCheerEmotes();
|
this->refreshCheerEmotes();
|
||||||
|
this->refreshTwitchChannelEmotes(false);
|
||||||
this->refreshFFZChannelEmotes(false);
|
this->refreshFFZChannelEmotes(false);
|
||||||
this->refreshBTTVChannelEmotes(false);
|
this->refreshBTTVChannelEmotes(false);
|
||||||
this->refreshSevenTVChannelEmotes(false);
|
this->refreshSevenTVChannelEmotes(false);
|
||||||
|
@ -789,6 +905,18 @@ SharedAccessGuard<const TwitchChannel::StreamStatus>
|
||||||
return this->streamStatus_.accessConst();
|
return this->streamStatus_.accessConst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<EmotePtr> TwitchChannel::twitchEmote(const EmoteName &name) const
|
||||||
|
{
|
||||||
|
auto emotes = this->localTwitchEmotes();
|
||||||
|
auto it = emotes->find(name);
|
||||||
|
|
||||||
|
if (it == emotes->end())
|
||||||
|
{
|
||||||
|
return getApp()->getAccounts()->twitch.getCurrent()->twitchEmote(name);
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<EmotePtr> TwitchChannel::bttvEmote(const EmoteName &name) const
|
std::optional<EmotePtr> TwitchChannel::bttvEmote(const EmoteName &name) const
|
||||||
{
|
{
|
||||||
auto emotes = this->bttvEmotes_.get();
|
auto emotes = this->bttvEmotes_.get();
|
||||||
|
@ -825,6 +953,11 @@ std::optional<EmotePtr> TwitchChannel::seventvEmote(const EmoteName &name) const
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<const EmoteMap> TwitchChannel::localTwitchEmotes() const
|
||||||
|
{
|
||||||
|
return this->localTwitchEmotes_.get();
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<const EmoteMap> TwitchChannel::bttvEmotes() const
|
std::shared_ptr<const EmoteMap> TwitchChannel::bttvEmotes() const
|
||||||
{
|
{
|
||||||
return this->bttvEmotes_.get();
|
return this->bttvEmotes_.get();
|
||||||
|
|
|
@ -159,13 +159,17 @@ public:
|
||||||
void markConnected();
|
void markConnected();
|
||||||
|
|
||||||
// Emotes
|
// Emotes
|
||||||
|
std::optional<EmotePtr> twitchEmote(const EmoteName &name) const;
|
||||||
std::optional<EmotePtr> bttvEmote(const EmoteName &name) const;
|
std::optional<EmotePtr> bttvEmote(const EmoteName &name) const;
|
||||||
std::optional<EmotePtr> ffzEmote(const EmoteName &name) const;
|
std::optional<EmotePtr> ffzEmote(const EmoteName &name) const;
|
||||||
std::optional<EmotePtr> seventvEmote(const EmoteName &name) const;
|
std::optional<EmotePtr> seventvEmote(const EmoteName &name) const;
|
||||||
|
|
||||||
|
std::shared_ptr<const EmoteMap> localTwitchEmotes() const;
|
||||||
std::shared_ptr<const EmoteMap> bttvEmotes() const;
|
std::shared_ptr<const EmoteMap> bttvEmotes() const;
|
||||||
std::shared_ptr<const EmoteMap> ffzEmotes() const;
|
std::shared_ptr<const EmoteMap> ffzEmotes() const;
|
||||||
std::shared_ptr<const EmoteMap> seventvEmotes() const;
|
std::shared_ptr<const EmoteMap> seventvEmotes() const;
|
||||||
|
|
||||||
|
void refreshTwitchChannelEmotes(bool manualRefresh);
|
||||||
void refreshBTTVChannelEmotes(bool manualRefresh);
|
void refreshBTTVChannelEmotes(bool manualRefresh);
|
||||||
void refreshFFZChannelEmotes(bool manualRefresh);
|
void refreshFFZChannelEmotes(bool manualRefresh);
|
||||||
void refreshSevenTVChannelEmotes(bool manualRefresh);
|
void refreshSevenTVChannelEmotes(bool manualRefresh);
|
||||||
|
@ -391,6 +395,8 @@ private:
|
||||||
protected:
|
protected:
|
||||||
void messageRemovedFromStart(const MessagePtr &msg) override;
|
void messageRemovedFromStart(const MessagePtr &msg) override;
|
||||||
|
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> localTwitchEmotes_;
|
||||||
|
Atomic<QString> localTwitchEmoteSetID_;
|
||||||
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
|
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
|
||||||
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
|
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
|
||||||
Atomic<std::shared_ptr<const EmoteMap>> seventvEmotes_;
|
Atomic<std::shared_ptr<const EmoteMap>> seventvEmotes_;
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
#include "providers/twitch/TwitchEmotes.hpp"
|
#include "providers/twitch/TwitchEmotes.hpp"
|
||||||
|
|
||||||
|
#include "common/Literals.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
|
#include "common/UniqueAccess.hpp"
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
#include "messages/Image.hpp"
|
#include "messages/Image.hpp"
|
||||||
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "util/QStringHash.hpp"
|
#include "util/QStringHash.hpp"
|
||||||
|
|
||||||
|
#include <QStringBuilder>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
@ -399,6 +404,22 @@ qreal getEmote3xScaleFactor(const EmoteId &id)
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
using namespace literals;
|
||||||
|
|
||||||
|
QString TwitchEmoteSet::title() const
|
||||||
|
{
|
||||||
|
if (!this->owner || this->owner->name.isEmpty())
|
||||||
|
{
|
||||||
|
return "Twitch";
|
||||||
|
}
|
||||||
|
if (this->isBits)
|
||||||
|
{
|
||||||
|
return this->owner->name + " (Bits)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this->owner->name;
|
||||||
|
}
|
||||||
|
|
||||||
QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode)
|
QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode)
|
||||||
{
|
{
|
||||||
auto cleanCode = dirtyEmoteCode;
|
auto cleanCode = dirtyEmoteCode;
|
||||||
|
@ -453,4 +474,44 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id,
|
||||||
return shared;
|
return shared;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TwitchEmoteSetMeta getTwitchEmoteSetMeta(const HelixChannelEmote &emote)
|
||||||
|
{
|
||||||
|
// follower emotes are treated as sub emotes
|
||||||
|
// A sub emote must have an owner or an emote-set id (otherwise it's a
|
||||||
|
// global like "BopBop")
|
||||||
|
bool isSub =
|
||||||
|
(emote.type == u"subscriptions" || emote.type == u"follower") &&
|
||||||
|
!(emote.ownerID.isEmpty() && emote.setID.isEmpty());
|
||||||
|
bool isBits = emote.type == u"bitstier";
|
||||||
|
bool isSubLike = isSub || isBits;
|
||||||
|
|
||||||
|
// A lot of emotes don't have their emote-set-id set, so we create a
|
||||||
|
// virtual emote set that groups emotes by the owner.
|
||||||
|
// Additionally, a lot of emote sets are small, so they're grouped together as globals.
|
||||||
|
auto actualSetID = [&]() -> QString {
|
||||||
|
if (!isSub && !isBits)
|
||||||
|
{
|
||||||
|
return u"x-c2-globals"_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emote.setID.isEmpty())
|
||||||
|
{
|
||||||
|
return emote.setID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSub)
|
||||||
|
{
|
||||||
|
return TWITCH_SUB_EMOTE_SET_PREFIX % emote.ownerID;
|
||||||
|
}
|
||||||
|
// isBits
|
||||||
|
return TWITCH_BIT_EMOTE_SET_PREFIX % emote.ownerID;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return {
|
||||||
|
.setID = actualSetID,
|
||||||
|
.isBits = isBits,
|
||||||
|
.isSubLike = isSubLike,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
#include "common/Aliases.hpp"
|
#include "common/Aliases.hpp"
|
||||||
#include "common/UniqueAccess.hpp"
|
#include "common/UniqueAccess.hpp"
|
||||||
|
#include "providers/twitch/TwitchUser.hpp"
|
||||||
|
|
||||||
|
#include <boost/unordered/unordered_flat_map_fwd.hpp>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
@ -36,6 +38,45 @@ struct CheerEmoteSet {
|
||||||
std::vector<CheerEmote> cheerEmotes;
|
std::vector<CheerEmote> cheerEmotes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TwitchEmoteSet {
|
||||||
|
/// @brief The owner of this set
|
||||||
|
///
|
||||||
|
/// This owner might not be resolved yet
|
||||||
|
std::shared_ptr<TwitchUser> owner;
|
||||||
|
|
||||||
|
std::vector<EmotePtr> emotes;
|
||||||
|
|
||||||
|
/// If this is a bitstier emote set
|
||||||
|
bool isBits = false;
|
||||||
|
|
||||||
|
/// @brief If this emote set is a subscriber or similar emote set
|
||||||
|
///
|
||||||
|
/// This includes sub and bit emotes
|
||||||
|
bool isSubLike = false;
|
||||||
|
|
||||||
|
/// @brief The title of this set
|
||||||
|
///
|
||||||
|
/// We generate this based on the emote set's flags & owner
|
||||||
|
QString title() const;
|
||||||
|
};
|
||||||
|
using TwitchEmoteSetMap = boost::unordered_flat_map<EmoteSetId, TwitchEmoteSet>;
|
||||||
|
|
||||||
|
struct HelixChannelEmote;
|
||||||
|
|
||||||
|
constexpr QStringView TWITCH_SUB_EMOTE_SET_PREFIX = u"x-c2-s-";
|
||||||
|
constexpr QStringView TWITCH_BIT_EMOTE_SET_PREFIX = u"x-c2-b-";
|
||||||
|
|
||||||
|
struct TwitchEmoteSetMeta {
|
||||||
|
QString setID;
|
||||||
|
|
||||||
|
/// See TwitchEmoteSet::isBits
|
||||||
|
bool isBits = false;
|
||||||
|
/// See TwitchEmoteSet::isSubLike
|
||||||
|
bool isSubLike = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
TwitchEmoteSetMeta getTwitchEmoteSetMeta(const HelixChannelEmote &emote);
|
||||||
|
|
||||||
class ITwitchEmotes
|
class ITwitchEmotes
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -853,10 +853,6 @@ void TwitchIrcServer::readConnectionMessageReceived(
|
||||||
this->markChannelsConnected();
|
this->markChannelsConnected();
|
||||||
this->connect();
|
this->connect();
|
||||||
}
|
}
|
||||||
else if (command == "GLOBALUSERSTATE")
|
|
||||||
{
|
|
||||||
handler.handleGlobalUserStateMessage(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchIrcServer::writeConnectionMessageReceived(
|
void TwitchIrcServer::writeConnectionMessageReceived(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#include "providers/twitch/TwitchUser.hpp"
|
#include "providers/twitch/TwitchUser.hpp"
|
||||||
|
|
||||||
|
#include "debug/AssertInGuiThread.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "util/RapidjsonHelpers.hpp"
|
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
@ -12,4 +12,12 @@ void TwitchUser::fromHelixBlock(const HelixBlock &ignore)
|
||||||
this->displayName = ignore.displayName;
|
this->displayName = ignore.displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchUser::update(const HelixUser &user) const
|
||||||
|
{
|
||||||
|
assertInGuiThread();
|
||||||
|
assert(this->id == user.id);
|
||||||
|
this->name = user.login;
|
||||||
|
this->displayName = user.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
struct HelixBlock;
|
struct HelixBlock;
|
||||||
|
struct HelixUser;
|
||||||
|
|
||||||
struct TwitchUser {
|
struct TwitchUser {
|
||||||
QString id;
|
QString id;
|
||||||
|
@ -26,6 +27,8 @@ struct TwitchUser {
|
||||||
this->displayName = other.displayName;
|
this->displayName = other.displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void update(const HelixUser &user) const;
|
||||||
|
|
||||||
void fromHelixBlock(const HelixBlock &ignore);
|
void fromHelixBlock(const HelixBlock &ignore);
|
||||||
|
|
||||||
bool operator<(const TwitchUser &rhs) const
|
bool operator<(const TwitchUser &rhs) const
|
||||||
|
|
147
src/providers/twitch/TwitchUsers.cpp
Normal file
147
src/providers/twitch/TwitchUsers.cpp
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
#include "providers/twitch/TwitchUsers.hpp"
|
||||||
|
|
||||||
|
#include "common/QLogging.hpp"
|
||||||
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
|
#include "providers/twitch/TwitchUser.hpp"
|
||||||
|
|
||||||
|
#include <boost/unordered/unordered_flat_map.hpp>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto withSelf(auto *ptr, auto cb)
|
||||||
|
{
|
||||||
|
return [weak{ptr->weak_from_this()}, cb = std::move(cb)](auto &&...args) {
|
||||||
|
auto self = weak.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb(std::move(self), std::forward<decltype(args)>(args)...);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class TwitchUsersPrivate
|
||||||
|
: public std::enable_shared_from_this<TwitchUsersPrivate>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TwitchUsersPrivate();
|
||||||
|
|
||||||
|
private:
|
||||||
|
boost::unordered_flat_map<UserId, std::shared_ptr<TwitchUser>> cache;
|
||||||
|
QStringList unresolved;
|
||||||
|
QTimer nextBatchTimer;
|
||||||
|
bool isResolving = false;
|
||||||
|
|
||||||
|
std::shared_ptr<TwitchUser> makeUnresolved(const UserId &id);
|
||||||
|
void makeNextRequest();
|
||||||
|
void updateUsers(const std::vector<HelixUser> &users);
|
||||||
|
|
||||||
|
friend TwitchUsers;
|
||||||
|
};
|
||||||
|
|
||||||
|
TwitchUsers::TwitchUsers()
|
||||||
|
: private_(new TwitchUsersPrivate)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TwitchUsers::~TwitchUsers() = default;
|
||||||
|
|
||||||
|
std::shared_ptr<TwitchUser> TwitchUsers::resolveID(const UserId &id)
|
||||||
|
{
|
||||||
|
auto cached = this->private_->cache.find(id);
|
||||||
|
if (cached != this->private_->cache.end())
|
||||||
|
{
|
||||||
|
return cached->second;
|
||||||
|
}
|
||||||
|
return this->private_->makeUnresolved(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
TwitchUsersPrivate::TwitchUsersPrivate()
|
||||||
|
{
|
||||||
|
this->nextBatchTimer.setSingleShot(true);
|
||||||
|
// Wait for multiple request batches to come in before making a request
|
||||||
|
this->nextBatchTimer.setInterval(250);
|
||||||
|
|
||||||
|
QObject::connect(&this->nextBatchTimer, &QTimer::timeout, [this] {
|
||||||
|
this->makeNextRequest();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<TwitchUser> TwitchUsersPrivate::makeUnresolved(const UserId &id)
|
||||||
|
{
|
||||||
|
// assumption: Cache entry is empty so neither a shared pointer was created
|
||||||
|
// nor an entry in the unresolved list was added.
|
||||||
|
auto ptr = this->cache
|
||||||
|
.emplace(id, std::make_shared<TwitchUser>(TwitchUser{
|
||||||
|
.id = id.string,
|
||||||
|
.name = {},
|
||||||
|
.displayName = {},
|
||||||
|
}))
|
||||||
|
.first->second;
|
||||||
|
if (id.string.isEmpty())
|
||||||
|
{
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->unresolved.append(id.string);
|
||||||
|
if (!this->isResolving && !this->nextBatchTimer.isActive())
|
||||||
|
{
|
||||||
|
this->nextBatchTimer.start();
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchUsersPrivate::makeNextRequest()
|
||||||
|
{
|
||||||
|
if (this->unresolved.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->isResolving)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoTwitch) << "Tried to start request while resolving";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->isResolving = true;
|
||||||
|
|
||||||
|
auto ids = this->unresolved.mid(
|
||||||
|
0, std::min<qsizetype>(this->unresolved.size(), 100));
|
||||||
|
this->unresolved = this->unresolved.mid(ids.length());
|
||||||
|
getHelix()->fetchUsers(ids, {},
|
||||||
|
withSelf(this,
|
||||||
|
[](auto self, const auto &users) {
|
||||||
|
self->updateUsers(users);
|
||||||
|
self->isResolving = false;
|
||||||
|
self->makeNextRequest();
|
||||||
|
}),
|
||||||
|
withSelf(this, [](auto self) {
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Failed to load users";
|
||||||
|
self->isResolving = false;
|
||||||
|
self->makeNextRequest();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchUsersPrivate::updateUsers(const std::vector<HelixUser> &users)
|
||||||
|
{
|
||||||
|
for (const auto &user : users)
|
||||||
|
{
|
||||||
|
auto cached = this->cache.find(UserId{user.id});
|
||||||
|
if (cached == this->cache.end())
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoTwitch) << "Couldn't find user" << user.login
|
||||||
|
<< "with id" << user.id << "in cache";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cached->second->update(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
55
src/providers/twitch/TwitchUsers.hpp
Normal file
55
src/providers/twitch/TwitchUsers.hpp
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/Aliases.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct TwitchUser;
|
||||||
|
class TwitchChannel;
|
||||||
|
|
||||||
|
class ITwitchUsers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ITwitchUsers() = default;
|
||||||
|
virtual ~ITwitchUsers() = default;
|
||||||
|
ITwitchUsers(const ITwitchUsers &) = delete;
|
||||||
|
ITwitchUsers(ITwitchUsers &&) = delete;
|
||||||
|
ITwitchUsers &operator=(const ITwitchUsers &) = delete;
|
||||||
|
ITwitchUsers &operator=(ITwitchUsers &&) = delete;
|
||||||
|
|
||||||
|
/// @brief Resolve a TwitchUser by their ID
|
||||||
|
///
|
||||||
|
/// Users are cached. If the user wasn't resolved yet, a request will be
|
||||||
|
/// scheduled. The returned shared pointer must only be used on the GUI
|
||||||
|
/// thread as it will be updated from there.
|
||||||
|
///
|
||||||
|
/// @returns A shared reference to the TwitchUser. The `name` and
|
||||||
|
/// `displayName` might be empty if the user wasn't resolved yet or
|
||||||
|
/// they don't exist.
|
||||||
|
virtual std::shared_ptr<TwitchUser> resolveID(const UserId &id) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TwitchUsersPrivate;
|
||||||
|
class TwitchUsers : public ITwitchUsers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TwitchUsers();
|
||||||
|
~TwitchUsers() override;
|
||||||
|
TwitchUsers(const TwitchUsers &) = delete;
|
||||||
|
TwitchUsers(TwitchUsers &&) = delete;
|
||||||
|
TwitchUsers &operator=(const TwitchUsers &) = delete;
|
||||||
|
TwitchUsers &operator=(TwitchUsers &&) = delete;
|
||||||
|
|
||||||
|
/// @see ITwitchUsers::resolveID()
|
||||||
|
std::shared_ptr<TwitchUser> resolveID(const UserId &id) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Using a shared_ptr to pass to network callbacks
|
||||||
|
std::shared_ptr<TwitchUsersPrivate> private_;
|
||||||
|
|
||||||
|
friend TwitchUsersPrivate;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -540,7 +540,8 @@ void Helix::loadBlocks(QString userId,
|
||||||
size_t receivedItems = 0;
|
size_t receivedItems = 0;
|
||||||
this->paginate(
|
this->paginate(
|
||||||
u"users/blocks"_s, query,
|
u"users/blocks"_s, query,
|
||||||
[pageCallback, receivedItems](const QJsonObject &json) mutable {
|
[pageCallback, receivedItems](const QJsonObject &json,
|
||||||
|
const auto & /*state*/) mutable {
|
||||||
const auto data = json["data"_L1].toArray();
|
const auto data = json["data"_L1].toArray();
|
||||||
|
|
||||||
if (data.isEmpty())
|
if (data.isEmpty())
|
||||||
|
@ -3063,6 +3064,123 @@ void Helix::sendChatMessage(
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Helix::getUserEmotes(
|
||||||
|
QString userID, QString broadcasterID,
|
||||||
|
ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||||
|
pageCallback,
|
||||||
|
FailureCallback<QString> failureCallback, CancellationToken &&token)
|
||||||
|
{
|
||||||
|
QUrlQuery query{{u"user_id"_s, userID}};
|
||||||
|
if (!broadcasterID.isEmpty())
|
||||||
|
{
|
||||||
|
query.addQueryItem(u"broadcaster_id"_s, broadcasterID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->paginate(
|
||||||
|
u"chat/emotes/user"_s, query,
|
||||||
|
[pageCallback](const QJsonObject &json, const auto &state) mutable {
|
||||||
|
const auto data = json["data"_L1].toArray();
|
||||||
|
|
||||||
|
if (data.isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<HelixChannelEmote> emotes;
|
||||||
|
emotes.reserve(data.count());
|
||||||
|
|
||||||
|
for (const auto &emote : data)
|
||||||
|
{
|
||||||
|
emotes.emplace_back(emote.toObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
pageCallback(emotes, state);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[failureCallback](const NetworkResult &result) {
|
||||||
|
if (!result.status())
|
||||||
|
{
|
||||||
|
failureCallback(result.formatError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto obj = result.parseJson();
|
||||||
|
auto message = obj["message"].toString();
|
||||||
|
|
||||||
|
switch (*result.status())
|
||||||
|
{
|
||||||
|
case 401: {
|
||||||
|
if (message.startsWith("Missing scope",
|
||||||
|
Qt::CaseInsensitive))
|
||||||
|
{
|
||||||
|
failureCallback("Missing required scope. Re-login with "
|
||||||
|
"your account and try again.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[[fallthrough]];
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Helix get user emotes, unhandled error data:"
|
||||||
|
<< result.formatError() << result.getData() << obj;
|
||||||
|
failureCallback(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
std::move(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Helix::getFollowedChannel(
|
||||||
|
QString userID, QString broadcasterID,
|
||||||
|
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback)
|
||||||
|
{
|
||||||
|
this->makeGet("channels/followed",
|
||||||
|
{
|
||||||
|
{u"user_id"_s, userID},
|
||||||
|
{u"broadcaster_id"_s, broadcasterID},
|
||||||
|
})
|
||||||
|
.onSuccess([successCallback](auto result) {
|
||||||
|
if (result.status() != 200)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Success result for getting badges was "
|
||||||
|
<< result.formatError() << "but we expected it to be 200";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto response = result.parseJson();
|
||||||
|
const auto channel = response["data"_L1].toArray().at(0);
|
||||||
|
if (channel.isObject())
|
||||||
|
{
|
||||||
|
successCallback(HelixFollowedChannel(channel.toObject()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
successCallback(std::nullopt);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError([failureCallback](const auto &result) -> void {
|
||||||
|
if (!result.status())
|
||||||
|
{
|
||||||
|
failureCallback(result.formatError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto obj = result.parseJson();
|
||||||
|
auto message = obj.value("message").toString();
|
||||||
|
if (!message.isEmpty())
|
||||||
|
{
|
||||||
|
failureCallback(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failureCallback(result.formatError());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery,
|
NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery,
|
||||||
NetworkRequestType type)
|
NetworkRequestType type)
|
||||||
{
|
{
|
||||||
|
@ -3120,8 +3238,10 @@ NetworkRequest Helix::makePatch(const QString &url, const QUrlQuery &urlQuery)
|
||||||
return this->makeRequest(url, urlQuery, NetworkRequestType::Patch);
|
return this->makeRequest(url, urlQuery, NetworkRequestType::Patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Helix::paginate(const QString &url, const QUrlQuery &baseQuery,
|
void Helix::paginate(
|
||||||
std::function<bool(const QJsonObject &)> onPage,
|
const QString &url, const QUrlQuery &baseQuery,
|
||||||
|
std::function<bool(const QJsonObject &, const HelixPaginationState &state)>
|
||||||
|
onPage,
|
||||||
std::function<void(NetworkResult)> onError,
|
std::function<void(NetworkResult)> onError,
|
||||||
CancellationToken &&cancellationToken)
|
CancellationToken &&cancellationToken)
|
||||||
{
|
{
|
||||||
|
@ -3143,14 +3263,19 @@ void Helix::paginate(const QString &url, const QUrlQuery &baseQuery,
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto json = res.parseJson();
|
const auto json = res.parseJson();
|
||||||
if (!onPage(json))
|
const auto pagination = json["pagination"_L1].toObject();
|
||||||
|
|
||||||
|
auto cursor = pagination["cursor"_L1].toString();
|
||||||
|
HelixPaginationState state{.done = cursor.isEmpty()};
|
||||||
|
|
||||||
|
if (!onPage(json, state))
|
||||||
{
|
{
|
||||||
// The consumer doesn't want any more pages
|
// The consumer doesn't want any more pages
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto cursor = json["pagination"_L1]["cursor"_L1].toString();
|
// After done is set, onPage must never be called again
|
||||||
if (cursor.isEmpty())
|
if (state.done)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,18 +266,18 @@ struct HelixEmoteSetData {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HelixChannelEmote {
|
struct HelixChannelEmote {
|
||||||
const QString emoteId;
|
const QString id;
|
||||||
const QString name;
|
const QString name;
|
||||||
const QString type;
|
const QString type;
|
||||||
const QString setId;
|
const QString setID;
|
||||||
const QString url;
|
const QString ownerID;
|
||||||
|
|
||||||
explicit HelixChannelEmote(QJsonObject jsonObject)
|
explicit HelixChannelEmote(const QJsonObject &jsonObject)
|
||||||
: emoteId(jsonObject.value("id").toString())
|
: id(jsonObject["id"].toString())
|
||||||
, name(jsonObject.value("name").toString())
|
, name(jsonObject["name"].toString())
|
||||||
, type(jsonObject.value("emote_type").toString())
|
, type(jsonObject["emote_type"].toString())
|
||||||
, setId(jsonObject.value("emote_set_id").toString())
|
, setID(jsonObject["emote_set_id"].toString())
|
||||||
, url(TWITCH_EMOTE_TEMPLATE.arg(this->emoteId, u"3.0"))
|
, ownerID(jsonObject["owner_id"].toString())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -438,6 +438,22 @@ struct HelixSentMessage {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct HelixFollowedChannel {
|
||||||
|
QString broadcasterID;
|
||||||
|
QString broadcasterLogin;
|
||||||
|
QString broadcasterName;
|
||||||
|
QDateTime followedAt;
|
||||||
|
|
||||||
|
explicit HelixFollowedChannel(const QJsonObject &jsonObject)
|
||||||
|
: broadcasterID(jsonObject["broadcaster_id"].toString())
|
||||||
|
, broadcasterLogin(jsonObject["broadcaster_login"].toString())
|
||||||
|
, broadcasterName(jsonObject["broadcaster_name"].toString())
|
||||||
|
, followedAt(QDateTime::fromString(jsonObject["followed_at"].toString(),
|
||||||
|
Qt::ISODate))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
struct HelixSendMessageArgs {
|
struct HelixSendMessageArgs {
|
||||||
QString broadcasterID;
|
QString broadcasterID;
|
||||||
QString senderID;
|
QString senderID;
|
||||||
|
@ -786,6 +802,10 @@ struct HelixError {
|
||||||
|
|
||||||
using HelixGetChannelBadgesError = HelixGetGlobalBadgesError;
|
using HelixGetChannelBadgesError = HelixGetGlobalBadgesError;
|
||||||
|
|
||||||
|
struct HelixPaginationState {
|
||||||
|
bool done;
|
||||||
|
};
|
||||||
|
|
||||||
class IHelix
|
class IHelix
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -1112,6 +1132,21 @@ public:
|
||||||
ResultCallback<HelixSentMessage> successCallback,
|
ResultCallback<HelixSentMessage> successCallback,
|
||||||
FailureCallback<HelixSendMessageError, QString> failureCallback) = 0;
|
FailureCallback<HelixSendMessageError, QString> failureCallback) = 0;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference/#get-user-emotes
|
||||||
|
virtual void getUserEmotes(
|
||||||
|
QString userID, QString broadcasterID,
|
||||||
|
ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||||
|
pageCallback,
|
||||||
|
FailureCallback<QString> failureCallback,
|
||||||
|
CancellationToken &&token) = 0;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference/#get-followed-channels
|
||||||
|
/// (non paginated)
|
||||||
|
virtual void getFollowedChannel(
|
||||||
|
QString userID, QString broadcasterID,
|
||||||
|
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback) = 0;
|
||||||
|
|
||||||
virtual void update(QString clientId, QString oauthToken) = 0;
|
virtual void update(QString clientId, QString oauthToken) = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -1440,6 +1475,21 @@ public:
|
||||||
ResultCallback<HelixSentMessage> successCallback,
|
ResultCallback<HelixSentMessage> successCallback,
|
||||||
FailureCallback<HelixSendMessageError, QString> failureCallback) final;
|
FailureCallback<HelixSendMessageError, QString> failureCallback) final;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference/#get-user-emotes
|
||||||
|
void getUserEmotes(
|
||||||
|
QString userID, QString broadcasterID,
|
||||||
|
ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||||
|
pageCallback,
|
||||||
|
FailureCallback<QString> failureCallback,
|
||||||
|
CancellationToken &&token) final;
|
||||||
|
|
||||||
|
/// https://dev.twitch.tv/docs/api/reference/#get-followed-channels
|
||||||
|
/// (non paginated)
|
||||||
|
void getFollowedChannel(
|
||||||
|
QString userID, QString broadcasterID,
|
||||||
|
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||||
|
FailureCallback<QString> failureCallback) final;
|
||||||
|
|
||||||
void update(QString clientId, QString oauthToken) final;
|
void update(QString clientId, QString oauthToken) final;
|
||||||
|
|
||||||
static void initialize();
|
static void initialize();
|
||||||
|
@ -1494,7 +1544,9 @@ private:
|
||||||
/// Paginate the `url` endpoint and use `baseQuery` as the starting point for pagination.
|
/// Paginate the `url` endpoint and use `baseQuery` as the starting point for pagination.
|
||||||
/// @param onPage returns true while a new page is expected. Once false is returned, pagination will stop.
|
/// @param onPage returns true while a new page is expected. Once false is returned, pagination will stop.
|
||||||
void paginate(const QString &url, const QUrlQuery &baseQuery,
|
void paginate(const QString &url, const QUrlQuery &baseQuery,
|
||||||
std::function<bool(const QJsonObject &)> onPage,
|
std::function<bool(const QJsonObject &,
|
||||||
|
const HelixPaginationState &state)>
|
||||||
|
onPage,
|
||||||
std::function<void(NetworkResult)> onError,
|
std::function<void(NetworkResult)> onError,
|
||||||
CancellationToken &&token);
|
CancellationToken &&token);
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
@ -10,24 +11,30 @@ namespace chatterino {
|
||||||
class CancellationToken
|
class CancellationToken
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CancellationToken() = default;
|
CancellationToken() noexcept = default;
|
||||||
explicit CancellationToken(bool isCancelled)
|
explicit CancellationToken(bool isCancelled)
|
||||||
: isCancelled_(new std::atomic<bool>(isCancelled))
|
: isCancelled_(new std::atomic<bool>(isCancelled))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
CancellationToken(const CancellationToken &) = default;
|
CancellationToken(const CancellationToken &) = default;
|
||||||
CancellationToken(CancellationToken &&other)
|
CancellationToken(CancellationToken &&other) noexcept
|
||||||
: isCancelled_(std::move(other.isCancelled_)){};
|
: isCancelled_(std::move(other.isCancelled_)){};
|
||||||
|
|
||||||
CancellationToken &operator=(CancellationToken &&other)
|
/// @brief This destructor doesn't cancel the token
|
||||||
|
///
|
||||||
|
/// @see ScopedCancellationToken
|
||||||
|
/// @see #cancel()
|
||||||
|
~CancellationToken() noexcept = default;
|
||||||
|
|
||||||
|
CancellationToken &operator=(CancellationToken &&other) noexcept
|
||||||
{
|
{
|
||||||
this->isCancelled_ = std::move(other.isCancelled_);
|
this->isCancelled_ = std::move(other.isCancelled_);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
CancellationToken &operator=(const CancellationToken &) = default;
|
CancellationToken &operator=(const CancellationToken &) = default;
|
||||||
|
|
||||||
void cancel()
|
void cancel() noexcept
|
||||||
{
|
{
|
||||||
if (this->isCancelled_ != nullptr)
|
if (this->isCancelled_ != nullptr)
|
||||||
{
|
{
|
||||||
|
@ -35,7 +42,7 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCancelled() const
|
bool isCancelled() const noexcept
|
||||||
{
|
{
|
||||||
return this->isCancelled_ == nullptr ||
|
return this->isCancelled_ == nullptr ||
|
||||||
this->isCancelled_->load(std::memory_order_acquire);
|
this->isCancelled_->load(std::memory_order_acquire);
|
||||||
|
@ -50,30 +57,35 @@ class ScopedCancellationToken
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ScopedCancellationToken() = default;
|
ScopedCancellationToken() = default;
|
||||||
ScopedCancellationToken(CancellationToken &&backingToken)
|
explicit ScopedCancellationToken(CancellationToken backingToken)
|
||||||
: backingToken_(std::move(backingToken))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
ScopedCancellationToken(CancellationToken backingToken)
|
|
||||||
: backingToken_(std::move(backingToken))
|
: backingToken_(std::move(backingToken))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScopedCancellationToken(const ScopedCancellationToken &) = delete;
|
||||||
|
ScopedCancellationToken(ScopedCancellationToken &&other) noexcept
|
||||||
|
: backingToken_(std::move(other.backingToken_)){};
|
||||||
|
|
||||||
~ScopedCancellationToken()
|
~ScopedCancellationToken()
|
||||||
{
|
{
|
||||||
this->backingToken_.cancel();
|
this->backingToken_.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedCancellationToken(const ScopedCancellationToken &) = delete;
|
ScopedCancellationToken &operator=(CancellationToken token) noexcept
|
||||||
ScopedCancellationToken(ScopedCancellationToken &&other)
|
|
||||||
: backingToken_(std::move(other.backingToken_)){};
|
|
||||||
ScopedCancellationToken &operator=(ScopedCancellationToken &&other)
|
|
||||||
{
|
{
|
||||||
|
this->backingToken_.cancel();
|
||||||
|
this->backingToken_ = std::move(token);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedCancellationToken &operator=(const ScopedCancellationToken &) =
|
||||||
|
delete;
|
||||||
|
ScopedCancellationToken &operator=(ScopedCancellationToken &&other) noexcept
|
||||||
|
{
|
||||||
|
this->backingToken_.cancel();
|
||||||
this->backingToken_ = std::move(other.backingToken_);
|
this->backingToken_ = std::move(other.backingToken_);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
ScopedCancellationToken &operator=(const ScopedCancellationToken &) =
|
|
||||||
delete;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CancellationToken backingToken_;
|
CancellationToken backingToken_;
|
||||||
|
|
22
src/util/Expected.hpp
Normal file
22
src/util/Expected.hpp
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <nonstd/expected.hpp>
|
||||||
|
|
||||||
|
class QString;
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
template <typename T, typename E>
|
||||||
|
using Expected = nonstd::expected_lite::expected<T, E>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using ExpectedStr = Expected<T, QString>;
|
||||||
|
|
||||||
|
// convenience function from nonstd/expected.hpp
|
||||||
|
template <typename E>
|
||||||
|
constexpr nonstd::unexpected<std::decay_t<E>> makeUnexpected(E &&value)
|
||||||
|
{
|
||||||
|
return nonstd::unexpected<std::decay_t<E>>(std::forward<E>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
#ifdef USEWINSDK
|
#ifdef USEWINSDK
|
||||||
# include <dwmapi.h>
|
# include <dwmapi.h>
|
||||||
|
# include <shellapi.h>
|
||||||
# include <VersionHelpers.h>
|
# include <VersionHelpers.h>
|
||||||
# include <Windows.h>
|
# include <Windows.h>
|
||||||
# include <windowsx.h>
|
# include <windowsx.h>
|
||||||
|
@ -32,7 +33,6 @@
|
||||||
# include <QHBoxLayout>
|
# include <QHBoxLayout>
|
||||||
# include <QMargins>
|
# include <QMargins>
|
||||||
# include <QOperatingSystemVersion>
|
# include <QOperatingSystemVersion>
|
||||||
# include <QWindow>
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "widgets/helper/TitlebarButton.hpp"
|
#include "widgets/helper/TitlebarButton.hpp"
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
#include <QAbstractButton>
|
#include <QAbstractButton>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QStringBuilder>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
@ -42,13 +43,14 @@ auto makeTitleMessage(const QString &title)
|
||||||
return builder.release();
|
return builder.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto makeEmoteMessage(const EmoteMap &map, const MessageElementFlag &emoteFlag)
|
auto makeEmoteMessage(std::vector<EmotePtr> emotes,
|
||||||
|
const MessageElementFlag &emoteFlag)
|
||||||
{
|
{
|
||||||
MessageBuilder builder;
|
MessageBuilder builder;
|
||||||
builder->flags.set(MessageFlag::Centered);
|
builder->flags.set(MessageFlag::Centered);
|
||||||
builder->flags.set(MessageFlag::DisableCompactEmotes);
|
builder->flags.set(MessageFlag::DisableCompactEmotes);
|
||||||
|
|
||||||
if (map.empty())
|
if (emotes.empty())
|
||||||
{
|
{
|
||||||
builder.emplace<TextElement>("no emotes available",
|
builder.emplace<TextElement>("no emotes available",
|
||||||
MessageElementFlag::Text,
|
MessageElementFlag::Text,
|
||||||
|
@ -56,24 +58,41 @@ auto makeEmoteMessage(const EmoteMap &map, const MessageElementFlag &emoteFlag)
|
||||||
return builder.release();
|
return builder.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::pair<EmoteName, EmotePtr>> vec(map.begin(), map.end());
|
std::sort(emotes.begin(), emotes.end(), [](const auto &l, const auto &r) {
|
||||||
std::sort(vec.begin(), vec.end(),
|
return compareEmoteStrings(l->name.string, r->name.string);
|
||||||
[](const std::pair<EmoteName, EmotePtr> &l,
|
|
||||||
const std::pair<EmoteName, EmotePtr> &r) {
|
|
||||||
return compareEmoteStrings(l.first.string, r.first.string);
|
|
||||||
});
|
});
|
||||||
for (const auto &emote : vec)
|
for (const auto &emote : emotes)
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
.emplace<EmoteElement>(
|
.emplace<EmoteElement>(
|
||||||
emote.second,
|
emote,
|
||||||
MessageElementFlags{MessageElementFlag::AlwaysShow, emoteFlag})
|
MessageElementFlags{MessageElementFlag::AlwaysShow, emoteFlag})
|
||||||
->setLink(Link(Link::InsertText, emote.first.string));
|
->setLink(Link(Link::InsertText, emote->name.string));
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.release();
|
return builder.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto makeEmoteMessage(const EmoteMap &map, const MessageElementFlag &emoteFlag)
|
||||||
|
{
|
||||||
|
if (map.empty())
|
||||||
|
{
|
||||||
|
MessageBuilder builder;
|
||||||
|
builder.emplace<TextElement>("no emotes available",
|
||||||
|
MessageElementFlag::Text,
|
||||||
|
MessageColor::System);
|
||||||
|
return builder.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<EmotePtr> vec;
|
||||||
|
vec.reserve(map.size());
|
||||||
|
for (const auto &[_name, ptr] : map)
|
||||||
|
{
|
||||||
|
vec.emplace_back(ptr);
|
||||||
|
}
|
||||||
|
return makeEmoteMessage(std::move(vec), emoteFlag);
|
||||||
|
}
|
||||||
|
|
||||||
auto makeEmojiMessage(const std::vector<EmojiPtr> &emojiMap)
|
auto makeEmojiMessage(const std::vector<EmojiPtr> &emojiMap)
|
||||||
{
|
{
|
||||||
MessageBuilder builder;
|
MessageBuilder builder;
|
||||||
|
@ -94,77 +113,46 @@ auto makeEmojiMessage(const std::vector<EmojiPtr> &emojiMap)
|
||||||
return builder.release();
|
return builder.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addTwitchEmoteSets(
|
void addEmotes(Channel &channel, auto emotes, const QString &title,
|
||||||
std::vector<std::shared_ptr<TwitchAccount::EmoteSet>> sets,
|
const MessageElementFlag &emoteFlag)
|
||||||
Channel &globalChannel, Channel &subChannel, QString currentChannelName)
|
|
||||||
{
|
{
|
||||||
QMap<QString, QPair<bool, std::vector<MessagePtr>>> mapOfSets;
|
channel.addMessage(makeTitleMessage(title), MessageContext::Original);
|
||||||
|
channel.addMessage(makeEmoteMessage(emotes, emoteFlag),
|
||||||
|
MessageContext::Original);
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto &set : sets)
|
void addTwitchEmoteSets(const std::shared_ptr<const EmoteMap> &local,
|
||||||
|
const std::shared_ptr<const TwitchEmoteSetMap> &sets,
|
||||||
|
Channel &globalChannel, Channel &subChannel,
|
||||||
|
const QString ¤tChannelID,
|
||||||
|
const QString &channelName)
|
||||||
{
|
{
|
||||||
// Some emotes (e.g. follower ones) are only available in their origin channel
|
if (!local->empty())
|
||||||
if (set->local && currentChannelName != set->channelName)
|
{
|
||||||
|
addEmotes(subChannel, *local, channelName % u" (follower)",
|
||||||
|
MessageElementFlag::TwitchEmote);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put current channel emotes at the top
|
||||||
|
for (const auto &[_id, set] : *sets)
|
||||||
|
{
|
||||||
|
if (set.owner->id == currentChannelID)
|
||||||
|
{
|
||||||
|
addEmotes(subChannel, set.emotes, set.title(),
|
||||||
|
MessageElementFlag::TwitchEmote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &[id, set] : *sets)
|
||||||
|
{
|
||||||
|
if (set.owner->id == currentChannelID)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TITLE
|
addEmotes(set.isSubLike ? subChannel : globalChannel, set.emotes,
|
||||||
auto channelName = set->channelName;
|
set.title(), MessageElementFlag::TwitchEmote);
|
||||||
auto text = set->text.isEmpty() ? "Twitch" : set->text;
|
|
||||||
|
|
||||||
// EMOTES
|
|
||||||
MessageBuilder builder;
|
|
||||||
builder->flags.set(MessageFlag::Centered);
|
|
||||||
builder->flags.set(MessageFlag::DisableCompactEmotes);
|
|
||||||
|
|
||||||
// If value of map is empty, create init pair and add title.
|
|
||||||
if (mapOfSets.find(channelName) == mapOfSets.end())
|
|
||||||
{
|
|
||||||
std::vector<MessagePtr> b;
|
|
||||||
b.push_back(makeTitleMessage(text));
|
|
||||||
mapOfSets[channelName] = qMakePair(set->key == "0", b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &emote : set->emotes)
|
|
||||||
{
|
|
||||||
builder
|
|
||||||
.emplace<EmoteElement>(
|
|
||||||
getApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote(
|
|
||||||
emote.id, emote.name),
|
|
||||||
MessageElementFlags{MessageElementFlag::AlwaysShow,
|
|
||||||
MessageElementFlag::TwitchEmote})
|
|
||||||
->setLink(Link(Link::InsertText, emote.name.string));
|
|
||||||
}
|
|
||||||
|
|
||||||
mapOfSets[channelName].second.push_back(builder.release());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output to channel all created messages,
|
|
||||||
// That contain title or emotes.
|
|
||||||
// Put current channel emotes at the top
|
|
||||||
auto currentChannelPair = mapOfSets[currentChannelName];
|
|
||||||
for (const auto &message : currentChannelPair.second)
|
|
||||||
{
|
|
||||||
subChannel.addMessage(message, MessageContext::Original);
|
|
||||||
}
|
|
||||||
mapOfSets.remove(currentChannelName);
|
|
||||||
|
|
||||||
for (const auto &pair : mapOfSets)
|
|
||||||
{
|
|
||||||
auto &channel = pair.first ? globalChannel : subChannel;
|
|
||||||
for (const auto &message : pair.second)
|
|
||||||
{
|
|
||||||
channel.addMessage(message, MessageContext::Original);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addEmotes(Channel &channel, const EmoteMap &map, const QString &title,
|
|
||||||
const MessageElementFlag &emoteFlag)
|
|
||||||
{
|
|
||||||
channel.addMessage(makeTitleMessage(title), MessageContext::Original);
|
|
||||||
channel.addMessage(makeEmoteMessage(map, emoteFlag),
|
|
||||||
MessageContext::Original);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadEmojis(ChannelView &view, const std::vector<EmojiPtr> &emojiMap)
|
void loadEmojis(ChannelView &view, const std::vector<EmojiPtr> &emojiMap)
|
||||||
|
@ -200,6 +188,22 @@ EmoteMap filterEmoteMap(const QString &text,
|
||||||
return filteredMap;
|
return filteredMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<EmotePtr> filterEmoteVec(const QString &text,
|
||||||
|
const std::vector<EmotePtr> &emotes)
|
||||||
|
{
|
||||||
|
std::vector<EmotePtr> filtered;
|
||||||
|
|
||||||
|
for (const auto &emote : emotes)
|
||||||
|
{
|
||||||
|
if (emote->name.string.contains(text, Qt::CaseInsensitive))
|
||||||
|
{
|
||||||
|
filtered.emplace_back(emote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -404,8 +408,10 @@ void EmotePopup::loadChannel(ChannelPtr channel)
|
||||||
|
|
||||||
// twitch
|
// twitch
|
||||||
addTwitchEmoteSets(
|
addTwitchEmoteSets(
|
||||||
getApp()->getAccounts()->twitch.getCurrent()->accessEmotes()->emoteSets,
|
twitchChannel_->localTwitchEmotes(),
|
||||||
*globalChannel, *subChannel, this->channel_->getName());
|
*getApp()->getAccounts()->twitch.getCurrent()->accessEmoteSets(),
|
||||||
|
*globalChannel, *subChannel, twitchChannel_->roomId(),
|
||||||
|
twitchChannel_->getName());
|
||||||
|
|
||||||
// global
|
// global
|
||||||
if (Settings::instance().enableBTTVGlobalEmotes)
|
if (Settings::instance().enableBTTVGlobalEmotes)
|
||||||
|
@ -475,24 +481,22 @@ bool EmotePopup::eventFilter(QObject *object, QEvent *event)
|
||||||
void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
|
void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
|
||||||
const QString &searchText)
|
const QString &searchText)
|
||||||
{
|
{
|
||||||
auto twitchEmoteSets =
|
if (this->twitchChannel_)
|
||||||
getApp()->getAccounts()->twitch.getCurrent()->accessEmotes()->emoteSets;
|
|
||||||
std::vector<std::shared_ptr<TwitchAccount::EmoteSet>> twitchGlobalEmotes{};
|
|
||||||
|
|
||||||
for (const auto &set : twitchEmoteSets)
|
|
||||||
{
|
{
|
||||||
auto setCopy = std::make_shared<TwitchAccount::EmoteSet>(*set);
|
auto local = filterEmoteMap(searchText,
|
||||||
auto setIt =
|
this->twitchChannel_->localTwitchEmotes());
|
||||||
std::remove_if(setCopy->emotes.begin(), setCopy->emotes.end(),
|
if (!local.empty())
|
||||||
[searchText](auto &emote) {
|
|
||||||
return !emote.name.string.contains(
|
|
||||||
searchText, Qt::CaseInsensitive);
|
|
||||||
});
|
|
||||||
setCopy->emotes.resize(std::distance(setCopy->emotes.begin(), setIt));
|
|
||||||
|
|
||||||
if (!setCopy->emotes.empty())
|
|
||||||
{
|
{
|
||||||
twitchGlobalEmotes.push_back(setCopy);
|
addEmotes(*searchChannel, local,
|
||||||
|
this->twitchChannel_->getName() % u" (local)",
|
||||||
|
MessageElementFlag::TwitchEmote);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &[_id, set] :
|
||||||
|
**getApp()->getAccounts()->twitch.getCurrent()->accessEmoteSets())
|
||||||
|
{
|
||||||
|
addEmotes(*searchChannel, filterEmoteVec(searchText, set.emotes),
|
||||||
|
set.title(), MessageElementFlag::TwitchEmote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,10 +507,6 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
|
||||||
auto seventvGlobalEmotes = filterEmoteMap(
|
auto seventvGlobalEmotes = filterEmoteMap(
|
||||||
searchText, getApp()->getSeventvEmotes()->globalEmotes());
|
searchText, getApp()->getSeventvEmotes()->globalEmotes());
|
||||||
|
|
||||||
// twitch
|
|
||||||
addTwitchEmoteSets(twitchGlobalEmotes, *searchChannel, *searchChannel,
|
|
||||||
this->channel_->getName());
|
|
||||||
|
|
||||||
// global
|
// global
|
||||||
if (!bttvGlobalEmotes.empty())
|
if (!bttvGlobalEmotes.empty())
|
||||||
{
|
{
|
||||||
|
|
|
@ -1508,10 +1508,10 @@ void Split::showSearch(bool singleChannel)
|
||||||
void Split::reloadChannelAndSubscriberEmotes()
|
void Split::reloadChannelAndSubscriberEmotes()
|
||||||
{
|
{
|
||||||
auto channel = this->getChannel();
|
auto channel = this->getChannel();
|
||||||
getApp()->getAccounts()->twitch.getCurrent()->loadEmotes(channel);
|
|
||||||
|
|
||||||
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||||
{
|
{
|
||||||
|
twitchChannel->refreshTwitchChannelEmotes(true);
|
||||||
twitchChannel->refreshBTTVChannelEmotes(true);
|
twitchChannel->refreshBTTVChannelEmotes(true);
|
||||||
twitchChannel->refreshFFZChannelEmotes(true);
|
twitchChannel->refreshFFZChannelEmotes(true);
|
||||||
twitchChannel->refreshSevenTVChannelEmotes(true);
|
twitchChannel->refreshSevenTVChannelEmotes(true);
|
||||||
|
|
|
@ -1078,7 +1078,10 @@ void SplitHeader::reloadSubscriberEmotes()
|
||||||
this->lastReloadedSubEmotes_ = now;
|
this->lastReloadedSubEmotes_ = now;
|
||||||
|
|
||||||
auto channel = this->split_->getChannel();
|
auto channel = this->split_->getChannel();
|
||||||
getApp()->getAccounts()->twitch.getCurrent()->loadEmotes(channel);
|
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||||
|
{
|
||||||
|
twitchChannel->refreshTwitchChannelEmotes(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SplitHeader::reconnect()
|
void SplitHeader::reconnect()
|
||||||
|
|
|
@ -47,6 +47,7 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/Commands.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/Commands.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
173
tests/src/CancellationToken.cpp
Normal file
173
tests/src/CancellationToken.cpp
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
#include "util/CancellationToken.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
// CancellationToken
|
||||||
|
|
||||||
|
TEST(CancellationToken, ctor)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
CancellationToken token;
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
token.cancel();
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
token.cancel();
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
CancellationToken token(true);
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
token.cancel();
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CancellationToken, moveCtor)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
CancellationToken token2(std::move(token));
|
||||||
|
// NOLINTNEXTLINE(bugprone-use-after-move)
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
|
||||||
|
token.cancel();
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
|
||||||
|
token2.cancel();
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CancellationToken, moveAssign)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
CancellationToken token2;
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
|
||||||
|
token2 = std::move(token);
|
||||||
|
// NOLINTNEXTLINE(bugprone-use-after-move)
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
token.cancel();
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
|
||||||
|
token2.cancel();
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CancellationToken, copyCtor)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
CancellationToken token2(token);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
token2.cancel();
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CancellationToken, copyAssign)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
CancellationToken token2;
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
|
||||||
|
token2 = token;
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
token2.cancel();
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CancellationToken, dtor)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
{
|
||||||
|
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
|
||||||
|
CancellationToken token2 = token;
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScopedCancellationToken
|
||||||
|
|
||||||
|
TEST(ScopedCancellationToken, moveCancelCtor)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
{
|
||||||
|
ScopedCancellationToken scoped(std::move(token));
|
||||||
|
// NOLINTNEXTLINE(bugprone-use-after-move)
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedCancellationToken, copyCancelCtor)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
{
|
||||||
|
ScopedCancellationToken scoped(token);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedCancellationToken, moveCtor)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
{
|
||||||
|
ScopedCancellationToken scoped(token);
|
||||||
|
{
|
||||||
|
ScopedCancellationToken inner(std::move(scoped));
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedCancellationToken, moveAssign)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
CancellationToken token2(false);
|
||||||
|
{
|
||||||
|
ScopedCancellationToken scoped(token);
|
||||||
|
{
|
||||||
|
ScopedCancellationToken inner(token2);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
inner = std::move(scoped);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedCancellationToken, copyAssign)
|
||||||
|
{
|
||||||
|
CancellationToken token(false);
|
||||||
|
CancellationToken token2(false);
|
||||||
|
{
|
||||||
|
ScopedCancellationToken scoped(token);
|
||||||
|
ASSERT_FALSE(token.isCancelled());
|
||||||
|
scoped = token2;
|
||||||
|
ASSERT_FALSE(token2.isCancelled());
|
||||||
|
ASSERT_TRUE(token.isCancelled());
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(token2.isCancelled());
|
||||||
|
}
|
Loading…
Reference in a new issue