mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
feat: Live Emote Updates for 7TV (#4090)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
8fa89b4073
commit
39f7d8ac4c
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -6,7 +6,7 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.4
|
TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.5
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: test-${{ github.ref }}
|
group: test-${{ github.ref }}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
- Major: Added support for Twitch's Chat Replies. [Wiki Page](https://wiki.chatterino.com/Features/#message-replies) (#3722, #3989, #4041, #4047, #4055, #4067, #4077, #3905, #4131)
|
- Major: Added support for Twitch's Chat Replies. [Wiki Page](https://wiki.chatterino.com/Features/#message-replies) (#3722, #3989, #4041, #4047, #4055, #4067, #4077, #3905, #4131)
|
||||||
- Major: Added multi-channel searching to search dialog via keyboard shortcut. (Ctrl+Shift+F by default) (#3694, #3875)
|
- Major: Added multi-channel searching to search dialog via keyboard shortcut. (Ctrl+Shift+F by default) (#3694, #3875)
|
||||||
- Major: Added support for emotes and badges from [7TV](https://7tv.app). [Wiki Page](https://wiki.chatterino.com/Third_party_services/#7tv) (#4002, #4062)
|
- Major: Added support for emotes, badges, and live emote updates from [7TV](https://7tv.app). [Wiki Page](https://wiki.chatterino.com/Third_party_services/#7tv) (#4002, #4062, #4090)
|
||||||
- Major: Added support for Right-to-Left Languages (#3958, #4139)
|
- Major: Added support for Right-to-Left Languages (#3958, #4139)
|
||||||
- Minor: Added setting to keep more message history in splits. (#3811)
|
- Minor: Added setting to keep more message history in splits. (#3811)
|
||||||
- Minor: Added setting to keep more message history in usercards. (#3811)
|
- Minor: Added setting to keep more message history in usercards. (#3811)
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
#include "providers/irc/Irc2.hpp"
|
#include "providers/irc/Irc2.hpp"
|
||||||
#include "providers/seventv/SeventvBadges.hpp"
|
#include "providers/seventv/SeventvBadges.hpp"
|
||||||
#include "providers/seventv/SeventvEmotes.hpp"
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
#include "providers/twitch/PubSubManager.hpp"
|
#include "providers/twitch/PubSubManager.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||||
|
@ -149,6 +150,8 @@ void Application::initialize(Settings &settings, Paths &paths)
|
||||||
this->initNm(paths);
|
this->initNm(paths);
|
||||||
}
|
}
|
||||||
this->initPubSub();
|
this->initPubSub();
|
||||||
|
|
||||||
|
this->initSeventvEventAPI();
|
||||||
}
|
}
|
||||||
|
|
||||||
int Application::run(QApplication &qtApp)
|
int Application::run(QApplication &qtApp)
|
||||||
|
@ -563,6 +566,53 @@ void Application::initPubSub()
|
||||||
RequestModerationActions();
|
RequestModerationActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::initSeventvEventAPI()
|
||||||
|
{
|
||||||
|
if (!this->twitch->seventvEventAPI)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Skipping initialization as the EventAPI is disabled";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->twitch->seventvEventAPI->signals_.emoteAdded.connect(
|
||||||
|
[&](const auto &data) {
|
||||||
|
postToThread([this, data] {
|
||||||
|
this->twitch->forEachSeventvEmoteSet(
|
||||||
|
data.emoteSetID, [data](TwitchChannel &chan) {
|
||||||
|
chan.addSeventvEmote(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this->twitch->seventvEventAPI->signals_.emoteUpdated.connect(
|
||||||
|
[&](const auto &data) {
|
||||||
|
postToThread([this, data] {
|
||||||
|
this->twitch->forEachSeventvEmoteSet(
|
||||||
|
data.emoteSetID, [data](TwitchChannel &chan) {
|
||||||
|
chan.updateSeventvEmote(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this->twitch->seventvEventAPI->signals_.emoteRemoved.connect(
|
||||||
|
[&](const auto &data) {
|
||||||
|
postToThread([this, data] {
|
||||||
|
this->twitch->forEachSeventvEmoteSet(
|
||||||
|
data.emoteSetID, [data](TwitchChannel &chan) {
|
||||||
|
chan.removeSeventvEmote(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this->twitch->seventvEventAPI->signals_.userUpdated.connect(
|
||||||
|
[&](const auto &data) {
|
||||||
|
this->twitch->forEachSeventvUser(data.userID,
|
||||||
|
[data](TwitchChannel &chan) {
|
||||||
|
chan.updateSeventvUser(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this->twitch->seventvEventAPI->start();
|
||||||
|
}
|
||||||
|
|
||||||
Application *getApp()
|
Application *getApp()
|
||||||
{
|
{
|
||||||
assert(Application::instance != nullptr);
|
assert(Application::instance != nullptr);
|
||||||
|
|
|
@ -145,6 +145,7 @@ public:
|
||||||
private:
|
private:
|
||||||
void addSingleton(Singleton *singleton);
|
void addSingleton(Singleton *singleton);
|
||||||
void initPubSub();
|
void initPubSub();
|
||||||
|
void initSeventvEventAPI();
|
||||||
void initNm(Paths &paths);
|
void initNm(Paths &paths);
|
||||||
|
|
||||||
template <typename T,
|
template <typename T,
|
||||||
|
|
|
@ -224,6 +224,17 @@ set(SOURCE_FILES
|
||||||
providers/seventv/SeventvBadges.hpp
|
providers/seventv/SeventvBadges.hpp
|
||||||
providers/seventv/SeventvEmotes.cpp
|
providers/seventv/SeventvEmotes.cpp
|
||||||
providers/seventv/SeventvEmotes.hpp
|
providers/seventv/SeventvEmotes.hpp
|
||||||
|
providers/seventv/SeventvEventAPI.cpp
|
||||||
|
providers/seventv/SeventvEventAPI.hpp
|
||||||
|
|
||||||
|
providers/seventv/eventapi/SeventvEventAPIClient.cpp
|
||||||
|
providers/seventv/eventapi/SeventvEventAPIClient.hpp
|
||||||
|
providers/seventv/eventapi/SeventvEventAPIDispatch.cpp
|
||||||
|
providers/seventv/eventapi/SeventvEventAPIDispatch.hpp
|
||||||
|
providers/seventv/eventapi/SeventvEventAPIMessage.cpp
|
||||||
|
providers/seventv/eventapi/SeventvEventAPIMessage.hpp
|
||||||
|
providers/seventv/eventapi/SeventvEventAPISubscription.cpp
|
||||||
|
providers/seventv/eventapi/SeventvEventAPISubscription.hpp
|
||||||
|
|
||||||
providers/twitch/ChannelPointReward.cpp
|
providers/twitch/ChannelPointReward.cpp
|
||||||
providers/twitch/ChannelPointReward.hpp
|
providers/twitch/ChannelPointReward.hpp
|
||||||
|
|
|
@ -37,6 +37,8 @@ Q_LOGGING_CATEGORY(chatterinoRecentMessages, "chatterino.recentmessages",
|
||||||
logThreshold);
|
logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoSettings, "chatterino.settings", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoSettings, "chatterino.settings", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoSeventv, "chatterino.seventv", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoSeventv, "chatterino.seventv", logThreshold);
|
||||||
|
Q_LOGGING_CATEGORY(chatterinoSeventvEventAPI, "chatterino.seventv.eventapi",
|
||||||
|
logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode",
|
Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode",
|
||||||
logThreshold);
|
logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
|
||||||
|
|
|
@ -28,6 +28,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventv);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventv);
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventvEventAPI);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
|
||||||
|
|
|
@ -46,4 +46,23 @@ EmotePtr cachedOrMakeEmotePtr(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmoteMap::const_iterator EmoteMap::findEmote(const QString &emoteNameHint,
|
||||||
|
const QString &emoteID) const
|
||||||
|
{
|
||||||
|
auto it = this->end();
|
||||||
|
if (!emoteNameHint.isEmpty())
|
||||||
|
{
|
||||||
|
it = this->find(EmoteName{emoteNameHint});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it == this->end() || it->second->id.string != emoteID)
|
||||||
|
{
|
||||||
|
it = std::find_if(this->begin(), this->end(),
|
||||||
|
[emoteID](const auto entry) {
|
||||||
|
return entry.second->id.string == emoteID;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/Atomic.hpp"
|
||||||
#include "messages/Image.hpp"
|
#include "messages/Image.hpp"
|
||||||
#include "messages/ImageSet.hpp"
|
#include "messages/ImageSet.hpp"
|
||||||
|
|
||||||
|
#include <boost/optional.hpp>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
@ -15,6 +17,13 @@ struct Emote {
|
||||||
Tooltip tooltip;
|
Tooltip tooltip;
|
||||||
Url homePage;
|
Url homePage;
|
||||||
bool zeroWidth;
|
bool zeroWidth;
|
||||||
|
EmoteId id;
|
||||||
|
EmoteAuthor author;
|
||||||
|
/**
|
||||||
|
* If this emote is aliased, this contains
|
||||||
|
* the original (base) name of the emote.
|
||||||
|
*/
|
||||||
|
boost::optional<EmoteName> baseName;
|
||||||
|
|
||||||
// FOURTF: no solution yet, to be refactored later
|
// FOURTF: no solution yet, to be refactored later
|
||||||
const QString &getCopyString() const
|
const QString &getCopyString() const
|
||||||
|
@ -30,6 +39,20 @@ using EmotePtr = std::shared_ptr<const Emote>;
|
||||||
|
|
||||||
class EmoteMap : public std::unordered_map<EmoteName, EmotePtr>
|
class EmoteMap : public std::unordered_map<EmoteName, EmotePtr>
|
||||||
{
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Finds an emote by it's id with a hint to it's name.
|
||||||
|
*
|
||||||
|
* 1. Searches by name for the emote, checking if the ids match (fast-path).
|
||||||
|
* 2. Searches through the map for an emote with the `emoteID` (slow-path).
|
||||||
|
*
|
||||||
|
* @param emoteNameHint A hint to the name of the searched emote,
|
||||||
|
* may be empty.
|
||||||
|
* @param emoteID The emote id to search for.
|
||||||
|
* @return An iterator to the found emote (possibly this->end()).
|
||||||
|
*/
|
||||||
|
EmoteMap::const_iterator findEmote(const QString &emoteNameHint,
|
||||||
|
const QString &emoteID) const;
|
||||||
};
|
};
|
||||||
using EmoteIdMap = std::unordered_map<EmoteId, EmotePtr>;
|
using EmoteIdMap = std::unordered_map<EmoteId, EmotePtr>;
|
||||||
using WeakEmoteMap = std::unordered_map<EmoteName, std::weak_ptr<const Emote>>;
|
using WeakEmoteMap = std::unordered_map<EmoteName, std::weak_ptr<const Emote>>;
|
||||||
|
|
|
@ -45,6 +45,9 @@ enum class MessageFlag : int64_t {
|
||||||
ElevatedMessage = (1LL << 25),
|
ElevatedMessage = (1LL << 25),
|
||||||
ParticipatedThread = (1LL << 26),
|
ParticipatedThread = (1LL << 26),
|
||||||
CheerMessage = (1LL << 27),
|
CheerMessage = (1LL << 27),
|
||||||
|
LiveUpdatesAdd = (1LL << 28),
|
||||||
|
LiveUpdatesRemove = (1LL << 29),
|
||||||
|
LiveUpdatesUpdate = (1LL << 30),
|
||||||
};
|
};
|
||||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,45 @@ QRegularExpression IRC_COLOR_PARSE_REGEX(
|
||||||
"(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)",
|
"(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)",
|
||||||
QRegularExpression::UseUnicodePropertiesOption);
|
QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
|
||||||
|
QString formatUpdatedEmoteList(const QString &platform,
|
||||||
|
const std::vector<QString> &emoteNames,
|
||||||
|
bool isAdd, bool isFirstWord)
|
||||||
|
{
|
||||||
|
QString text = "";
|
||||||
|
if (isAdd)
|
||||||
|
{
|
||||||
|
text += isFirstWord ? "Added" : "added";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text += isFirstWord ? "Removed" : "removed";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoteNames.size() == 1)
|
||||||
|
{
|
||||||
|
text += QString(" %1 emote ").arg(platform);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text += QString(" %1 %2 emotes ").arg(emoteNames.size()).arg(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto i = 0;
|
||||||
|
for (const auto &emoteName : emoteNames)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
if (i > 1)
|
||||||
|
{
|
||||||
|
text += i == emoteNames.size() ? " and " : ", ";
|
||||||
|
}
|
||||||
|
text += emoteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
text += ".";
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -473,6 +512,133 @@ MessageBuilder::MessageBuilder(const AutomodUserAction &action)
|
||||||
MessageColor::System);
|
MessageColor::System);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageBuilder::MessageBuilder(LiveUpdatesAddEmoteMessageTag /*unused*/,
|
||||||
|
const QString &platform, const QString &actor,
|
||||||
|
const std::vector<QString> &emoteNames)
|
||||||
|
: MessageBuilder()
|
||||||
|
{
|
||||||
|
auto text =
|
||||||
|
formatUpdatedEmoteList(platform, emoteNames, true, actor.isEmpty());
|
||||||
|
|
||||||
|
this->emplace<TimestampElement>();
|
||||||
|
if (!actor.isEmpty())
|
||||||
|
{
|
||||||
|
this->emplace<TextElement>(actor, MessageElementFlag::Username,
|
||||||
|
MessageColor::System)
|
||||||
|
->setLink({Link::UserInfo, actor});
|
||||||
|
}
|
||||||
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||||
|
MessageColor::System);
|
||||||
|
|
||||||
|
QString finalText;
|
||||||
|
if (actor.isEmpty())
|
||||||
|
{
|
||||||
|
finalText = text;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalText = QString("%1 %2").arg(actor, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->message().loginName = actor;
|
||||||
|
this->message().messageText = finalText;
|
||||||
|
this->message().searchText = finalText;
|
||||||
|
|
||||||
|
this->message().flags.set(MessageFlag::System);
|
||||||
|
this->message().flags.set(MessageFlag::LiveUpdatesAdd);
|
||||||
|
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageBuilder::MessageBuilder(LiveUpdatesRemoveEmoteMessageTag /*unused*/,
|
||||||
|
const QString &platform, const QString &actor,
|
||||||
|
const std::vector<QString> &emoteNames)
|
||||||
|
: MessageBuilder()
|
||||||
|
{
|
||||||
|
auto text =
|
||||||
|
formatUpdatedEmoteList(platform, emoteNames, false, actor.isEmpty());
|
||||||
|
|
||||||
|
this->emplace<TimestampElement>();
|
||||||
|
if (!actor.isEmpty())
|
||||||
|
{
|
||||||
|
this->emplace<TextElement>(actor, MessageElementFlag::Username,
|
||||||
|
MessageColor::System)
|
||||||
|
->setLink({Link::UserInfo, actor});
|
||||||
|
}
|
||||||
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||||
|
MessageColor::System);
|
||||||
|
|
||||||
|
QString finalText;
|
||||||
|
if (actor.isEmpty())
|
||||||
|
{
|
||||||
|
finalText = text;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalText = QString("%1 %2").arg(actor, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->message().loginName = actor;
|
||||||
|
this->message().messageText = finalText;
|
||||||
|
this->message().searchText = finalText;
|
||||||
|
|
||||||
|
this->message().flags.set(MessageFlag::System);
|
||||||
|
this->message().flags.set(MessageFlag::LiveUpdatesRemove);
|
||||||
|
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteMessageTag /*unused*/,
|
||||||
|
const QString &platform, const QString &actor,
|
||||||
|
const QString &emoteName,
|
||||||
|
const QString &oldEmoteName)
|
||||||
|
: MessageBuilder()
|
||||||
|
{
|
||||||
|
auto text = QString("renamed %1 emote %2 to %3.")
|
||||||
|
.arg(platform, oldEmoteName, emoteName);
|
||||||
|
|
||||||
|
this->emplace<TimestampElement>();
|
||||||
|
this->emplace<TextElement>(actor, MessageElementFlag::Username,
|
||||||
|
MessageColor::System)
|
||||||
|
->setLink({Link::UserInfo, actor});
|
||||||
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||||
|
MessageColor::System);
|
||||||
|
|
||||||
|
auto finalText = QString("%1 %2").arg(actor, text);
|
||||||
|
|
||||||
|
this->message().loginName = actor;
|
||||||
|
this->message().messageText = finalText;
|
||||||
|
this->message().searchText = finalText;
|
||||||
|
|
||||||
|
this->message().flags.set(MessageFlag::System);
|
||||||
|
this->message().flags.set(MessageFlag::LiveUpdatesUpdate);
|
||||||
|
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag /*unused*/,
|
||||||
|
const QString &platform, const QString &actor,
|
||||||
|
const QString &emoteSetName)
|
||||||
|
: MessageBuilder()
|
||||||
|
{
|
||||||
|
auto text = QString("switched the active %1 Emote Set to \"%2\".")
|
||||||
|
.arg(platform, emoteSetName);
|
||||||
|
|
||||||
|
this->emplace<TimestampElement>();
|
||||||
|
this->emplace<TextElement>(actor, MessageElementFlag::Username,
|
||||||
|
MessageColor::System)
|
||||||
|
->setLink({Link::UserInfo, actor});
|
||||||
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||||
|
MessageColor::System);
|
||||||
|
|
||||||
|
auto finalText = QString("%1 %2").arg(actor, text);
|
||||||
|
|
||||||
|
this->message().loginName = actor;
|
||||||
|
this->message().messageText = finalText;
|
||||||
|
this->message().searchText = finalText;
|
||||||
|
|
||||||
|
this->message().flags.set(MessageFlag::System);
|
||||||
|
this->message().flags.set(MessageFlag::LiveUpdatesUpdate);
|
||||||
|
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
|
||||||
|
}
|
||||||
|
|
||||||
Message *MessageBuilder::operator->()
|
Message *MessageBuilder::operator->()
|
||||||
{
|
{
|
||||||
return this->message_.get();
|
return this->message_.get();
|
||||||
|
|
|
@ -19,8 +19,20 @@ struct SystemMessageTag {
|
||||||
};
|
};
|
||||||
struct TimeoutMessageTag {
|
struct TimeoutMessageTag {
|
||||||
};
|
};
|
||||||
|
struct LiveUpdatesUpdateEmoteMessageTag {
|
||||||
|
};
|
||||||
|
struct LiveUpdatesRemoveEmoteMessageTag {
|
||||||
|
};
|
||||||
|
struct LiveUpdatesAddEmoteMessageTag {
|
||||||
|
};
|
||||||
|
struct LiveUpdatesUpdateEmoteSetMessageTag {
|
||||||
|
};
|
||||||
const SystemMessageTag systemMessage{};
|
const SystemMessageTag systemMessage{};
|
||||||
const TimeoutMessageTag timeoutMessage{};
|
const TimeoutMessageTag timeoutMessage{};
|
||||||
|
const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{};
|
||||||
|
const LiveUpdatesRemoveEmoteMessageTag liveUpdatesRemoveEmoteMessage{};
|
||||||
|
const LiveUpdatesAddEmoteMessageTag liveUpdatesAddEmoteMessage{};
|
||||||
|
const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{};
|
||||||
|
|
||||||
MessagePtr makeSystemMessage(const QString &text);
|
MessagePtr makeSystemMessage(const QString &text);
|
||||||
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
||||||
|
@ -53,6 +65,19 @@ public:
|
||||||
MessageBuilder(const BanAction &action, uint32_t count = 1);
|
MessageBuilder(const BanAction &action, uint32_t count = 1);
|
||||||
MessageBuilder(const UnbanAction &action);
|
MessageBuilder(const UnbanAction &action);
|
||||||
MessageBuilder(const AutomodUserAction &action);
|
MessageBuilder(const AutomodUserAction &action);
|
||||||
|
|
||||||
|
MessageBuilder(LiveUpdatesAddEmoteMessageTag, const QString &platform,
|
||||||
|
const QString &actor,
|
||||||
|
const std::vector<QString> &emoteNames);
|
||||||
|
MessageBuilder(LiveUpdatesRemoveEmoteMessageTag, const QString &platform,
|
||||||
|
const QString &actor,
|
||||||
|
const std::vector<QString> &emoteNames);
|
||||||
|
MessageBuilder(LiveUpdatesUpdateEmoteMessageTag, const QString &platform,
|
||||||
|
const QString &actor, const QString &emoteName,
|
||||||
|
const QString &oldEmoteName);
|
||||||
|
MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag, const QString &platform,
|
||||||
|
const QString &actor, const QString &emoteSetName);
|
||||||
|
|
||||||
virtual ~MessageBuilder() = default;
|
virtual ~MessageBuilder() = default;
|
||||||
|
|
||||||
Message *operator->();
|
Message *operator->();
|
||||||
|
|
|
@ -121,27 +121,6 @@ protected:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isStarted() const
|
|
||||||
{
|
|
||||||
return this->started_.load(std::memory_order_acquire);
|
|
||||||
}
|
|
||||||
|
|
||||||
liveupdates::WebsocketClient &websocketClient_;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void start()
|
|
||||||
{
|
|
||||||
assert(!this->isStarted());
|
|
||||||
this->started_.store(true, std::memory_order_release);
|
|
||||||
this->onConnectionEstablished();
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop()
|
|
||||||
{
|
|
||||||
assert(this->isStarted());
|
|
||||||
this->started_.store(false, std::memory_order_release);
|
|
||||||
}
|
|
||||||
|
|
||||||
void close(const std::string &reason,
|
void close(const std::string &reason,
|
||||||
websocketpp::close::status::value code =
|
websocketpp::close::status::value code =
|
||||||
websocketpp::close::status::normal)
|
websocketpp::close::status::normal)
|
||||||
|
@ -165,6 +144,27 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isStarted() const
|
||||||
|
{
|
||||||
|
return this->started_.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
liveupdates::WebsocketClient &websocketClient_;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void start()
|
||||||
|
{
|
||||||
|
assert(!this->isStarted());
|
||||||
|
this->started_.store(true, std::memory_order_release);
|
||||||
|
this->onConnectionEstablished();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop()
|
||||||
|
{
|
||||||
|
assert(this->isStarted());
|
||||||
|
this->started_.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
liveupdates::WebsocketHandle handle_;
|
liveupdates::WebsocketHandle handle_;
|
||||||
std::unordered_set<Subscription> subscriptions_;
|
std::unordered_set<Subscription> subscriptions_;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
|
#include "common/Version.hpp"
|
||||||
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
||||||
#include "providers/liveupdates/BasicPubSubWebsocket.hpp"
|
#include "providers/liveupdates/BasicPubSubWebsocket.hpp"
|
||||||
#include "providers/twitch/PubSubHelpers.hpp"
|
#include "providers/twitch/PubSubHelpers.hpp"
|
||||||
|
@ -85,6 +86,8 @@ public:
|
||||||
this->websocketClient_.set_fail_handler([this](auto hdl) {
|
this->websocketClient_.set_fail_handler([this](auto hdl) {
|
||||||
this->onConnectionFail(hdl);
|
this->onConnectionFail(hdl);
|
||||||
});
|
});
|
||||||
|
this->websocketClient_.set_user_agent("Chatterino/" CHATTERINO_VERSION
|
||||||
|
" (" CHATTERINO_GIT_HASH ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~BasicPubSubManager() = default;
|
virtual ~BasicPubSubManager() = default;
|
||||||
|
|
|
@ -32,9 +32,9 @@ using namespace chatterino;
|
||||||
const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes.");
|
const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes.");
|
||||||
const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1");
|
const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1");
|
||||||
|
|
||||||
// TODO(nerix): add links to documentation (7tv.io)
|
|
||||||
const QString API_URL_USER("https://7tv.io/v3/users/twitch/%1");
|
const QString API_URL_USER("https://7tv.io/v3/users/twitch/%1");
|
||||||
const QString API_URL_GLOBAL_EMOTE_SET("https://7tv.io/v3/emote-sets/global");
|
const QString API_URL_GLOBAL_EMOTE_SET("https://7tv.io/v3/emote-sets/global");
|
||||||
|
const QString API_URL_EMOTE_SET("https://7tv.io/v3/emote-sets/%1");
|
||||||
|
|
||||||
struct CreateEmoteResult {
|
struct CreateEmoteResult {
|
||||||
Emote emote;
|
Emote emote;
|
||||||
|
@ -160,17 +160,20 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote,
|
||||||
auto emoteName = EmoteName{activeEmote["name"].toString()};
|
auto emoteName = EmoteName{activeEmote["name"].toString()};
|
||||||
auto author =
|
auto author =
|
||||||
EmoteAuthor{emoteData["owner"].toObject()["display_name"].toString()};
|
EmoteAuthor{emoteData["owner"].toObject()["display_name"].toString()};
|
||||||
auto baseEmoteName = emoteData["name"].toString();
|
auto baseEmoteName = EmoteName{emoteData["name"].toString()};
|
||||||
bool zeroWidth = isZeroWidthActive(activeEmote);
|
bool zeroWidth = isZeroWidthActive(activeEmote);
|
||||||
bool aliasedName = emoteName.string != baseEmoteName;
|
bool aliasedName = emoteName != baseEmoteName;
|
||||||
auto tooltip =
|
auto tooltip =
|
||||||
aliasedName ? createAliasedTooltip(emoteName.string, baseEmoteName,
|
aliasedName
|
||||||
|
? createAliasedTooltip(emoteName.string, baseEmoteName.string,
|
||||||
author.string, isGlobal)
|
author.string, isGlobal)
|
||||||
: createTooltip(emoteName.string, author.string, isGlobal);
|
: createTooltip(emoteName.string, author.string, isGlobal);
|
||||||
auto imageSet = makeImageSet(emoteData);
|
auto imageSet = makeImageSet(emoteData);
|
||||||
|
|
||||||
auto emote = Emote({emoteName, imageSet, tooltip,
|
auto emote =
|
||||||
Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth});
|
Emote({emoteName, imageSet, tooltip,
|
||||||
|
Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth, emoteId,
|
||||||
|
author, boost::make_optional(aliasedName, baseEmoteName)});
|
||||||
|
|
||||||
return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()};
|
return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()};
|
||||||
}
|
}
|
||||||
|
@ -217,6 +220,24 @@ EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal)
|
||||||
return emotes;
|
return emotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmotePtr createUpdatedEmote(const EmotePtr &oldEmote,
|
||||||
|
const SeventvEventAPIEmoteUpdateDispatch &dispatch)
|
||||||
|
{
|
||||||
|
bool toNonAliased = oldEmote->baseName.has_value() &&
|
||||||
|
dispatch.emoteName == oldEmote->baseName->string;
|
||||||
|
|
||||||
|
auto baseName = oldEmote->baseName.get_value_or(oldEmote->name);
|
||||||
|
auto emote = std::make_shared<const Emote>(Emote(
|
||||||
|
{EmoteName{dispatch.emoteName}, oldEmote->images,
|
||||||
|
toNonAliased
|
||||||
|
? createTooltip(dispatch.emoteName, oldEmote->author.string, false)
|
||||||
|
: createAliasedTooltip(dispatch.emoteName, baseName.string,
|
||||||
|
oldEmote->author.string, false),
|
||||||
|
oldEmote->homePage, oldEmote->zeroWidth, oldEmote->id,
|
||||||
|
oldEmote->author, boost::make_optional(!toNonAliased, baseName)}));
|
||||||
|
return emote;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -273,10 +294,9 @@ void SeventvEmotes::loadGlobalEmotes()
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SeventvEmotes::loadChannelEmotes(const std::weak_ptr<Channel> &channel,
|
void SeventvEmotes::loadChannelEmotes(
|
||||||
const QString &channelId,
|
const std::weak_ptr<Channel> &channel, const QString &channelId,
|
||||||
std::function<void(EmoteMap &&)> callback,
|
std::function<void(EmoteMap &&, ChannelInfo)> callback, bool manualRefresh)
|
||||||
bool manualRefresh)
|
|
||||||
{
|
{
|
||||||
qCDebug(chatterinoSeventv)
|
qCDebug(chatterinoSeventv)
|
||||||
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
|
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
|
||||||
|
@ -298,7 +318,21 @@ void SeventvEmotes::loadChannelEmotes(const std::weak_ptr<Channel> &channel,
|
||||||
|
|
||||||
if (hasEmotes)
|
if (hasEmotes)
|
||||||
{
|
{
|
||||||
callback(std::move(emoteMap));
|
auto user = json["user"].toObject();
|
||||||
|
|
||||||
|
size_t connectionIdx = 0;
|
||||||
|
for (const auto &conn : user["connections"].toArray())
|
||||||
|
{
|
||||||
|
if (conn.toObject()["platform"].toString() == "TWITCH")
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
connectionIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(std::move(emoteMap),
|
||||||
|
{user["id"].toString(), emoteSet["id"].toString(),
|
||||||
|
connectionIdx});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto shared = channel.lock();
|
auto shared = channel.lock();
|
||||||
|
@ -362,4 +396,110 @@ void SeventvEmotes::loadChannelEmotes(const std::weak_ptr<Channel> &channel,
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boost::optional<EmotePtr> SeventvEmotes::addEmote(
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||||
|
const SeventvEventAPIEmoteAddDispatch &dispatch)
|
||||||
|
{
|
||||||
|
// Check for visibility first, so we don't copy the map.
|
||||||
|
auto emoteData = dispatch.emoteJson["data"].toObject();
|
||||||
|
if (emoteData.empty() || !checkEmoteVisibility(emoteData))
|
||||||
|
{
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This copies the map.
|
||||||
|
EmoteMap updatedMap = *map.get();
|
||||||
|
auto result = createEmote(dispatch.emoteJson, emoteData, false);
|
||||||
|
if (!result.hasImages)
|
||||||
|
{
|
||||||
|
// Incoming emote didn't contain any images, abort
|
||||||
|
qCDebug(chatterinoSeventv)
|
||||||
|
<< "Emote without images:" << dispatch.emoteJson;
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
auto emote = std::make_shared<const Emote>(std::move(result.emote));
|
||||||
|
updatedMap[result.name] = emote;
|
||||||
|
map.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||||
|
|
||||||
|
return emote;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<EmotePtr> SeventvEmotes::updateEmote(
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||||
|
const SeventvEventAPIEmoteUpdateDispatch &dispatch)
|
||||||
|
{
|
||||||
|
auto oldMap = map.get();
|
||||||
|
auto oldEmote = oldMap->findEmote(dispatch.emoteName, dispatch.emoteID);
|
||||||
|
if (oldEmote == oldMap->end())
|
||||||
|
{
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This copies the map.
|
||||||
|
EmoteMap updatedMap = *map.get();
|
||||||
|
updatedMap.erase(oldEmote->second->name);
|
||||||
|
|
||||||
|
auto emote = createUpdatedEmote(oldEmote->second, dispatch);
|
||||||
|
updatedMap[emote->name] = emote;
|
||||||
|
map.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||||
|
|
||||||
|
return emote;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<EmotePtr> SeventvEmotes::removeEmote(
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||||
|
const SeventvEventAPIEmoteRemoveDispatch &dispatch)
|
||||||
|
{
|
||||||
|
// This copies the map.
|
||||||
|
EmoteMap updatedMap = *map.get();
|
||||||
|
auto it = updatedMap.findEmote(dispatch.emoteName, dispatch.emoteID);
|
||||||
|
if (it == updatedMap.end())
|
||||||
|
{
|
||||||
|
// We already copied the map at this point and are now discarding the copy.
|
||||||
|
// This is fine, because this case should be really rare.
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
auto emote = it->second;
|
||||||
|
updatedMap.erase(it);
|
||||||
|
map.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||||
|
|
||||||
|
return emote;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEmotes::getEmoteSet(
|
||||||
|
const QString &emoteSetId,
|
||||||
|
std::function<void(EmoteMap &&, QString)> successCallback,
|
||||||
|
std::function<void(QString)> errorCallback)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId;
|
||||||
|
|
||||||
|
NetworkRequest(API_URL_EMOTE_SET.arg(emoteSetId), NetworkRequestType::Get)
|
||||||
|
.timeout(20000)
|
||||||
|
.onSuccess([callback = std::move(successCallback),
|
||||||
|
emoteSetId](const NetworkResult &result) -> Outcome {
|
||||||
|
auto json = result.parseJson();
|
||||||
|
auto parsedEmotes = json["emotes"].toArray();
|
||||||
|
|
||||||
|
auto emoteMap = parseEmotes(parsedEmotes, false);
|
||||||
|
|
||||||
|
qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size()
|
||||||
|
<< "7TV Emotes from" << emoteSetId;
|
||||||
|
|
||||||
|
callback(std::move(emoteMap), json["name"].toString());
|
||||||
|
return Success;
|
||||||
|
})
|
||||||
|
.onError([emoteSetId, callback = std::move(errorCallback)](
|
||||||
|
const NetworkResult &result) {
|
||||||
|
if (result.status() == NetworkResult::timedoutStatus)
|
||||||
|
{
|
||||||
|
callback("timed out");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
callback(QString("status: %1").arg(result.status()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "boost/optional.hpp"
|
#include "boost/optional.hpp"
|
||||||
#include "common/Aliases.hpp"
|
#include "common/Aliases.hpp"
|
||||||
#include "common/Atomic.hpp"
|
#include "common/Atomic.hpp"
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -56,16 +57,61 @@ class EmoteMap;
|
||||||
class SeventvEmotes final
|
class SeventvEmotes final
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct ChannelInfo {
|
||||||
|
QString userID;
|
||||||
|
QString emoteSetID;
|
||||||
|
size_t twitchConnectionIndex;
|
||||||
|
};
|
||||||
|
|
||||||
SeventvEmotes();
|
SeventvEmotes();
|
||||||
|
|
||||||
std::shared_ptr<const EmoteMap> globalEmotes() const;
|
std::shared_ptr<const EmoteMap> globalEmotes() const;
|
||||||
boost::optional<EmotePtr> globalEmote(const EmoteName &name) const;
|
boost::optional<EmotePtr> globalEmote(const EmoteName &name) const;
|
||||||
void loadGlobalEmotes();
|
void loadGlobalEmotes();
|
||||||
static void loadChannelEmotes(const std::weak_ptr<Channel> &channel,
|
static void loadChannelEmotes(
|
||||||
const QString &channelId,
|
const std::weak_ptr<Channel> &channel, const QString &channelId,
|
||||||
std::function<void(EmoteMap &&)> callback,
|
std::function<void(EmoteMap &&, ChannelInfo)> callback,
|
||||||
bool manualRefresh);
|
bool manualRefresh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an emote to the `map` if it's valid.
|
||||||
|
* This will _copy_ the emote map and
|
||||||
|
* update the `Atomic`.
|
||||||
|
*
|
||||||
|
* @return The added emote if an emote was added.
|
||||||
|
*/
|
||||||
|
static boost::optional<EmotePtr> addEmote(
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||||
|
const SeventvEventAPIEmoteAddDispatch &dispatch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an emote in this `map`.
|
||||||
|
* This will _copy_ the emote map and
|
||||||
|
* update the `Atomic`.
|
||||||
|
*
|
||||||
|
* @return The updated emote if any emote was updated.
|
||||||
|
*/
|
||||||
|
static boost::optional<EmotePtr> updateEmote(
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||||
|
const SeventvEventAPIEmoteUpdateDispatch &dispatch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an emote from this `map`.
|
||||||
|
* This will _copy_ the emote map and
|
||||||
|
* update the `Atomic`.
|
||||||
|
*
|
||||||
|
* @return The removed emote if any emote was removed.
|
||||||
|
*/
|
||||||
|
static boost::optional<EmotePtr> removeEmote(
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||||
|
const SeventvEventAPIEmoteRemoveDispatch &dispatch);
|
||||||
|
|
||||||
|
/** Fetches an emote-set by its id */
|
||||||
|
static void getEmoteSet(
|
||||||
|
const QString &emoteSetId,
|
||||||
|
std::function<void(EmoteMap &&, QString)> successCallback,
|
||||||
|
std::function<void(QString)> errorCallback);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||||
};
|
};
|
||||||
|
|
249
src/providers/seventv/SeventvEventAPI.cpp
Normal file
249
src/providers/seventv/SeventvEventAPI.cpp
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
|
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp"
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
SeventvEventAPI::SeventvEventAPI(
|
||||||
|
QString host, std::chrono::milliseconds defaultHeartbeatInterval)
|
||||||
|
: BasicPubSubManager(std::move(host))
|
||||||
|
, heartbeatInterval_(defaultHeartbeatInterval)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPI::subscribeUser(const QString &userID,
|
||||||
|
const QString &emoteSetID)
|
||||||
|
{
|
||||||
|
if (!userID.isEmpty() && this->subscribedUsers_.insert(userID).second)
|
||||||
|
{
|
||||||
|
this->subscribe({userID, SeventvEventAPISubscriptionType::UpdateUser});
|
||||||
|
}
|
||||||
|
if (!emoteSetID.isEmpty() &&
|
||||||
|
this->subscribedEmoteSets_.insert(emoteSetID).second)
|
||||||
|
{
|
||||||
|
this->subscribe(
|
||||||
|
{emoteSetID, SeventvEventAPISubscriptionType::UpdateEmoteSet});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPI::unsubscribeEmoteSet(const QString &id)
|
||||||
|
{
|
||||||
|
if (this->subscribedEmoteSets_.erase(id) > 0)
|
||||||
|
{
|
||||||
|
this->unsubscribe(
|
||||||
|
{id, SeventvEventAPISubscriptionType::UpdateEmoteSet});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPI::unsubscribeUser(const QString &id)
|
||||||
|
{
|
||||||
|
if (this->subscribedUsers_.erase(id) > 0)
|
||||||
|
{
|
||||||
|
this->unsubscribe({id, SeventvEventAPISubscriptionType::UpdateUser});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<BasicPubSubClient<SeventvEventAPISubscription>>
|
||||||
|
SeventvEventAPI::createClient(liveupdates::WebsocketClient &client,
|
||||||
|
websocketpp::connection_hdl hdl)
|
||||||
|
{
|
||||||
|
auto shared = std::make_shared<SeventvEventAPIClient>(
|
||||||
|
client, hdl, this->heartbeatInterval_);
|
||||||
|
return std::static_pointer_cast<
|
||||||
|
BasicPubSubClient<SeventvEventAPISubscription>>(std::move(shared));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPI::onMessage(
|
||||||
|
websocketpp::connection_hdl hdl,
|
||||||
|
BasicPubSubManager<SeventvEventAPISubscription>::WebsocketMessagePtr msg)
|
||||||
|
{
|
||||||
|
const auto &payload = QString::fromStdString(msg->get_payload());
|
||||||
|
|
||||||
|
auto pMessage = parseSeventvEventAPIBaseMessage(payload);
|
||||||
|
|
||||||
|
if (!pMessage)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Unable to parse incoming event-api message: " << payload;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto message = *pMessage;
|
||||||
|
switch (message.op)
|
||||||
|
{
|
||||||
|
case SeventvEventAPIOpcode::Hello: {
|
||||||
|
if (auto client = this->findClient(hdl))
|
||||||
|
{
|
||||||
|
if (auto *stvClient =
|
||||||
|
dynamic_cast<SeventvEventAPIClient *>(client.get()))
|
||||||
|
{
|
||||||
|
stvClient->setHeartbeatInterval(
|
||||||
|
message.data["heartbeat_interval"].toInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SeventvEventAPIOpcode::Heartbeat: {
|
||||||
|
if (auto client = this->findClient(hdl))
|
||||||
|
{
|
||||||
|
if (auto *stvClient =
|
||||||
|
dynamic_cast<SeventvEventAPIClient *>(client.get()))
|
||||||
|
{
|
||||||
|
stvClient->handleHeartbeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SeventvEventAPIOpcode::Dispatch: {
|
||||||
|
auto dispatch = message.toInner<SeventvEventAPIDispatch>();
|
||||||
|
if (!dispatch)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Malformed dispatch" << payload;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->handleDispatch(*dispatch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SeventvEventAPIOpcode::Reconnect: {
|
||||||
|
if (auto client = this->findClient(hdl))
|
||||||
|
{
|
||||||
|
if (auto *stvClient =
|
||||||
|
dynamic_cast<SeventvEventAPIClient *>(client.get()))
|
||||||
|
{
|
||||||
|
stvClient->close("Reconnecting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
qCDebug(chatterinoSeventvEventAPI) << "Unhandled op: " << payload;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
|
||||||
|
{
|
||||||
|
switch (dispatch.type)
|
||||||
|
{
|
||||||
|
case SeventvEventAPISubscriptionType::UpdateEmoteSet: {
|
||||||
|
// dispatchBody: {
|
||||||
|
// pushed: Array<{ key, value }>,
|
||||||
|
// pulled: Array<{ key, old_value }>,
|
||||||
|
// updated: Array<{ key, value, old_value }>,
|
||||||
|
// }
|
||||||
|
for (const auto pushedRef : dispatch.body["pushed"].toArray())
|
||||||
|
{
|
||||||
|
auto pushed = pushedRef.toObject();
|
||||||
|
if (pushed["key"].toString() != "emotes")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteAddDispatch added(
|
||||||
|
dispatch, pushed["value"].toObject());
|
||||||
|
|
||||||
|
if (added.validate())
|
||||||
|
{
|
||||||
|
this->signals_.emoteAdded.invoke(added);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Invalid dispatch" << dispatch.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto updatedRef : dispatch.body["updated"].toArray())
|
||||||
|
{
|
||||||
|
auto updated = updatedRef.toObject();
|
||||||
|
if (updated["key"].toString() != "emotes")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteUpdateDispatch update(
|
||||||
|
dispatch, updated["old_value"].toObject(),
|
||||||
|
updated["value"].toObject());
|
||||||
|
|
||||||
|
if (update.validate())
|
||||||
|
{
|
||||||
|
this->signals_.emoteUpdated.invoke(update);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Invalid dispatch" << dispatch.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto pulledRef : dispatch.body["pulled"].toArray())
|
||||||
|
{
|
||||||
|
auto pulled = pulledRef.toObject();
|
||||||
|
if (pulled["key"].toString() != "emotes")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteRemoveDispatch removed(
|
||||||
|
dispatch, pulled["old_value"].toObject());
|
||||||
|
|
||||||
|
if (removed.validate())
|
||||||
|
{
|
||||||
|
this->signals_.emoteRemoved.invoke(removed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Invalid dispatch" << dispatch.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SeventvEventAPISubscriptionType::UpdateUser: {
|
||||||
|
// dispatchBody: {
|
||||||
|
// updated: Array<{ key, value: Array<{key, value}> }>
|
||||||
|
// }
|
||||||
|
for (const auto updatedRef : dispatch.body["updated"].toArray())
|
||||||
|
{
|
||||||
|
auto updated = updatedRef.toObject();
|
||||||
|
if (updated["key"].toString() != "connections")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const auto valueRef : updated["value"].toArray())
|
||||||
|
{
|
||||||
|
auto value = valueRef.toObject();
|
||||||
|
if (value["key"].toString() != "emote_set")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIUserConnectionUpdateDispatch update(
|
||||||
|
dispatch, value, (size_t)updated["index"].toInt());
|
||||||
|
|
||||||
|
if (update.validate())
|
||||||
|
{
|
||||||
|
this->signals_.userUpdated.invoke(update);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Invalid dispatch" << dispatch.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Unknown subscription type:" << (int)dispatch.type
|
||||||
|
<< "body:" << dispatch.body;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
62
src/providers/seventv/SeventvEventAPI.hpp
Normal file
62
src/providers/seventv/SeventvEventAPI.hpp
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
||||||
|
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||||
|
#include "util/QStringHash.hpp"
|
||||||
|
|
||||||
|
#include <pajlada/signals/signal.hpp>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class SeventvEventAPI : public BasicPubSubManager<SeventvEventAPISubscription>
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
using Signal =
|
||||||
|
pajlada::Signals::Signal<T>; // type-id is vector<T, Alloc<T>>
|
||||||
|
|
||||||
|
public:
|
||||||
|
SeventvEventAPI(QString host,
|
||||||
|
std::chrono::milliseconds defaultHeartbeatInterval =
|
||||||
|
std::chrono::milliseconds(25000));
|
||||||
|
|
||||||
|
struct {
|
||||||
|
Signal<SeventvEventAPIEmoteAddDispatch> emoteAdded;
|
||||||
|
Signal<SeventvEventAPIEmoteUpdateDispatch> emoteUpdated;
|
||||||
|
Signal<SeventvEventAPIEmoteRemoveDispatch> emoteRemoved;
|
||||||
|
Signal<SeventvEventAPIUserConnectionUpdateDispatch> userUpdated;
|
||||||
|
} signals_; // NOLINT(readability-identifier-naming)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to a user and emote-set
|
||||||
|
* if not already subscribed.
|
||||||
|
*
|
||||||
|
* @param userID 7TV user-id, may be empty.
|
||||||
|
* @param emoteSetID 7TV emote-set-id, may be empty.
|
||||||
|
*/
|
||||||
|
void subscribeUser(const QString &userID, const QString &emoteSetID);
|
||||||
|
|
||||||
|
/** Unsubscribes from a user by its 7TV user id */
|
||||||
|
void unsubscribeUser(const QString &id);
|
||||||
|
/** Unsubscribes from an emote-set by its id */
|
||||||
|
void unsubscribeEmoteSet(const QString &id);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<BasicPubSubClient<SeventvEventAPISubscription>>
|
||||||
|
createClient(liveupdates::WebsocketClient &client,
|
||||||
|
websocketpp::connection_hdl hdl) override;
|
||||||
|
void onMessage(
|
||||||
|
websocketpp::connection_hdl hdl,
|
||||||
|
BasicPubSubManager<SeventvEventAPISubscription>::WebsocketMessagePtr
|
||||||
|
msg) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleDispatch(const SeventvEventAPIDispatch &dispatch);
|
||||||
|
|
||||||
|
std::unordered_set<QString> subscribedEmoteSets_;
|
||||||
|
std::unordered_set<QString> subscribedUsers_;
|
||||||
|
std::chrono::milliseconds heartbeatInterval_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
69
src/providers/seventv/eventapi/SeventvEventAPIClient.cpp
Normal file
69
src/providers/seventv/eventapi/SeventvEventAPIClient.cpp
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp"
|
||||||
|
|
||||||
|
#include "providers/twitch/PubSubHelpers.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
SeventvEventAPIClient::SeventvEventAPIClient(
|
||||||
|
liveupdates::WebsocketClient &websocketClient,
|
||||||
|
liveupdates::WebsocketHandle handle,
|
||||||
|
std::chrono::milliseconds heartbeatInterval)
|
||||||
|
: BasicPubSubClient<SeventvEventAPISubscription>(websocketClient,
|
||||||
|
std::move(handle))
|
||||||
|
, lastHeartbeat_(std::chrono::steady_clock::now())
|
||||||
|
, heartbeatInterval_(heartbeatInterval)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPIClient::onConnectionEstablished()
|
||||||
|
{
|
||||||
|
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
|
||||||
|
std::memory_order_release);
|
||||||
|
this->checkHeartbeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPIClient::setHeartbeatInterval(int intervalMs)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Setting expected heartbeat interval to" << intervalMs << "ms";
|
||||||
|
this->heartbeatInterval_ = std::chrono::milliseconds(intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPIClient::handleHeartbeat()
|
||||||
|
{
|
||||||
|
this->lastHeartbeat_.store(std::chrono::steady_clock::now(),
|
||||||
|
std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SeventvEventAPIClient::checkHeartbeat()
|
||||||
|
{
|
||||||
|
// Following the heartbeat docs, a connection is dead
|
||||||
|
// after three missed heartbeats.
|
||||||
|
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#heartbeat
|
||||||
|
assert(this->isStarted());
|
||||||
|
if ((std::chrono::steady_clock::now() - this->lastHeartbeat_.load()) >
|
||||||
|
3 * this->heartbeatInterval_)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
|
<< "Didn't receive a heartbeat in time, disconnecting!";
|
||||||
|
this->close("Didn't receive a heartbeat in time");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto self = std::dynamic_pointer_cast<SeventvEventAPIClient>(
|
||||||
|
this->shared_from_this());
|
||||||
|
|
||||||
|
runAfter(this->websocketClient_.get_io_service(), this->heartbeatInterval_,
|
||||||
|
[self](auto) {
|
||||||
|
if (!self->isStarted())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->checkHeartbeat();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
33
src/providers/seventv/eventapi/SeventvEventAPIClient.hpp
Normal file
33
src/providers/seventv/eventapi/SeventvEventAPIClient.hpp
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "providers/liveupdates/BasicPubSubClient.hpp"
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class SeventvEventAPIClient
|
||||||
|
: public BasicPubSubClient<SeventvEventAPISubscription>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SeventvEventAPIClient(liveupdates::WebsocketClient &websocketClient,
|
||||||
|
liveupdates::WebsocketHandle handle,
|
||||||
|
std::chrono::milliseconds heartbeatInterval);
|
||||||
|
|
||||||
|
void setHeartbeatInterval(int intervalMs);
|
||||||
|
void handleHeartbeat();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onConnectionEstablished() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void checkHeartbeat();
|
||||||
|
|
||||||
|
std::atomic<std::chrono::time_point<std::chrono::steady_clock>>
|
||||||
|
lastHeartbeat_;
|
||||||
|
// This will be set once on the welcome message.
|
||||||
|
std::chrono::milliseconds heartbeatInterval_;
|
||||||
|
|
||||||
|
friend class SeventvEventAPI;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
97
src/providers/seventv/eventapi/SeventvEventAPIDispatch.cpp
Normal file
97
src/providers/seventv/eventapi/SeventvEventAPIDispatch.cpp
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
SeventvEventAPIDispatch::SeventvEventAPIDispatch(QJsonObject obj)
|
||||||
|
: type(magic_enum::enum_cast<SeventvEventAPISubscriptionType>(
|
||||||
|
obj["type"].toString().toStdString())
|
||||||
|
.value_or(SeventvEventAPISubscriptionType::INVALID))
|
||||||
|
, body(obj["body"].toObject())
|
||||||
|
, id(this->body["id"].toString())
|
||||||
|
, actorName(this->body["actor"].toObject()["display_name"].toString())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteAddDispatch::SeventvEventAPIEmoteAddDispatch(
|
||||||
|
const SeventvEventAPIDispatch &dispatch, QJsonObject emote)
|
||||||
|
: emoteSetID(dispatch.id)
|
||||||
|
, actorName(dispatch.actorName)
|
||||||
|
, emoteJson(std::move(emote))
|
||||||
|
, emoteID(this->emoteJson["id"].toString())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SeventvEventAPIEmoteAddDispatch::validate() const
|
||||||
|
{
|
||||||
|
bool validValues =
|
||||||
|
!this->emoteSetID.isEmpty() && !this->emoteJson.isEmpty();
|
||||||
|
if (!validValues)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool validActiveEmote = this->emoteJson.contains("id") &&
|
||||||
|
this->emoteJson.contains("name") &&
|
||||||
|
this->emoteJson.contains("data");
|
||||||
|
if (!validActiveEmote)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto emoteData = this->emoteJson["data"].toObject();
|
||||||
|
return emoteData.contains("name") && emoteData.contains("host") &&
|
||||||
|
emoteData.contains("owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteRemoveDispatch::SeventvEventAPIEmoteRemoveDispatch(
|
||||||
|
const SeventvEventAPIDispatch &dispatch, QJsonObject emote)
|
||||||
|
: emoteSetID(dispatch.id)
|
||||||
|
, actorName(dispatch.actorName)
|
||||||
|
, emoteName(emote["name"].toString())
|
||||||
|
, emoteID(emote["id"].toString())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SeventvEventAPIEmoteRemoveDispatch::validate() const
|
||||||
|
{
|
||||||
|
return !this->emoteSetID.isEmpty() && !this->emoteName.isEmpty() &&
|
||||||
|
!this->emoteID.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteUpdateDispatch::SeventvEventAPIEmoteUpdateDispatch(
|
||||||
|
const SeventvEventAPIDispatch &dispatch, QJsonObject oldValue,
|
||||||
|
QJsonObject value)
|
||||||
|
: emoteSetID(dispatch.id)
|
||||||
|
, actorName(dispatch.actorName)
|
||||||
|
, emoteID(value["id"].toString())
|
||||||
|
, oldEmoteName(oldValue["name"].toString())
|
||||||
|
, emoteName(value["name"].toString())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SeventvEventAPIEmoteUpdateDispatch::validate() const
|
||||||
|
{
|
||||||
|
return !this->emoteSetID.isEmpty() && !this->emoteID.isEmpty() &&
|
||||||
|
!this->oldEmoteName.isEmpty() && !this->emoteName.isEmpty() &&
|
||||||
|
this->oldEmoteName != this->emoteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeventvEventAPIUserConnectionUpdateDispatch::
|
||||||
|
SeventvEventAPIUserConnectionUpdateDispatch(
|
||||||
|
const SeventvEventAPIDispatch &dispatch, const QJsonObject &update,
|
||||||
|
size_t connectionIndex)
|
||||||
|
: userID(dispatch.id)
|
||||||
|
, actorName(dispatch.actorName)
|
||||||
|
, oldEmoteSetID(update["old_value"].toObject()["id"].toString())
|
||||||
|
, emoteSetID(update["value"].toObject()["id"].toString())
|
||||||
|
, connectionIndex(connectionIndex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SeventvEventAPIUserConnectionUpdateDispatch::validate() const
|
||||||
|
{
|
||||||
|
return !this->userID.isEmpty() && !this->oldEmoteSetID.isEmpty() &&
|
||||||
|
!this->emoteSetID.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
72
src/providers/seventv/eventapi/SeventvEventAPIDispatch.hpp
Normal file
72
src/providers/seventv/eventapi/SeventvEventAPIDispatch.hpp
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#message-payload
|
||||||
|
struct SeventvEventAPIDispatch {
|
||||||
|
SeventvEventAPISubscriptionType type;
|
||||||
|
QJsonObject body;
|
||||||
|
QString id;
|
||||||
|
// it's okay for this to be empty
|
||||||
|
QString actorName;
|
||||||
|
|
||||||
|
SeventvEventAPIDispatch(QJsonObject obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SeventvEventAPIEmoteAddDispatch {
|
||||||
|
QString emoteSetID;
|
||||||
|
QString actorName;
|
||||||
|
QJsonObject emoteJson;
|
||||||
|
QString emoteID;
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteAddDispatch(const SeventvEventAPIDispatch &dispatch,
|
||||||
|
QJsonObject emote);
|
||||||
|
|
||||||
|
bool validate() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SeventvEventAPIEmoteRemoveDispatch {
|
||||||
|
QString emoteSetID;
|
||||||
|
QString actorName;
|
||||||
|
QString emoteName;
|
||||||
|
QString emoteID;
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteRemoveDispatch(const SeventvEventAPIDispatch &dispatch,
|
||||||
|
QJsonObject emote);
|
||||||
|
|
||||||
|
bool validate() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SeventvEventAPIEmoteUpdateDispatch {
|
||||||
|
QString emoteSetID;
|
||||||
|
QString actorName;
|
||||||
|
QString emoteID;
|
||||||
|
QString oldEmoteName;
|
||||||
|
QString emoteName;
|
||||||
|
|
||||||
|
SeventvEventAPIEmoteUpdateDispatch(const SeventvEventAPIDispatch &dispatch,
|
||||||
|
QJsonObject oldValue, QJsonObject value);
|
||||||
|
|
||||||
|
bool validate() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SeventvEventAPIUserConnectionUpdateDispatch {
|
||||||
|
QString userID;
|
||||||
|
QString actorName;
|
||||||
|
QString oldEmoteSetID;
|
||||||
|
QString emoteSetID;
|
||||||
|
size_t connectionIndex;
|
||||||
|
|
||||||
|
SeventvEventAPIUserConnectionUpdateDispatch(
|
||||||
|
const SeventvEventAPIDispatch &dispatch, const QJsonObject &update,
|
||||||
|
size_t connectionIndex);
|
||||||
|
|
||||||
|
bool validate() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
11
src/providers/seventv/eventapi/SeventvEventAPIMessage.cpp
Normal file
11
src/providers/seventv/eventapi/SeventvEventAPIMessage.cpp
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
SeventvEventAPIMessage::SeventvEventAPIMessage(QJsonObject _json)
|
||||||
|
: data(_json["d"].toObject())
|
||||||
|
, op(SeventvEventAPIOpcode(_json["op"].toInt()))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
44
src/providers/seventv/eventapi/SeventvEventAPIMessage.hpp
Normal file
44
src/providers/seventv/eventapi/SeventvEventAPIMessage.hpp
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
|
|
||||||
|
#include <boost/optional.hpp>
|
||||||
|
#include <magic_enum.hpp>
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct SeventvEventAPIMessage {
|
||||||
|
QJsonObject data;
|
||||||
|
|
||||||
|
SeventvEventAPIOpcode op;
|
||||||
|
|
||||||
|
SeventvEventAPIMessage(QJsonObject _json);
|
||||||
|
|
||||||
|
template <class InnerClass>
|
||||||
|
boost::optional<InnerClass> toInner();
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class InnerClass>
|
||||||
|
boost::optional<InnerClass> SeventvEventAPIMessage::toInner()
|
||||||
|
{
|
||||||
|
return InnerClass{this->data};
|
||||||
|
}
|
||||||
|
|
||||||
|
static boost::optional<SeventvEventAPIMessage> parseSeventvEventAPIBaseMessage(
|
||||||
|
const QString &blob)
|
||||||
|
{
|
||||||
|
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
|
||||||
|
|
||||||
|
if (jsonDoc.isNull())
|
||||||
|
{
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SeventvEventAPIMessage(jsonDoc.object());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -0,0 +1,77 @@
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
const char *typeToString(SeventvEventAPISubscriptionType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case SeventvEventAPISubscriptionType::UpdateEmoteSet:
|
||||||
|
return "emote_set.update";
|
||||||
|
case SeventvEventAPISubscriptionType::UpdateUser:
|
||||||
|
return "user.update";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject createDataJson(const char *typeName, const QString &condition)
|
||||||
|
{
|
||||||
|
QJsonObject data;
|
||||||
|
data["type"] = typeName;
|
||||||
|
{
|
||||||
|
QJsonObject conditionObj;
|
||||||
|
conditionObj["object_id"] = condition;
|
||||||
|
data["condition"] = conditionObj;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
bool SeventvEventAPISubscription::operator==(
|
||||||
|
const SeventvEventAPISubscription &rhs) const
|
||||||
|
{
|
||||||
|
return std::tie(this->condition, this->type) ==
|
||||||
|
std::tie(rhs.condition, rhs.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SeventvEventAPISubscription::operator!=(
|
||||||
|
const SeventvEventAPISubscription &rhs) const
|
||||||
|
{
|
||||||
|
return !(rhs == *this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray SeventvEventAPISubscription::encodeSubscribe() const
|
||||||
|
{
|
||||||
|
const auto *typeName = typeToString(this->type);
|
||||||
|
QJsonObject root;
|
||||||
|
root["op"] = (int)SeventvEventAPIOpcode::Subscribe;
|
||||||
|
root["d"] = createDataJson(typeName, this->condition);
|
||||||
|
return QJsonDocument(root).toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray SeventvEventAPISubscription::encodeUnsubscribe() const
|
||||||
|
{
|
||||||
|
const auto *typeName = typeToString(this->type);
|
||||||
|
QJsonObject root;
|
||||||
|
root["op"] = (int)SeventvEventAPIOpcode::Unsubscribe;
|
||||||
|
root["d"] = createDataJson(typeName, this->condition);
|
||||||
|
return QJsonDocument(root).toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDebug &operator<<(QDebug &dbg, const SeventvEventAPISubscription &subscription)
|
||||||
|
{
|
||||||
|
dbg << "SeventvEventAPISubscription{ condition:" << subscription.condition
|
||||||
|
<< "type:" << (int)subscription.type << '}';
|
||||||
|
return dbg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -0,0 +1,77 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <magic_enum.hpp>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types
|
||||||
|
enum class SeventvEventAPISubscriptionType {
|
||||||
|
UpdateEmoteSet,
|
||||||
|
UpdateUser,
|
||||||
|
|
||||||
|
INVALID,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#opcodes
|
||||||
|
enum class SeventvEventAPIOpcode {
|
||||||
|
Dispatch = 0,
|
||||||
|
Hello = 1,
|
||||||
|
Heartbeat = 2,
|
||||||
|
Reconnect = 4,
|
||||||
|
Ack = 5,
|
||||||
|
Error = 6,
|
||||||
|
EndOfStream = 7,
|
||||||
|
Identify = 33,
|
||||||
|
Resume = 34,
|
||||||
|
Subscribe = 35,
|
||||||
|
Unsubscribe = 36,
|
||||||
|
Signal = 37,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SeventvEventAPISubscription {
|
||||||
|
bool operator==(const SeventvEventAPISubscription &rhs) const;
|
||||||
|
bool operator!=(const SeventvEventAPISubscription &rhs) const;
|
||||||
|
QString condition;
|
||||||
|
SeventvEventAPISubscriptionType type;
|
||||||
|
|
||||||
|
QByteArray encodeSubscribe() const;
|
||||||
|
QByteArray encodeUnsubscribe() const;
|
||||||
|
|
||||||
|
friend QDebug &operator<<(QDebug &dbg,
|
||||||
|
const SeventvEventAPISubscription &subscription);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
template <>
|
||||||
|
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
|
||||||
|
chatterino::SeventvEventAPISubscriptionType>(
|
||||||
|
chatterino::SeventvEventAPISubscriptionType value) noexcept
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case chatterino::SeventvEventAPISubscriptionType::UpdateEmoteSet:
|
||||||
|
return "emote_set.update";
|
||||||
|
case chatterino::SeventvEventAPISubscriptionType::UpdateUser:
|
||||||
|
return "user.update";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return default_tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct hash<chatterino::SeventvEventAPISubscription> {
|
||||||
|
size_t operator()(const chatterino::SeventvEventAPISubscription &sub) const
|
||||||
|
{
|
||||||
|
return (size_t)qHash(sub.condition, qHash((int)sub.type));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace std
|
|
@ -11,6 +11,7 @@
|
||||||
#include "providers/bttv/BttvEmotes.hpp"
|
#include "providers/bttv/BttvEmotes.hpp"
|
||||||
#include "providers/bttv/LoadBttvChannelEmote.hpp"
|
#include "providers/bttv/LoadBttvChannelEmote.hpp"
|
||||||
#include "providers/seventv/SeventvEmotes.hpp"
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||||
#include "providers/twitch/PubSubManager.hpp"
|
#include "providers/twitch/PubSubManager.hpp"
|
||||||
#include "providers/twitch/TwitchCommon.hpp"
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
|
@ -104,6 +105,11 @@ TwitchChannel::TwitchChannel(const QString &name)
|
||||||
this->loadRecentMessagesReconnect();
|
this->loadRecentMessagesReconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this->destroyed.connect([this]() {
|
||||||
|
getApp()->twitch->dropSeventvChannel(this->seventvUserID_,
|
||||||
|
this->seventvEmoteSetID_);
|
||||||
|
});
|
||||||
|
|
||||||
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
|
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
|
||||||
if (msg->replyThread)
|
if (msg->replyThread)
|
||||||
{
|
{
|
||||||
|
@ -237,11 +243,16 @@ void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
|
||||||
|
|
||||||
SeventvEmotes::loadChannelEmotes(
|
SeventvEmotes::loadChannelEmotes(
|
||||||
weakOf<Channel>(this), this->roomId(),
|
weakOf<Channel>(this), this->roomId(),
|
||||||
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
|
[this, weak = weakOf<Channel>(this)](auto &&emoteMap,
|
||||||
|
auto channelInfo) {
|
||||||
if (auto shared = weak.lock())
|
if (auto shared = weak.lock())
|
||||||
{
|
{
|
||||||
this->seventvEmotes_.set(std::make_shared<EmoteMap>(
|
this->seventvEmotes_.set(std::make_shared<EmoteMap>(
|
||||||
std::forward<decltype(emoteMap)>(emoteMap)));
|
std::forward<decltype(emoteMap)>(emoteMap)));
|
||||||
|
this->updateSeventvData(channelInfo.userID,
|
||||||
|
channelInfo.emoteSetID);
|
||||||
|
this->seventvUserTwitchConnectionIndex_ =
|
||||||
|
channelInfo.twitchConnectionIndex;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
manualRefresh);
|
manualRefresh);
|
||||||
|
@ -589,6 +600,203 @@ std::shared_ptr<const EmoteMap> TwitchChannel::seventvEmotes() const
|
||||||
return this->seventvEmotes_.get();
|
return this->seventvEmotes_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString &TwitchChannel::seventvUserID() const
|
||||||
|
{
|
||||||
|
return this->seventvUserID_;
|
||||||
|
}
|
||||||
|
const QString &TwitchChannel::seventvEmoteSetID() const
|
||||||
|
{
|
||||||
|
return this->seventvEmoteSetID_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::addSeventvEmote(
|
||||||
|
const SeventvEventAPIEmoteAddDispatch &dispatch)
|
||||||
|
{
|
||||||
|
if (!SeventvEmotes::addEmote(this->seventvEmotes_, dispatch))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->addOrReplaceLiveUpdatesAddRemove(
|
||||||
|
true, "7TV", dispatch.actorName, dispatch.emoteJson["name"].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::updateSeventvEmote(
|
||||||
|
const SeventvEventAPIEmoteUpdateDispatch &dispatch)
|
||||||
|
{
|
||||||
|
if (!SeventvEmotes::updateEmote(this->seventvEmotes_, dispatch))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto builder =
|
||||||
|
MessageBuilder(liveUpdatesUpdateEmoteMessage, "7TV", dispatch.actorName,
|
||||||
|
dispatch.emoteName, dispatch.oldEmoteName);
|
||||||
|
this->addMessage(builder.release());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::removeSeventvEmote(
|
||||||
|
const SeventvEventAPIEmoteRemoveDispatch &dispatch)
|
||||||
|
{
|
||||||
|
auto removed = SeventvEmotes::removeEmote(this->seventvEmotes_, dispatch);
|
||||||
|
if (!removed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->addOrReplaceLiveUpdatesAddRemove(false, "7TV", dispatch.actorName,
|
||||||
|
removed.get()->name.string);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::updateSeventvUser(
|
||||||
|
const SeventvEventAPIUserConnectionUpdateDispatch &dispatch)
|
||||||
|
{
|
||||||
|
if (dispatch.connectionIndex != this->seventvUserTwitchConnectionIndex_)
|
||||||
|
{
|
||||||
|
// A different connection was updated
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSeventvData(this->seventvUserID_, dispatch.emoteSetID);
|
||||||
|
SeventvEmotes::getEmoteSet(
|
||||||
|
dispatch.emoteSetID,
|
||||||
|
[this, weak = weakOf<Channel>(this), dispatch](auto &&emotes,
|
||||||
|
const auto &name) {
|
||||||
|
postToThread([this, weak, dispatch, emotes, name]() {
|
||||||
|
if (auto shared = weak.lock())
|
||||||
|
{
|
||||||
|
this->seventvEmotes_.set(
|
||||||
|
std::make_shared<EmoteMap>(emotes));
|
||||||
|
auto builder =
|
||||||
|
MessageBuilder(liveUpdatesUpdateEmoteSetMessage, "7TV",
|
||||||
|
dispatch.actorName, name);
|
||||||
|
this->addMessage(builder.release());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[this, weak = weakOf<Channel>(this)](const auto &reason) {
|
||||||
|
postToThread([this, weak, reason]() {
|
||||||
|
if (auto shared = weak.lock())
|
||||||
|
{
|
||||||
|
this->seventvEmotes_.set(EMPTY_EMOTE_MAP);
|
||||||
|
this->addMessage(makeSystemMessage(
|
||||||
|
QString("Failed updating 7TV emote set (%1).")
|
||||||
|
.arg(reason)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::updateSeventvData(const QString &newUserID,
|
||||||
|
const QString &newEmoteSetID)
|
||||||
|
{
|
||||||
|
if (this->seventvUserID_ == newUserID &&
|
||||||
|
this->seventvEmoteSetID_ == newEmoteSetID)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<QString> oldUserID = boost::make_optional(
|
||||||
|
!this->seventvUserID_.isEmpty() && this->seventvUserID_ != newUserID,
|
||||||
|
this->seventvUserID_);
|
||||||
|
boost::optional<QString> oldEmoteSetID =
|
||||||
|
boost::make_optional(!this->seventvEmoteSetID_.isEmpty() &&
|
||||||
|
this->seventvEmoteSetID_ != newEmoteSetID,
|
||||||
|
this->seventvEmoteSetID_);
|
||||||
|
|
||||||
|
this->seventvUserID_ = newUserID;
|
||||||
|
this->seventvEmoteSetID_ = newEmoteSetID;
|
||||||
|
runInGuiThread([this, oldUserID, oldEmoteSetID]() {
|
||||||
|
if (getApp()->twitch->seventvEventAPI)
|
||||||
|
{
|
||||||
|
getApp()->twitch->seventvEventAPI->subscribeUser(
|
||||||
|
this->seventvUserID_, this->seventvEmoteSetID_);
|
||||||
|
|
||||||
|
if (oldUserID || oldEmoteSetID)
|
||||||
|
{
|
||||||
|
getApp()->twitch->dropSeventvChannel(
|
||||||
|
oldUserID.get_value_or(QString()),
|
||||||
|
oldEmoteSetID.get_value_or(QString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::addOrReplaceLiveUpdatesAddRemove(bool isEmoteAdd,
|
||||||
|
const QString &platform,
|
||||||
|
const QString &actor,
|
||||||
|
const QString &emoteName)
|
||||||
|
{
|
||||||
|
if (this->tryReplaceLastLiveUpdateAddOrRemove(
|
||||||
|
isEmoteAdd ? MessageFlag::LiveUpdatesAdd
|
||||||
|
: MessageFlag::LiveUpdatesRemove,
|
||||||
|
platform, actor, emoteName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->lastLiveUpdateEmoteNames_ = {emoteName};
|
||||||
|
|
||||||
|
MessagePtr msg;
|
||||||
|
if (isEmoteAdd)
|
||||||
|
{
|
||||||
|
msg = MessageBuilder(liveUpdatesAddEmoteMessage, platform, actor,
|
||||||
|
this->lastLiveUpdateEmoteNames_)
|
||||||
|
.release();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msg = MessageBuilder(liveUpdatesRemoveEmoteMessage, platform, actor,
|
||||||
|
this->lastLiveUpdateEmoteNames_)
|
||||||
|
.release();
|
||||||
|
}
|
||||||
|
this->lastLiveUpdateEmotePlatform_ = platform;
|
||||||
|
this->lastLiveUpdateMessage_ = msg;
|
||||||
|
this->lastLiveUpdateEmoteActor_ = actor;
|
||||||
|
this->addMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TwitchChannel::tryReplaceLastLiveUpdateAddOrRemove(
|
||||||
|
MessageFlag op, const QString &platform, const QString &actor,
|
||||||
|
const QString &emoteName)
|
||||||
|
{
|
||||||
|
if (this->lastLiveUpdateEmotePlatform_ != platform)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto last = this->lastLiveUpdateMessage_.lock();
|
||||||
|
if (!last || !last->flags.has(op) ||
|
||||||
|
last->parseTime < QTime::currentTime().addSecs(-5) ||
|
||||||
|
last->loginName != actor)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Update the message
|
||||||
|
this->lastLiveUpdateEmoteNames_.push_back(emoteName);
|
||||||
|
|
||||||
|
MessageBuilder replacement;
|
||||||
|
if (op == MessageFlag::LiveUpdatesAdd)
|
||||||
|
{
|
||||||
|
replacement =
|
||||||
|
MessageBuilder(liveUpdatesAddEmoteMessage, platform,
|
||||||
|
last->loginName, this->lastLiveUpdateEmoteNames_);
|
||||||
|
}
|
||||||
|
else // op == RemoveEmoteMessage
|
||||||
|
{
|
||||||
|
replacement =
|
||||||
|
MessageBuilder(liveUpdatesRemoveEmoteMessage, platform,
|
||||||
|
last->loginName, this->lastLiveUpdateEmoteNames_);
|
||||||
|
}
|
||||||
|
|
||||||
|
replacement->flags = last->flags;
|
||||||
|
|
||||||
|
auto msg = replacement.release();
|
||||||
|
this->lastLiveUpdateMessage_ = msg;
|
||||||
|
this->replaceMessage(last, msg);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const QString &TwitchChannel::subscriptionUrl()
|
const QString &TwitchChannel::subscriptionUrl()
|
||||||
{
|
{
|
||||||
return this->subscriptionUrl_;
|
return this->subscriptionUrl_;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include "common/Outcome.hpp"
|
#include "common/Outcome.hpp"
|
||||||
#include "common/UniqueAccess.hpp"
|
#include "common/UniqueAccess.hpp"
|
||||||
#include "messages/MessageThread.hpp"
|
#include "messages/MessageThread.hpp"
|
||||||
|
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||||
#include "providers/twitch/ChannelPointReward.hpp"
|
#include "providers/twitch/ChannelPointReward.hpp"
|
||||||
#include "providers/twitch/TwitchEmotes.hpp"
|
#include "providers/twitch/TwitchEmotes.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
|
@ -119,6 +120,23 @@ public:
|
||||||
virtual void refreshFFZChannelEmotes(bool manualRefresh);
|
virtual void refreshFFZChannelEmotes(bool manualRefresh);
|
||||||
virtual void refreshSevenTVChannelEmotes(bool manualRefresh);
|
virtual void refreshSevenTVChannelEmotes(bool manualRefresh);
|
||||||
|
|
||||||
|
const QString &seventvUserID() const;
|
||||||
|
const QString &seventvEmoteSetID() const;
|
||||||
|
|
||||||
|
/** Adds a 7TV channel emote to this channel. */
|
||||||
|
void addSeventvEmote(const SeventvEventAPIEmoteAddDispatch &dispatch);
|
||||||
|
/** Updates a 7TV channel emote's name in this channel */
|
||||||
|
void updateSeventvEmote(const SeventvEventAPIEmoteUpdateDispatch &dispatch);
|
||||||
|
/** Removes a 7TV channel emote from this channel */
|
||||||
|
void removeSeventvEmote(const SeventvEventAPIEmoteRemoveDispatch &dispatch);
|
||||||
|
/** Updates the current 7TV user. Currently, only the emote-set is updated. */
|
||||||
|
void updateSeventvUser(
|
||||||
|
const SeventvEventAPIUserConnectionUpdateDispatch &dispatch);
|
||||||
|
|
||||||
|
// Update the channel's 7TV information (the channel's 7TV user ID and emote set ID)
|
||||||
|
void updateSeventvData(const QString &newUserID,
|
||||||
|
const QString &newEmoteSetID);
|
||||||
|
|
||||||
// Badges
|
// Badges
|
||||||
boost::optional<EmotePtr> ffzCustomModBadge() const;
|
boost::optional<EmotePtr> ffzCustomModBadge() const;
|
||||||
boost::optional<EmotePtr> ffzCustomVipBadge() const;
|
boost::optional<EmotePtr> ffzCustomVipBadge() const;
|
||||||
|
@ -187,6 +205,41 @@ private:
|
||||||
|
|
||||||
QString prepareMessage(const QString &message) const;
|
QString prepareMessage(const QString &message) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Either adds a message mentioning the updated emotes
|
||||||
|
* or replaces an existing message. For criteria on existing messages,
|
||||||
|
* see `tryReplaceLastLiveUpdateAddOrRemove`.
|
||||||
|
*
|
||||||
|
* @param isEmoteAdd true if the emote was added, false if it was removed.
|
||||||
|
* @param platform The platform the emote was updated on ("7TV", "BTTV", "FFZ")
|
||||||
|
* @param actor The actor performing the update (possibly empty)
|
||||||
|
* @param emoteName The emote's name
|
||||||
|
*/
|
||||||
|
void addOrReplaceLiveUpdatesAddRemove(bool isEmoteAdd,
|
||||||
|
const QString &platform,
|
||||||
|
const QString &actor,
|
||||||
|
const QString &emoteName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to replace the last emote update message.
|
||||||
|
*
|
||||||
|
* A last message is valid if:
|
||||||
|
* * The actors match
|
||||||
|
* * The operations match
|
||||||
|
* * The platform matches
|
||||||
|
* * The last message isn't older than 5s
|
||||||
|
*
|
||||||
|
* @param op The emote operation (LiveUpdatesAdd or LiveUpdatesRemove)
|
||||||
|
* @param platform The emote platform ("7TV", "BTTV", "FFZ")
|
||||||
|
* @param actor The actor performing the action (possibly empty)
|
||||||
|
* @param emoteName The updated emote's name
|
||||||
|
* @return true, if the last message was replaced
|
||||||
|
*/
|
||||||
|
bool tryReplaceLastLiveUpdateAddOrRemove(MessageFlag op,
|
||||||
|
const QString &platform,
|
||||||
|
const QString &actor,
|
||||||
|
const QString &emoteName);
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
const QString subscriptionUrl_;
|
const QString subscriptionUrl_;
|
||||||
const QString channelUrl_;
|
const QString channelUrl_;
|
||||||
|
@ -225,6 +278,31 @@ private:
|
||||||
QElapsedTimer clipCreationTimer_;
|
QElapsedTimer clipCreationTimer_;
|
||||||
bool isClipCreationInProgress{false};
|
bool isClipCreationInProgress{false};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This channels 7TV user-id,
|
||||||
|
* empty if this channel isn't connected with 7TV.
|
||||||
|
*/
|
||||||
|
QString seventvUserID_;
|
||||||
|
/**
|
||||||
|
* This channels current 7TV emote-set-id,
|
||||||
|
* empty if this channel isn't connected with 7TV
|
||||||
|
*/
|
||||||
|
QString seventvEmoteSetID_;
|
||||||
|
/**
|
||||||
|
* The index of the twitch connection in
|
||||||
|
* 7TV's user representation.
|
||||||
|
*/
|
||||||
|
size_t seventvUserTwitchConnectionIndex_;
|
||||||
|
|
||||||
|
/** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */
|
||||||
|
QString lastLiveUpdateEmotePlatform_;
|
||||||
|
/** The actor name of the last live emote update. */
|
||||||
|
QString lastLiveUpdateEmoteActor_;
|
||||||
|
/** A weak reference to the last live emote update message. */
|
||||||
|
std::weak_ptr<const Message> lastLiveUpdateMessage_;
|
||||||
|
/** A list of the emotes listed in the lat live emote update message. */
|
||||||
|
std::vector<QString> lastLiveUpdateEmoteNames_;
|
||||||
|
|
||||||
pajlada::Signals::SignalHolder signalHolder_;
|
pajlada::Signals::SignalHolder signalHolder_;
|
||||||
std::vector<boost::signals2::scoped_connection> bSignals_;
|
std::vector<boost::signals2::scoped_connection> bSignals_;
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,13 @@
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||||
#include "providers/twitch/PubSubManager.hpp"
|
#include "providers/twitch/PubSubManager.hpp"
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchHelpers.hpp"
|
#include "providers/twitch/TwitchHelpers.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
#include "util/PostToThread.hpp"
|
#include "util/PostToThread.hpp"
|
||||||
|
|
||||||
|
@ -25,6 +27,12 @@ using namespace std::chrono_literals;
|
||||||
|
|
||||||
#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv"
|
#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
TwitchIrcServer::TwitchIrcServer()
|
TwitchIrcServer::TwitchIrcServer()
|
||||||
|
@ -36,6 +44,12 @@ TwitchIrcServer::TwitchIrcServer()
|
||||||
this->initializeIrc();
|
this->initializeIrc();
|
||||||
|
|
||||||
this->pubsub = new PubSub(TWITCH_PUBSUB_URL);
|
this->pubsub = new PubSub(TWITCH_PUBSUB_URL);
|
||||||
|
if (getSettings()->enableSevenTVEventAPI &&
|
||||||
|
getSettings()->enableSevenTVChannelEmotes)
|
||||||
|
{
|
||||||
|
this->seventvEventAPI =
|
||||||
|
std::make_unique<SeventvEventAPI>(SEVENTV_EVENTAPI_URL);
|
||||||
|
}
|
||||||
|
|
||||||
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
|
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
|
||||||
// this->connect(); },
|
// this->connect(); },
|
||||||
|
@ -517,4 +531,78 @@ void TwitchIrcServer::reloadAllSevenTVChannelEmotes()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchIrcServer::forEachSeventvEmoteSet(
|
||||||
|
const QString &emoteSetId, std::function<void(TwitchChannel &)> func)
|
||||||
|
{
|
||||||
|
this->forEachChannel([emoteSetId, func](const auto &chan) {
|
||||||
|
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get());
|
||||||
|
channel->seventvEmoteSetID() == emoteSetId)
|
||||||
|
{
|
||||||
|
func(*channel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void TwitchIrcServer::forEachSeventvUser(
|
||||||
|
const QString &userId, std::function<void(TwitchChannel &)> func)
|
||||||
|
{
|
||||||
|
this->forEachChannel([userId, func](const auto &chan) {
|
||||||
|
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get());
|
||||||
|
channel->seventvUserID() == userId)
|
||||||
|
{
|
||||||
|
func(*channel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchIrcServer::dropSeventvChannel(const QString &userID,
|
||||||
|
const QString &emoteSetID)
|
||||||
|
{
|
||||||
|
if (!this->seventvEventAPI)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||||
|
|
||||||
|
// ignore empty values
|
||||||
|
bool skipUser = userID.isEmpty();
|
||||||
|
bool skipSet = emoteSetID.isEmpty();
|
||||||
|
|
||||||
|
bool foundUser = skipUser;
|
||||||
|
bool foundSet = skipSet;
|
||||||
|
for (std::weak_ptr<Channel> &weak : this->channels)
|
||||||
|
{
|
||||||
|
ChannelPtr chan = weak.lock();
|
||||||
|
if (!chan)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *channel = dynamic_cast<TwitchChannel *>(chan.get());
|
||||||
|
if (!foundSet && channel->seventvEmoteSetID() == emoteSetID)
|
||||||
|
{
|
||||||
|
foundSet = true;
|
||||||
|
}
|
||||||
|
if (!foundUser && channel->seventvUserID() == userID)
|
||||||
|
{
|
||||||
|
foundUser = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundSet && foundUser)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundUser)
|
||||||
|
{
|
||||||
|
this->seventvEventAPI->unsubscribeUser(userID);
|
||||||
|
}
|
||||||
|
if (!foundSet)
|
||||||
|
{
|
||||||
|
this->seventvEventAPI->unsubscribeEmoteSet(emoteSetID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Settings;
|
||||||
class Paths;
|
class Paths;
|
||||||
class PubSub;
|
class PubSub;
|
||||||
class TwitchChannel;
|
class TwitchChannel;
|
||||||
|
class SeventvEventAPI;
|
||||||
|
|
||||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||||
{
|
{
|
||||||
|
@ -41,6 +42,21 @@ public:
|
||||||
void reloadSevenTVGlobalEmotes();
|
void reloadSevenTVGlobalEmotes();
|
||||||
void reloadAllSevenTVChannelEmotes();
|
void reloadAllSevenTVChannelEmotes();
|
||||||
|
|
||||||
|
/** Calls `func` with all twitch channels that have `emoteSetId` added. */
|
||||||
|
void forEachSeventvEmoteSet(const QString &emoteSetId,
|
||||||
|
std::function<void(TwitchChannel &)> func);
|
||||||
|
/** Calls `func` with all twitch channels where the seventv-user-id is `userId`. */
|
||||||
|
void forEachSeventvUser(const QString &userId,
|
||||||
|
std::function<void(TwitchChannel &)> func);
|
||||||
|
/**
|
||||||
|
* Checks if any channel still needs this `userID` or `emoteSetID`.
|
||||||
|
* If not, it unsubscribes from the respective messages.
|
||||||
|
*
|
||||||
|
* It's currently not possible to share emote sets among users,
|
||||||
|
* but it's a commonly requested feature.
|
||||||
|
*/
|
||||||
|
void dropSeventvChannel(const QString &userID, const QString &emoteSetID);
|
||||||
|
|
||||||
Atomic<QString> lastUserThatWhisperedMe;
|
Atomic<QString> lastUserThatWhisperedMe;
|
||||||
|
|
||||||
const ChannelPtr whispersChannel;
|
const ChannelPtr whispersChannel;
|
||||||
|
@ -49,6 +65,7 @@ public:
|
||||||
IndirectChannel watchingChannel;
|
IndirectChannel watchingChannel;
|
||||||
|
|
||||||
PubSub *pubsub;
|
PubSub *pubsub;
|
||||||
|
std::unique_ptr<SeventvEventAPI> seventvEventAPI;
|
||||||
|
|
||||||
const BttvEmotes &getBttvEmotes() const;
|
const BttvEmotes &getBttvEmotes() const;
|
||||||
const FfzEmotes &getFfzEmotes() const;
|
const FfzEmotes &getFfzEmotes() const;
|
||||||
|
|
|
@ -224,6 +224,7 @@ public:
|
||||||
BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true};
|
BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true};
|
||||||
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
|
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
|
||||||
BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true};
|
BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true};
|
||||||
|
BoolSetting enableSevenTVEventAPI = {"/emotes/seventv/eventapi", true};
|
||||||
|
|
||||||
/// Links
|
/// Links
|
||||||
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
|
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
|
||||||
|
|
|
@ -30,19 +30,6 @@ private:
|
||||||
std::function<void()> action_;
|
std::function<void()> action_;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename F>
|
|
||||||
static void runInGuiThread(F &&fun)
|
|
||||||
{
|
|
||||||
if (isGuiThread())
|
|
||||||
{
|
|
||||||
fun();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
postToThread(fun);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from
|
// Taken from
|
||||||
// https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style
|
// https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style
|
||||||
// Qt 5/4 - preferred, has least allocations
|
// Qt 5/4 - preferred, has least allocations
|
||||||
|
@ -70,4 +57,17 @@ static void postToThread(F &&fun, QObject *obj = qApp)
|
||||||
QCoreApplication::postEvent(obj, new Event(std::forward<F>(fun)));
|
QCoreApplication::postEvent(obj, new Event(std::forward<F>(fun)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
static void runInGuiThread(F &&fun)
|
||||||
|
{
|
||||||
|
if (isGuiThread())
|
||||||
|
{
|
||||||
|
fun();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
postToThread(fun);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -387,6 +387,8 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
||||||
layout.addCheckbox("Show FFZ channel emotes", s.enableFFZChannelEmotes);
|
layout.addCheckbox("Show FFZ channel emotes", s.enableFFZChannelEmotes);
|
||||||
layout.addCheckbox("Show 7TV global emotes", s.enableSevenTVGlobalEmotes);
|
layout.addCheckbox("Show 7TV global emotes", s.enableSevenTVGlobalEmotes);
|
||||||
layout.addCheckbox("Show 7TV channel emotes", s.enableSevenTVChannelEmotes);
|
layout.addCheckbox("Show 7TV channel emotes", s.enableSevenTVChannelEmotes);
|
||||||
|
layout.addCheckbox("Enable 7TV live emote updates (requires restart)",
|
||||||
|
s.enableSevenTVEventAPI);
|
||||||
|
|
||||||
layout.addTitle("Streamer Mode");
|
layout.addTitle("Streamer Mode");
|
||||||
layout.addDescription(
|
layout.addDescription(
|
||||||
|
|
|
@ -21,6 +21,7 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/FormatTime.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/FormatTime.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/LimitedQueue.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/LimitedQueue.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/BasicPubSub.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/BasicPubSub.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/SeventvEventAPI.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
103
tests/src/SeventvEventAPI.cpp
Normal file
103
tests/src/SeventvEventAPI.cpp
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QString>
|
||||||
|
#include <boost/optional.hpp>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
const QString EMOTE_SET_A = "60b39e943e203cc169dfc106";
|
||||||
|
const QString EMOTE_SET_B = "60bca831e7ecd2f892c9b9ab";
|
||||||
|
const QString TARGET_USER_ID = "60b39e943e203cc169dfc106";
|
||||||
|
|
||||||
|
TEST(SeventvEventAPI, AllEvents)
|
||||||
|
{
|
||||||
|
const QString host("wss://127.0.0.1:9050/liveupdates/seventv/all-events");
|
||||||
|
auto *eventAPI = new SeventvEventAPI(host, std::chrono::milliseconds(1000));
|
||||||
|
eventAPI->start();
|
||||||
|
|
||||||
|
boost::optional<SeventvEventAPIEmoteAddDispatch> addDispatch;
|
||||||
|
boost::optional<SeventvEventAPIEmoteUpdateDispatch> updateDispatch;
|
||||||
|
boost::optional<SeventvEventAPIEmoteRemoveDispatch> removeDispatch;
|
||||||
|
boost::optional<SeventvEventAPIUserConnectionUpdateDispatch> userDispatch;
|
||||||
|
|
||||||
|
eventAPI->signals_.emoteAdded.connect([&](const auto &d) {
|
||||||
|
addDispatch = d;
|
||||||
|
});
|
||||||
|
eventAPI->signals_.emoteUpdated.connect([&](const auto &d) {
|
||||||
|
updateDispatch = d;
|
||||||
|
});
|
||||||
|
eventAPI->signals_.emoteRemoved.connect([&](const auto &d) {
|
||||||
|
removeDispatch = d;
|
||||||
|
});
|
||||||
|
eventAPI->signals_.userUpdated.connect([&](const auto &d) {
|
||||||
|
userDispatch = d;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
eventAPI->subscribeUser("", EMOTE_SET_A);
|
||||||
|
std::this_thread::sleep_for(500ms);
|
||||||
|
|
||||||
|
ASSERT_EQ(eventAPI->diag.connectionsOpened, 1);
|
||||||
|
ASSERT_EQ(eventAPI->diag.connectionsClosed, 0);
|
||||||
|
ASSERT_EQ(eventAPI->diag.connectionsFailed, 0);
|
||||||
|
|
||||||
|
auto add = *addDispatch;
|
||||||
|
ASSERT_EQ(add.emoteSetID, EMOTE_SET_A);
|
||||||
|
ASSERT_EQ(add.actorName, QString("nerixyz"));
|
||||||
|
ASSERT_EQ(add.emoteID, QString("621d13967cc2d4e1953838ed"));
|
||||||
|
|
||||||
|
auto upd = *updateDispatch;
|
||||||
|
ASSERT_EQ(upd.emoteSetID, EMOTE_SET_A);
|
||||||
|
ASSERT_EQ(upd.actorName, QString("nerixyz"));
|
||||||
|
ASSERT_EQ(upd.emoteID, QString("621d13967cc2d4e1953838ed"));
|
||||||
|
ASSERT_EQ(upd.oldEmoteName, QString("Chatterinoge"));
|
||||||
|
ASSERT_EQ(upd.emoteName, QString("Chatterino"));
|
||||||
|
|
||||||
|
auto rem = *removeDispatch;
|
||||||
|
ASSERT_EQ(rem.emoteSetID, EMOTE_SET_A);
|
||||||
|
ASSERT_EQ(rem.actorName, QString("nerixyz"));
|
||||||
|
ASSERT_EQ(rem.emoteName, QString("Chatterino"));
|
||||||
|
ASSERT_EQ(rem.emoteID, QString("621d13967cc2d4e1953838ed"));
|
||||||
|
|
||||||
|
ASSERT_EQ(userDispatch.has_value(), false);
|
||||||
|
addDispatch = boost::none;
|
||||||
|
updateDispatch = boost::none;
|
||||||
|
removeDispatch = boost::none;
|
||||||
|
|
||||||
|
eventAPI->subscribeUser(TARGET_USER_ID, "");
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
|
||||||
|
ASSERT_EQ(addDispatch.has_value(), false);
|
||||||
|
ASSERT_EQ(updateDispatch.has_value(), false);
|
||||||
|
ASSERT_EQ(removeDispatch.has_value(), false);
|
||||||
|
|
||||||
|
auto user = *userDispatch;
|
||||||
|
ASSERT_EQ(user.userID, TARGET_USER_ID);
|
||||||
|
ASSERT_EQ(user.actorName, QString("nerixyz"));
|
||||||
|
ASSERT_EQ(user.oldEmoteSetID, EMOTE_SET_A);
|
||||||
|
ASSERT_EQ(user.emoteSetID, EMOTE_SET_B);
|
||||||
|
ASSERT_EQ(user.connectionIndex, 0);
|
||||||
|
|
||||||
|
eventAPI->stop();
|
||||||
|
ASSERT_EQ(eventAPI->diag.connectionsOpened, 1);
|
||||||
|
ASSERT_EQ(eventAPI->diag.connectionsClosed, 1);
|
||||||
|
ASSERT_EQ(eventAPI->diag.connectionsFailed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SeventvEventAPI, NoHeartbeat)
|
||||||
|
{
|
||||||
|
const QString host("wss://127.0.0.1:9050/liveupdates/seventv/no-heartbeat");
|
||||||
|
auto *eventApi = new SeventvEventAPI(host, std::chrono::milliseconds(1000));
|
||||||
|
eventApi->start();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
eventApi->subscribeUser("", EMOTE_SET_A);
|
||||||
|
std::this_thread::sleep_for(1250ms);
|
||||||
|
ASSERT_EQ(eventApi->diag.connectionsOpened, 2);
|
||||||
|
ASSERT_EQ(eventApi->diag.connectionsClosed, 1);
|
||||||
|
ASSERT_EQ(eventApi->diag.connectionsFailed, 0);
|
||||||
|
|
||||||
|
eventApi->stop();
|
||||||
|
}
|
Loading…
Reference in a new issue