feat: Add Live Emote Updates for BTTV (#4147)
This feature is enabled by default and can be disabled in settings with the "Enable BTTV live emotes updates" setting. Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -6,7 +6,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.5
|
TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:master
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: test-${{ github.ref }}
|
group: test-${{ github.ref }}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
|
- Major: Added live emote updates for BTTV (#4147)
|
||||||
- Minor: Change the highlight order to prioritize Message highlights over User highlights. (#4303)
|
- Minor: Change the highlight order to prioritize Message highlights over User highlights. (#4303)
|
||||||
- Minor: Added ability to negate search options by prefixing it with an exclamation mark (e.g. `!badge:mod` to search for messages where the author does not have the moderator badge). (#4207)
|
- Minor: Added ability to negate search options by prefixing it with an exclamation mark (e.g. `!badge:mod` to search for messages where the author does not have the moderator badge). (#4207)
|
||||||
- Minor: Search window input will automatically use currently selected text if present. (#4178)
|
- Minor: Search window input will automatically use currently selected text if present. (#4178)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "debug/AssertInGuiThread.hpp"
|
#include "debug/AssertInGuiThread.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
|
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||||
#include "providers/ffz/FfzBadges.hpp"
|
#include "providers/ffz/FfzBadges.hpp"
|
||||||
#include "providers/irc/Irc2.hpp"
|
#include "providers/irc/Irc2.hpp"
|
||||||
@@ -156,6 +157,7 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||||||
}
|
}
|
||||||
this->initPubSub();
|
this->initPubSub();
|
||||||
|
|
||||||
|
this->initBttvLiveUpdates();
|
||||||
this->initSeventvEventAPI();
|
this->initSeventvEventAPI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,6 +578,51 @@ void Application::initPubSub()
|
|||||||
RequestModerationActions();
|
RequestModerationActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::initBttvLiveUpdates()
|
||||||
|
{
|
||||||
|
if (!this->twitch->bttvLiveUpdates)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoBttv)
|
||||||
|
<< "Skipping initialization of Live Updates as it's disabled";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->twitch->bttvLiveUpdates->signals_.emoteAdded.connect(
|
||||||
|
[&](const auto &data) {
|
||||||
|
auto chan = this->twitch->getChannelOrEmptyByID(data.channelID);
|
||||||
|
|
||||||
|
postToThread([chan, data] {
|
||||||
|
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
|
||||||
|
{
|
||||||
|
channel->addBttvEmote(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this->twitch->bttvLiveUpdates->signals_.emoteUpdated.connect(
|
||||||
|
[&](const auto &data) {
|
||||||
|
auto chan = this->twitch->getChannelOrEmptyByID(data.channelID);
|
||||||
|
|
||||||
|
postToThread([chan, data] {
|
||||||
|
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
|
||||||
|
{
|
||||||
|
channel->updateBttvEmote(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this->twitch->bttvLiveUpdates->signals_.emoteRemoved.connect(
|
||||||
|
[&](const auto &data) {
|
||||||
|
auto chan = this->twitch->getChannelOrEmptyByID(data.channelID);
|
||||||
|
|
||||||
|
postToThread([chan, data] {
|
||||||
|
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
|
||||||
|
{
|
||||||
|
channel->removeBttvEmote(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this->twitch->bttvLiveUpdates->start();
|
||||||
|
}
|
||||||
|
|
||||||
void Application::initSeventvEventAPI()
|
void Application::initSeventvEventAPI()
|
||||||
{
|
{
|
||||||
if (!this->twitch->seventvEventAPI)
|
if (!this->twitch->seventvEventAPI)
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void addSingleton(Singleton *singleton);
|
void addSingleton(Singleton *singleton);
|
||||||
void initPubSub();
|
void initPubSub();
|
||||||
|
void initBttvLiveUpdates();
|
||||||
void initSeventvEventAPI();
|
void initSeventvEventAPI();
|
||||||
void initNm(Paths &paths);
|
void initNm(Paths &paths);
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,13 @@ set(SOURCE_FILES
|
|||||||
|
|
||||||
providers/bttv/BttvEmotes.cpp
|
providers/bttv/BttvEmotes.cpp
|
||||||
providers/bttv/BttvEmotes.hpp
|
providers/bttv/BttvEmotes.hpp
|
||||||
|
providers/bttv/BttvLiveUpdates.cpp
|
||||||
|
providers/bttv/BttvLiveUpdates.hpp
|
||||||
|
|
||||||
|
providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp
|
||||||
|
providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp
|
||||||
|
providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp
|
||||||
|
providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp
|
||||||
|
|
||||||
providers/chatterino/ChatterinoBadges.cpp
|
providers/chatterino/ChatterinoBadges.cpp
|
||||||
providers/chatterino/ChatterinoBadges.hpp
|
providers/chatterino/ChatterinoBadges.hpp
|
||||||
|
|||||||
@@ -593,17 +593,37 @@ MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteMessageTag /*unused*/,
|
|||||||
const QString &oldEmoteName)
|
const QString &oldEmoteName)
|
||||||
: MessageBuilder()
|
: MessageBuilder()
|
||||||
{
|
{
|
||||||
auto text = QString("renamed %1 emote %2 to %3.")
|
QString text;
|
||||||
.arg(platform, oldEmoteName, emoteName);
|
if (actor.isEmpty())
|
||||||
|
{
|
||||||
|
text = "Renamed";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text = "renamed";
|
||||||
|
}
|
||||||
|
text +=
|
||||||
|
QString(" %1 emote %2 to %3.").arg(platform, oldEmoteName, emoteName);
|
||||||
|
|
||||||
this->emplace<TimestampElement>();
|
this->emplace<TimestampElement>();
|
||||||
this->emplace<TextElement>(actor, MessageElementFlag::Username,
|
if (!actor.isEmpty())
|
||||||
MessageColor::System)
|
{
|
||||||
->setLink({Link::UserInfo, actor});
|
this->emplace<TextElement>(actor, MessageElementFlag::Username,
|
||||||
|
MessageColor::System)
|
||||||
|
->setLink({Link::UserInfo, actor});
|
||||||
|
}
|
||||||
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||||
MessageColor::System);
|
MessageColor::System);
|
||||||
|
|
||||||
auto finalText = QString("%1 %2").arg(actor, text);
|
QString finalText;
|
||||||
|
if (actor.isEmpty())
|
||||||
|
{
|
||||||
|
finalText = text;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalText = QString("%1 %2").arg(actor, text);
|
||||||
|
}
|
||||||
|
|
||||||
this->message().loginName = actor;
|
this->message().loginName = actor;
|
||||||
this->message().messageText = finalText;
|
this->message().messageText = finalText;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "messages/Image.hpp"
|
#include "messages/Image.hpp"
|
||||||
#include "messages/ImageSet.hpp"
|
#include "messages/ImageSet.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
|
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
@@ -21,6 +22,12 @@ namespace {
|
|||||||
|
|
||||||
QString emoteLinkFormat("https://betterttv.com/emotes/%1");
|
QString emoteLinkFormat("https://betterttv.com/emotes/%1");
|
||||||
|
|
||||||
|
struct CreateEmoteResult {
|
||||||
|
EmoteId id;
|
||||||
|
EmoteName name;
|
||||||
|
Emote emote;
|
||||||
|
};
|
||||||
|
|
||||||
Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
||||||
const QString &emoteScale)
|
const QString &emoteScale)
|
||||||
{
|
{
|
||||||
@@ -70,6 +77,70 @@ namespace {
|
|||||||
|
|
||||||
return {Success, std::move(emotes)};
|
return {Success, std::move(emotes)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateEmoteResult createChannelEmote(const QString &channelDisplayName,
|
||||||
|
const QJsonObject &jsonEmote)
|
||||||
|
{
|
||||||
|
auto id = EmoteId{jsonEmote.value("id").toString()};
|
||||||
|
auto name = EmoteName{jsonEmote.value("code").toString()};
|
||||||
|
auto author = EmoteAuthor{
|
||||||
|
jsonEmote.value("user").toObject().value("displayName").toString()};
|
||||||
|
|
||||||
|
auto emote = Emote({
|
||||||
|
name,
|
||||||
|
ImageSet{
|
||||||
|
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
||||||
|
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||||
|
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
||||||
|
},
|
||||||
|
Tooltip{
|
||||||
|
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||||
|
.arg(name.string)
|
||||||
|
// when author is empty, it is a channel emote created by the broadcaster
|
||||||
|
.arg(author.string.isEmpty() ? "Channel" : "Shared")
|
||||||
|
.arg(author.string.isEmpty() ? channelDisplayName
|
||||||
|
: author.string)},
|
||||||
|
Url{emoteLinkFormat.arg(id.string)},
|
||||||
|
false,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {id, name, emote};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool updateChannelEmote(Emote &emote, const QString &channelDisplayName,
|
||||||
|
const QJsonObject &jsonEmote)
|
||||||
|
{
|
||||||
|
bool anyModifications = false;
|
||||||
|
|
||||||
|
if (jsonEmote.contains("code"))
|
||||||
|
{
|
||||||
|
emote.name = EmoteName{jsonEmote.value("code").toString()};
|
||||||
|
anyModifications = true;
|
||||||
|
}
|
||||||
|
if (jsonEmote.contains("user"))
|
||||||
|
{
|
||||||
|
emote.author = EmoteAuthor{jsonEmote.value("user")
|
||||||
|
.toObject()
|
||||||
|
.value("displayName")
|
||||||
|
.toString()};
|
||||||
|
anyModifications = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyModifications)
|
||||||
|
{
|
||||||
|
emote.tooltip = Tooltip{
|
||||||
|
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||||
|
.arg(emote.name.string)
|
||||||
|
// when author is empty, it is a channel emote created by the broadcaster
|
||||||
|
.arg(emote.author.string.isEmpty() ? "Channel" : "Shared")
|
||||||
|
.arg(emote.author.string.isEmpty() ? channelDisplayName
|
||||||
|
: emote.author.string)};
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyModifications;
|
||||||
|
}
|
||||||
|
|
||||||
std::pair<Outcome, EmoteMap> parseChannelEmotes(
|
std::pair<Outcome, EmoteMap> parseChannelEmotes(
|
||||||
const QJsonObject &jsonRoot, const QString &channelDisplayName)
|
const QJsonObject &jsonRoot, const QString &channelDisplayName)
|
||||||
{
|
{
|
||||||
@@ -80,33 +151,11 @@ namespace {
|
|||||||
auto jsonEmotes = jsonRoot.value(key).toArray();
|
auto jsonEmotes = jsonRoot.value(key).toArray();
|
||||||
for (auto jsonEmote_ : jsonEmotes)
|
for (auto jsonEmote_ : jsonEmotes)
|
||||||
{
|
{
|
||||||
auto jsonEmote = jsonEmote_.toObject();
|
auto emote = createChannelEmote(channelDisplayName,
|
||||||
|
jsonEmote_.toObject());
|
||||||
|
|
||||||
auto id = EmoteId{jsonEmote.value("id").toString()};
|
emotes[emote.name] =
|
||||||
auto name = EmoteName{jsonEmote.value("code").toString()};
|
cachedOrMake(std::move(emote.emote), emote.id);
|
||||||
auto author = EmoteAuthor{jsonEmote.value("user")
|
|
||||||
.toObject()
|
|
||||||
.value("displayName")
|
|
||||||
.toString()};
|
|
||||||
|
|
||||||
auto emote = Emote({
|
|
||||||
name,
|
|
||||||
ImageSet{
|
|
||||||
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
|
||||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
|
||||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
|
||||||
},
|
|
||||||
Tooltip{
|
|
||||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
|
||||||
.arg(name.string)
|
|
||||||
// when author is empty, it is a channel emote created by the broadcaster
|
|
||||||
.arg(author.string.isEmpty() ? "Channel" : "Shared")
|
|
||||||
.arg(author.string.isEmpty() ? channelDisplayName
|
|
||||||
: author.string)},
|
|
||||||
Url{emoteLinkFormat.arg(id.string)},
|
|
||||||
});
|
|
||||||
|
|
||||||
emotes[name] = cachedOrMake(std::move(emote), id);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -227,6 +276,78 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmotePtr BttvEmotes::addEmote(
|
||||||
|
const QString &channelDisplayName,
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||||
|
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||||
|
{
|
||||||
|
// This copies the map.
|
||||||
|
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||||
|
auto result = createChannelEmote(channelDisplayName, message.jsonEmote);
|
||||||
|
|
||||||
|
auto emote = std::make_shared<const Emote>(std::move(result.emote));
|
||||||
|
updatedMap[result.name] = emote;
|
||||||
|
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||||
|
|
||||||
|
return emote;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<std::pair<EmotePtr, EmotePtr>> BttvEmotes::updateEmote(
|
||||||
|
const QString &channelDisplayName,
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||||
|
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||||
|
{
|
||||||
|
// This copies the map.
|
||||||
|
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||||
|
|
||||||
|
// Step 1: remove the existing emote
|
||||||
|
auto it = updatedMap.findEmote(QString(), message.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 oldEmotePtr = it->second;
|
||||||
|
// copy the existing emote, to not change the original one
|
||||||
|
auto emote = *oldEmotePtr;
|
||||||
|
updatedMap.erase(it);
|
||||||
|
|
||||||
|
// Step 2: update the emote
|
||||||
|
if (!updateChannelEmote(emote, channelDisplayName, message.jsonEmote))
|
||||||
|
{
|
||||||
|
// The emote wasn't actually updated
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto name = emote.name;
|
||||||
|
auto emotePtr = std::make_shared<const Emote>(std::move(emote));
|
||||||
|
updatedMap[name] = emotePtr;
|
||||||
|
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||||
|
|
||||||
|
return std::make_pair(oldEmotePtr, emotePtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<EmotePtr> BttvEmotes::removeEmote(
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||||
|
const BttvLiveUpdateEmoteRemoveMessage &message)
|
||||||
|
{
|
||||||
|
// This copies the map.
|
||||||
|
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||||
|
auto it = updatedMap.findEmote(QString(), message.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);
|
||||||
|
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||||
|
|
||||||
|
return emote;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
static Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
static Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
||||||
const QString &emoteScale)
|
const QString &emoteScale)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ struct Emote;
|
|||||||
using EmotePtr = std::shared_ptr<const Emote>;
|
using EmotePtr = std::shared_ptr<const Emote>;
|
||||||
class EmoteMap;
|
class EmoteMap;
|
||||||
class Channel;
|
class Channel;
|
||||||
|
struct BttvLiveUpdateEmoteUpdateAddMessage;
|
||||||
|
struct BttvLiveUpdateEmoteRemoveMessage;
|
||||||
|
|
||||||
class BttvEmotes final
|
class BttvEmotes final
|
||||||
{
|
{
|
||||||
@@ -33,6 +35,41 @@ public:
|
|||||||
std::function<void(EmoteMap &&)> callback,
|
std::function<void(EmoteMap &&)> callback,
|
||||||
bool manualRefresh);
|
bool manualRefresh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an emote to the `channelEmoteMap`.
|
||||||
|
* This will _copy_ the emote map and
|
||||||
|
* update the `Atomic`.
|
||||||
|
*
|
||||||
|
* @return The added emote.
|
||||||
|
*/
|
||||||
|
static EmotePtr addEmote(
|
||||||
|
const QString &channelDisplayName,
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||||
|
const BttvLiveUpdateEmoteUpdateAddMessage &message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an emote in this `channelEmoteMap`.
|
||||||
|
* This will _copy_ the emote map and
|
||||||
|
* update the `Atomic`.
|
||||||
|
*
|
||||||
|
* @return pair<old emote, new emote> if any emote was updated.
|
||||||
|
*/
|
||||||
|
static boost::optional<std::pair<EmotePtr, EmotePtr>> updateEmote(
|
||||||
|
const QString &channelDisplayName,
|
||||||
|
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||||
|
const BttvLiveUpdateEmoteUpdateAddMessage &message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an emote from this `channelEmoteMap`.
|
||||||
|
* 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>> &channelEmoteMap,
|
||||||
|
const BttvLiveUpdateEmoteRemoveMessage &message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
BttvLiveUpdates::BttvLiveUpdates(QString host)
|
||||||
|
: BasicPubSubManager(std::move(host))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void BttvLiveUpdates::joinChannel(const QString &channelID,
|
||||||
|
const QString &userName)
|
||||||
|
{
|
||||||
|
if (this->joinedChannels_.insert(channelID).second)
|
||||||
|
{
|
||||||
|
this->subscribe({BttvLiveUpdateSubscriptionChannel{channelID}});
|
||||||
|
this->subscribe({BttvLiveUpdateBroadcastMe{.twitchID = channelID,
|
||||||
|
.userName = userName}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BttvLiveUpdates::partChannel(const QString &id)
|
||||||
|
{
|
||||||
|
if (this->joinedChannels_.erase(id) > 0)
|
||||||
|
{
|
||||||
|
this->unsubscribe({BttvLiveUpdateSubscriptionChannel{id}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BttvLiveUpdates::onMessage(
|
||||||
|
websocketpp::connection_hdl /*hdl*/,
|
||||||
|
BasicPubSubManager<BttvLiveUpdateSubscription>::WebsocketMessagePtr msg)
|
||||||
|
{
|
||||||
|
const auto &payload = QString::fromStdString(msg->get_payload());
|
||||||
|
QJsonDocument jsonDoc(QJsonDocument::fromJson(payload.toUtf8()));
|
||||||
|
|
||||||
|
if (jsonDoc.isNull())
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoBttv) << "Failed to parse live update JSON";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto json = jsonDoc.object();
|
||||||
|
|
||||||
|
auto eventType = json["name"].toString();
|
||||||
|
auto eventData = json["data"].toObject();
|
||||||
|
|
||||||
|
if (eventType == "emote_create")
|
||||||
|
{
|
||||||
|
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||||
|
|
||||||
|
if (!message.validate())
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoBttv) << "Invalid add message" << json;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->signals_.emoteAdded.invoke(message);
|
||||||
|
}
|
||||||
|
else if (eventType == "emote_update")
|
||||||
|
{
|
||||||
|
auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData);
|
||||||
|
|
||||||
|
if (!message.validate())
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoBttv) << "Invalid update message" << json;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->signals_.emoteUpdated.invoke(message);
|
||||||
|
}
|
||||||
|
else if (eventType == "emote_delete")
|
||||||
|
{
|
||||||
|
auto message = BttvLiveUpdateEmoteRemoveMessage(eventData);
|
||||||
|
|
||||||
|
if (!message.validate())
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoBttv) << "Invalid deletion message" << json;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->signals_.emoteRemoved.invoke(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoBttv) << "Unhandled event:" << json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||||
|
#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp"
|
||||||
|
#include "providers/liveupdates/BasicPubSubManager.hpp"
|
||||||
|
#include "util/QStringHash.hpp"
|
||||||
|
|
||||||
|
#include <pajlada/signals/signal.hpp>
|
||||||
|
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class BttvLiveUpdates : public BasicPubSubManager<BttvLiveUpdateSubscription>
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
using Signal =
|
||||||
|
pajlada::Signals::Signal<T>; // type-id is vector<T, Alloc<T>>
|
||||||
|
|
||||||
|
public:
|
||||||
|
BttvLiveUpdates(QString host);
|
||||||
|
|
||||||
|
struct {
|
||||||
|
Signal<BttvLiveUpdateEmoteUpdateAddMessage> emoteAdded;
|
||||||
|
Signal<BttvLiveUpdateEmoteUpdateAddMessage> emoteUpdated;
|
||||||
|
Signal<BttvLiveUpdateEmoteRemoveMessage> emoteRemoved;
|
||||||
|
} signals_; // NOLINT(readability-identifier-naming)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins a Twitch channel by its id (without any prefix like 'twitch:')
|
||||||
|
* if it's not already joined.
|
||||||
|
*
|
||||||
|
* @param channelID the Twitch channel-id of the broadcaster.
|
||||||
|
* @param userName the Twitch username of the current user.
|
||||||
|
*/
|
||||||
|
void joinChannel(const QString &channelID, const QString &userName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parts a twitch channel by its id (without any prefix like 'twitch:')
|
||||||
|
* if it's joined.
|
||||||
|
*
|
||||||
|
* @param id the Twitch channel-id of the broadcaster.
|
||||||
|
*/
|
||||||
|
void partChannel(const QString &id);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onMessage(
|
||||||
|
websocketpp::connection_hdl hdl,
|
||||||
|
BasicPubSubManager<BttvLiveUpdateSubscription>::WebsocketMessagePtr msg)
|
||||||
|
override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Contains all joined Twitch channel-ids
|
||||||
|
std::unordered_set<QString> joinedChannels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool tryParseChannelId(QString &channelId)
|
||||||
|
{
|
||||||
|
if (!channelId.startsWith("twitch:"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelId.remove(0, 7); // "twitch:"
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
BttvLiveUpdateEmoteUpdateAddMessage::BttvLiveUpdateEmoteUpdateAddMessage(
|
||||||
|
const QJsonObject &json)
|
||||||
|
: channelID(json["channel"].toString())
|
||||||
|
, jsonEmote(json["emote"].toObject())
|
||||||
|
, emoteName(this->jsonEmote["code"].toString())
|
||||||
|
, emoteID(this->jsonEmote["id"].toString())
|
||||||
|
, badChannelID_(!tryParseChannelId(this->channelID))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BttvLiveUpdateEmoteUpdateAddMessage::validate() const
|
||||||
|
{
|
||||||
|
// We don't need to check for jsonEmote["code"]/["id"],
|
||||||
|
// because these are this->emoteID and this->emoteName.
|
||||||
|
return !this->badChannelID_ && !this->channelID.isEmpty() &&
|
||||||
|
!this->emoteID.isEmpty() && !this->emoteName.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
BttvLiveUpdateEmoteRemoveMessage::BttvLiveUpdateEmoteRemoveMessage(
|
||||||
|
const QJsonObject &json)
|
||||||
|
: channelID(json["channel"].toString())
|
||||||
|
, emoteID(json["emoteId"].toString())
|
||||||
|
, badChannelID_(!tryParseChannelId(this->channelID))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BttvLiveUpdateEmoteRemoveMessage::validate() const
|
||||||
|
{
|
||||||
|
return !this->badChannelID_ && !this->emoteID.isEmpty() &&
|
||||||
|
!this->channelID.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct BttvLiveUpdateEmoteUpdateAddMessage {
|
||||||
|
BttvLiveUpdateEmoteUpdateAddMessage(const QJsonObject &json);
|
||||||
|
|
||||||
|
QString channelID;
|
||||||
|
|
||||||
|
QJsonObject jsonEmote;
|
||||||
|
QString emoteName;
|
||||||
|
QString emoteID;
|
||||||
|
|
||||||
|
bool validate() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// true if the channel id is malformed
|
||||||
|
// (e.g. doesn't start with "twitch:")
|
||||||
|
bool badChannelID_;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BttvLiveUpdateEmoteRemoveMessage {
|
||||||
|
BttvLiveUpdateEmoteRemoveMessage(const QJsonObject &json);
|
||||||
|
|
||||||
|
QString channelID;
|
||||||
|
QString emoteID;
|
||||||
|
|
||||||
|
bool validate() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// true if the channel id is malformed
|
||||||
|
// (e.g. doesn't start with "twitch:")
|
||||||
|
bool badChannelID_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
QByteArray BttvLiveUpdateSubscription::encodeSubscribe() const
|
||||||
|
{
|
||||||
|
return QJsonDocument(std::visit(
|
||||||
|
[](const auto &d) {
|
||||||
|
return d.encode(true);
|
||||||
|
},
|
||||||
|
this->data))
|
||||||
|
.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray BttvLiveUpdateSubscription::encodeUnsubscribe() const
|
||||||
|
{
|
||||||
|
return QJsonDocument(std::visit(
|
||||||
|
[](const auto &d) {
|
||||||
|
return d.encode(false);
|
||||||
|
},
|
||||||
|
this->data))
|
||||||
|
.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateSubscription &subscription)
|
||||||
|
{
|
||||||
|
std::visit(
|
||||||
|
[&](const auto &data) {
|
||||||
|
dbg << data;
|
||||||
|
},
|
||||||
|
subscription.data);
|
||||||
|
return dbg;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject BttvLiveUpdateSubscriptionChannel::encode(bool isSubscribe) const
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
if (isSubscribe)
|
||||||
|
{
|
||||||
|
root["name"] = "join_channel";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
root["name"] = "part_channel";
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject data;
|
||||||
|
data["name"] = QString("twitch:%1").arg(this->twitchID);
|
||||||
|
|
||||||
|
root["data"] = data;
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BttvLiveUpdateSubscriptionChannel::operator==(
|
||||||
|
const BttvLiveUpdateSubscriptionChannel &rhs) const
|
||||||
|
{
|
||||||
|
return this->twitchID == rhs.twitchID;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BttvLiveUpdateSubscriptionChannel::operator!=(
|
||||||
|
const BttvLiveUpdateSubscriptionChannel &rhs) const
|
||||||
|
{
|
||||||
|
return !(*this == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateSubscriptionChannel &data)
|
||||||
|
{
|
||||||
|
dbg << "BttvLiveUpdateSubscriptionChannel{ twitchID:" << data.twitchID
|
||||||
|
<< '}';
|
||||||
|
return dbg;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject BttvLiveUpdateBroadcastMe::encode(bool /*isSubscribe*/) const
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
root["name"] = "broadcast_me";
|
||||||
|
|
||||||
|
QJsonObject data;
|
||||||
|
data["name"] = this->userName;
|
||||||
|
data["channel"] = QString("twitch:%1").arg(this->twitchID);
|
||||||
|
|
||||||
|
root["data"] = data;
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BttvLiveUpdateBroadcastMe::operator==(
|
||||||
|
const BttvLiveUpdateBroadcastMe &rhs) const
|
||||||
|
{
|
||||||
|
return this->twitchID == rhs.twitchID && this->userName == rhs.userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BttvLiveUpdateBroadcastMe::operator!=(
|
||||||
|
const BttvLiveUpdateBroadcastMe &rhs) const
|
||||||
|
{
|
||||||
|
return !(*this == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateBroadcastMe &data)
|
||||||
|
{
|
||||||
|
dbg << "BttvLiveUpdateBroadcastMe{ twitchID:" << data.twitchID
|
||||||
|
<< "userName:" << data.userName << '}';
|
||||||
|
return dbg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/functional/hash.hpp>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct BttvLiveUpdateSubscriptionChannel {
|
||||||
|
QString twitchID;
|
||||||
|
|
||||||
|
QJsonObject encode(bool isSubscribe) const;
|
||||||
|
bool operator==(const BttvLiveUpdateSubscriptionChannel &rhs) const;
|
||||||
|
bool operator!=(const BttvLiveUpdateSubscriptionChannel &rhs) const;
|
||||||
|
friend QDebug &operator<<(QDebug &dbg,
|
||||||
|
const BttvLiveUpdateSubscriptionChannel &data);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BttvLiveUpdateBroadcastMe {
|
||||||
|
QString twitchID;
|
||||||
|
QString userName;
|
||||||
|
|
||||||
|
QJsonObject encode(bool isSubscribe) const;
|
||||||
|
bool operator==(const BttvLiveUpdateBroadcastMe &rhs) const;
|
||||||
|
bool operator!=(const BttvLiveUpdateBroadcastMe &rhs) const;
|
||||||
|
friend QDebug &operator<<(QDebug &dbg,
|
||||||
|
const BttvLiveUpdateBroadcastMe &data);
|
||||||
|
};
|
||||||
|
|
||||||
|
using BttvLiveUpdateSubscriptionData =
|
||||||
|
std::variant<BttvLiveUpdateSubscriptionChannel, BttvLiveUpdateBroadcastMe>;
|
||||||
|
|
||||||
|
struct BttvLiveUpdateSubscription {
|
||||||
|
BttvLiveUpdateSubscriptionData data;
|
||||||
|
|
||||||
|
QByteArray encodeSubscribe() const;
|
||||||
|
QByteArray encodeUnsubscribe() const;
|
||||||
|
|
||||||
|
bool operator==(const BttvLiveUpdateSubscription &rhs) const
|
||||||
|
{
|
||||||
|
return this->data == rhs.data;
|
||||||
|
}
|
||||||
|
bool operator!=(const BttvLiveUpdateSubscription &rhs) const
|
||||||
|
{
|
||||||
|
return !(*this == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend QDebug &operator<<(QDebug &dbg,
|
||||||
|
const BttvLiveUpdateSubscription &subscription);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct hash<chatterino::BttvLiveUpdateSubscriptionChannel> {
|
||||||
|
size_t operator()(
|
||||||
|
const chatterino::BttvLiveUpdateSubscriptionChannel &data) const
|
||||||
|
{
|
||||||
|
return qHash(data.twitchID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct hash<chatterino::BttvLiveUpdateBroadcastMe> {
|
||||||
|
size_t operator()(const chatterino::BttvLiveUpdateBroadcastMe &data) const
|
||||||
|
{
|
||||||
|
size_t seed = 0;
|
||||||
|
boost::hash_combine(seed, qHash(data.twitchID));
|
||||||
|
boost::hash_combine(seed, qHash(data.userName));
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct hash<chatterino::BttvLiveUpdateSubscription> {
|
||||||
|
size_t operator()(const chatterino::BttvLiveUpdateSubscription &sub) const
|
||||||
|
{
|
||||||
|
return std::hash<chatterino::BttvLiveUpdateSubscriptionData>{}(
|
||||||
|
sub.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace std
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
#include "messages/MessageElement.hpp"
|
#include "messages/MessageElement.hpp"
|
||||||
#include "messages/MessageThread.hpp"
|
#include "messages/MessageThread.hpp"
|
||||||
#include "providers/bttv/BttvEmotes.hpp"
|
#include "providers/bttv/BttvEmotes.hpp"
|
||||||
|
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||||
|
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||||
#include "providers/RecentMessagesApi.hpp"
|
#include "providers/RecentMessagesApi.hpp"
|
||||||
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||||
#include "providers/seventv/SeventvEmotes.hpp"
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
@@ -103,6 +105,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||||||
this->refreshFFZChannelEmotes(false);
|
this->refreshFFZChannelEmotes(false);
|
||||||
this->refreshBTTVChannelEmotes(false);
|
this->refreshBTTVChannelEmotes(false);
|
||||||
this->refreshSevenTVChannelEmotes(false);
|
this->refreshSevenTVChannelEmotes(false);
|
||||||
|
this->joinBttvChannel();
|
||||||
});
|
});
|
||||||
|
|
||||||
this->connected.connect([this]() {
|
this->connected.connect([this]() {
|
||||||
@@ -121,6 +124,11 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||||||
this->destroyed.connect([this]() {
|
this->destroyed.connect([this]() {
|
||||||
getApp()->twitch->dropSeventvChannel(this->seventvUserID_,
|
getApp()->twitch->dropSeventvChannel(this->seventvUserID_,
|
||||||
this->seventvEmoteSetID_);
|
this->seventvEmoteSetID_);
|
||||||
|
|
||||||
|
if (getApp()->twitch->bttvLiveUpdates)
|
||||||
|
{
|
||||||
|
getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
|
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
|
||||||
@@ -622,6 +630,66 @@ const QString &TwitchChannel::seventvEmoteSetID() const
|
|||||||
return this->seventvEmoteSetID_;
|
return this->seventvEmoteSetID_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::joinBttvChannel() const
|
||||||
|
{
|
||||||
|
if (getApp()->twitch->bttvLiveUpdates)
|
||||||
|
{
|
||||||
|
const auto currentAccount = getApp()->accounts->twitch.getCurrent();
|
||||||
|
QString userName;
|
||||||
|
if (currentAccount && !currentAccount->isAnon())
|
||||||
|
{
|
||||||
|
userName = currentAccount->getUserName();
|
||||||
|
}
|
||||||
|
getApp()->twitch->bttvLiveUpdates->joinChannel(this->roomId(),
|
||||||
|
userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::addBttvEmote(
|
||||||
|
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||||
|
{
|
||||||
|
auto emote = BttvEmotes::addEmote(this->getDisplayName(), this->bttvEmotes_,
|
||||||
|
message);
|
||||||
|
|
||||||
|
this->addOrReplaceLiveUpdatesAddRemove(true, "BTTV", QString() /*actor*/,
|
||||||
|
emote->name.string);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::updateBttvEmote(
|
||||||
|
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||||
|
{
|
||||||
|
auto updated = BttvEmotes::updateEmote(this->getDisplayName(),
|
||||||
|
this->bttvEmotes_, message);
|
||||||
|
if (!updated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto [oldEmote, newEmote] = *updated;
|
||||||
|
if (oldEmote->name == newEmote->name)
|
||||||
|
{
|
||||||
|
return; // only the creator changed
|
||||||
|
}
|
||||||
|
|
||||||
|
auto builder = MessageBuilder(liveUpdatesUpdateEmoteMessage, "BTTV",
|
||||||
|
QString() /* actor */, newEmote->name.string,
|
||||||
|
oldEmote->name.string);
|
||||||
|
this->addMessage(builder.release());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::removeBttvEmote(
|
||||||
|
const BttvLiveUpdateEmoteRemoveMessage &message)
|
||||||
|
{
|
||||||
|
auto removed = BttvEmotes::removeEmote(this->bttvEmotes_, message);
|
||||||
|
if (!removed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->addOrReplaceLiveUpdatesAddRemove(false, "BTTV", QString() /*actor*/,
|
||||||
|
removed.get()->name.string);
|
||||||
|
}
|
||||||
|
|
||||||
void TwitchChannel::addSeventvEmote(
|
void TwitchChannel::addSeventvEmote(
|
||||||
const SeventvEventAPIEmoteAddDispatch &dispatch)
|
const SeventvEventAPIEmoteAddDispatch &dispatch)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class EmoteMap;
|
|||||||
class TwitchBadges;
|
class TwitchBadges;
|
||||||
class FfzEmotes;
|
class FfzEmotes;
|
||||||
class BttvEmotes;
|
class BttvEmotes;
|
||||||
|
struct BttvLiveUpdateEmoteUpdateAddMessage;
|
||||||
|
struct BttvLiveUpdateEmoteRemoveMessage;
|
||||||
class SeventvEmotes;
|
class SeventvEmotes;
|
||||||
struct SeventvEventAPIEmoteAddDispatch;
|
struct SeventvEventAPIEmoteAddDispatch;
|
||||||
struct SeventvEventAPIEmoteUpdateDispatch;
|
struct SeventvEventAPIEmoteUpdateDispatch;
|
||||||
@@ -139,6 +141,13 @@ public:
|
|||||||
const QString &seventvUserID() const;
|
const QString &seventvUserID() const;
|
||||||
const QString &seventvEmoteSetID() const;
|
const QString &seventvEmoteSetID() const;
|
||||||
|
|
||||||
|
/** Adds a BTTV channel emote to this channel. */
|
||||||
|
void addBttvEmote(const BttvLiveUpdateEmoteUpdateAddMessage &message);
|
||||||
|
/** Updates a BTTV channel emote in this channel. */
|
||||||
|
void updateBttvEmote(const BttvLiveUpdateEmoteUpdateAddMessage &message);
|
||||||
|
/** Removes a BTTV channel emote from this channel. */
|
||||||
|
void removeBttvEmote(const BttvLiveUpdateEmoteRemoveMessage &message);
|
||||||
|
|
||||||
/** Adds a 7TV channel emote to this channel. */
|
/** Adds a 7TV channel emote to this channel. */
|
||||||
void addSeventvEmote(const SeventvEventAPIEmoteAddDispatch &dispatch);
|
void addSeventvEmote(const SeventvEventAPIEmoteAddDispatch &dispatch);
|
||||||
/** Updates a 7TV channel emote's name in this channel */
|
/** Updates a 7TV channel emote's name in this channel */
|
||||||
@@ -206,6 +215,8 @@ private:
|
|||||||
void fetchDisplayName();
|
void fetchDisplayName();
|
||||||
void cleanUpReplyThreads();
|
void cleanUpReplyThreads();
|
||||||
void showLoginMessage();
|
void showLoginMessage();
|
||||||
|
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
|
||||||
|
void joinBttvChannel() const;
|
||||||
|
|
||||||
void setLive(bool newLiveStatus);
|
void setLive(bool newLiveStatus);
|
||||||
void setMod(bool value);
|
void setMod(bool value);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#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/bttv/BttvLiveUpdates.hpp"
|
||||||
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
@@ -30,6 +31,7 @@ using namespace std::chrono_literals;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws";
|
||||||
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
|
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -45,6 +47,14 @@ TwitchIrcServer::TwitchIrcServer()
|
|||||||
this->initializeIrc();
|
this->initializeIrc();
|
||||||
|
|
||||||
this->pubsub = new PubSub(TWITCH_PUBSUB_URL);
|
this->pubsub = new PubSub(TWITCH_PUBSUB_URL);
|
||||||
|
|
||||||
|
if (getSettings()->enableBTTVLiveUpdates &&
|
||||||
|
getSettings()->enableBTTVChannelEmotes)
|
||||||
|
{
|
||||||
|
this->bttvLiveUpdates =
|
||||||
|
std::make_unique<BttvLiveUpdates>(BTTV_LIVE_UPDATES_URL);
|
||||||
|
}
|
||||||
|
|
||||||
if (getSettings()->enableSevenTVEventAPI &&
|
if (getSettings()->enableSevenTVEventAPI &&
|
||||||
getSettings()->enableSevenTVChannelEmotes)
|
getSettings()->enableSevenTVChannelEmotes)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Settings;
|
|||||||
class Paths;
|
class Paths;
|
||||||
class PubSub;
|
class PubSub;
|
||||||
class TwitchChannel;
|
class TwitchChannel;
|
||||||
|
class BttvLiveUpdates;
|
||||||
class SeventvEventAPI;
|
class SeventvEventAPI;
|
||||||
|
|
||||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||||
@@ -66,6 +67,7 @@ public:
|
|||||||
IndirectChannel watchingChannel;
|
IndirectChannel watchingChannel;
|
||||||
|
|
||||||
PubSub *pubsub;
|
PubSub *pubsub;
|
||||||
|
std::unique_ptr<BttvLiveUpdates> bttvLiveUpdates;
|
||||||
std::unique_ptr<SeventvEventAPI> seventvEventAPI;
|
std::unique_ptr<SeventvEventAPI> seventvEventAPI;
|
||||||
|
|
||||||
const BttvEmotes &getBttvEmotes() const;
|
const BttvEmotes &getBttvEmotes() const;
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ public:
|
|||||||
|
|
||||||
BoolSetting enableBTTVGlobalEmotes = {"/emotes/bttv/global", true};
|
BoolSetting enableBTTVGlobalEmotes = {"/emotes/bttv/global", true};
|
||||||
BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true};
|
BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true};
|
||||||
|
BoolSetting enableBTTVLiveUpdates = {"/emotes/bttv/liveupdates", true};
|
||||||
BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true};
|
BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true};
|
||||||
BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true};
|
BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true};
|
||||||
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
|
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
|
||||||
|
|||||||
@@ -432,6 +432,8 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
|||||||
s.emojiSet);
|
s.emojiSet);
|
||||||
layout.addCheckbox("Show BTTV global emotes", s.enableBTTVGlobalEmotes);
|
layout.addCheckbox("Show BTTV global emotes", s.enableBTTVGlobalEmotes);
|
||||||
layout.addCheckbox("Show BTTV channel emotes", s.enableBTTVChannelEmotes);
|
layout.addCheckbox("Show BTTV channel emotes", s.enableBTTVChannelEmotes);
|
||||||
|
layout.addCheckbox("Enable BTTV live emote updates (requires restart)",
|
||||||
|
s.enableBTTVLiveUpdates);
|
||||||
layout.addCheckbox("Show FFZ global emotes", s.enableFFZGlobalEmotes);
|
layout.addCheckbox("Show FFZ global emotes", s.enableFFZGlobalEmotes);
|
||||||
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);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ set(test_SOURCES
|
|||||||
${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
|
${CMAKE_CURRENT_LIST_DIR}/src/SeventvEventAPI.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/BttvLiveUpdates.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||||
|
|
||||||
|
#include <boost/optional.hpp>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
const QString TARGET_USER_ID = "1234567";
|
||||||
|
const QString TARGET_USER_NAME = "Alien";
|
||||||
|
|
||||||
|
TEST(BttvLiveUpdates, AllEvents)
|
||||||
|
{
|
||||||
|
const QString host("wss://127.0.0.1:9050/liveupdates/bttv/all-events");
|
||||||
|
auto *liveUpdates = new BttvLiveUpdates(host);
|
||||||
|
liveUpdates->start();
|
||||||
|
|
||||||
|
boost::optional<BttvLiveUpdateEmoteUpdateAddMessage> addMessage;
|
||||||
|
boost::optional<BttvLiveUpdateEmoteUpdateAddMessage> updateMessage;
|
||||||
|
boost::optional<BttvLiveUpdateEmoteRemoveMessage> removeMessage;
|
||||||
|
|
||||||
|
liveUpdates->signals_.emoteAdded.connect([&](const auto &m) {
|
||||||
|
addMessage = m;
|
||||||
|
});
|
||||||
|
liveUpdates->signals_.emoteUpdated.connect([&](const auto &m) {
|
||||||
|
updateMessage = m;
|
||||||
|
});
|
||||||
|
liveUpdates->signals_.emoteRemoved.connect([&](const auto &m) {
|
||||||
|
removeMessage = m;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
liveUpdates->joinChannel(TARGET_USER_ID, TARGET_USER_NAME);
|
||||||
|
std::this_thread::sleep_for(500ms);
|
||||||
|
|
||||||
|
ASSERT_EQ(liveUpdates->diag.connectionsOpened, 1);
|
||||||
|
ASSERT_EQ(liveUpdates->diag.connectionsClosed, 0);
|
||||||
|
ASSERT_EQ(liveUpdates->diag.connectionsFailed, 0);
|
||||||
|
|
||||||
|
auto add = *addMessage;
|
||||||
|
ASSERT_EQ(add.channelID, TARGET_USER_ID);
|
||||||
|
ASSERT_EQ(add.emoteName, QString("PepePls"));
|
||||||
|
ASSERT_EQ(add.emoteID, QString("55898e122612142e6aaa935b"));
|
||||||
|
|
||||||
|
auto update = *updateMessage;
|
||||||
|
ASSERT_EQ(update.channelID, TARGET_USER_ID);
|
||||||
|
ASSERT_EQ(update.emoteName, QString("PepePls"));
|
||||||
|
ASSERT_EQ(update.emoteID, QString("55898e122612142e6aaa935b"));
|
||||||
|
|
||||||
|
auto rem = *removeMessage;
|
||||||
|
ASSERT_EQ(rem.channelID, TARGET_USER_ID);
|
||||||
|
ASSERT_EQ(rem.emoteID, QString("55898e122612142e6aaa935b"));
|
||||||
|
|
||||||
|
liveUpdates->stop();
|
||||||
|
ASSERT_EQ(liveUpdates->diag.connectionsOpened, 1);
|
||||||
|
ASSERT_EQ(liveUpdates->diag.connectionsClosed, 1);
|
||||||
|
ASSERT_EQ(liveUpdates->diag.connectionsFailed, 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user