mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
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:
parent
56f7c91a64
commit
904749cf62
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -6,7 +6,7 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
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:
|
||||
group: test-${{ github.ref }}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Unversioned
|
||||
|
||||
- Major: Added live emote updates for BTTV (#4147)
|
||||
- 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: Search window input will automatically use currently selected text if present. (#4178)
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||
#include "providers/ffz/FfzBadges.hpp"
|
||||
#include "providers/irc/Irc2.hpp"
|
||||
|
@ -156,6 +157,7 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||
}
|
||||
this->initPubSub();
|
||||
|
||||
this->initBttvLiveUpdates();
|
||||
this->initSeventvEventAPI();
|
||||
}
|
||||
|
||||
|
@ -576,6 +578,51 @@ void Application::initPubSub()
|
|||
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()
|
||||
{
|
||||
if (!this->twitch->seventvEventAPI)
|
||||
|
|
|
@ -149,6 +149,7 @@ public:
|
|||
private:
|
||||
void addSingleton(Singleton *singleton);
|
||||
void initPubSub();
|
||||
void initBttvLiveUpdates();
|
||||
void initSeventvEventAPI();
|
||||
void initNm(Paths &paths);
|
||||
|
||||
|
|
|
@ -197,6 +197,13 @@ set(SOURCE_FILES
|
|||
|
||||
providers/bttv/BttvEmotes.cpp
|
||||
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.hpp
|
||||
|
|
|
@ -593,17 +593,37 @@ MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteMessageTag /*unused*/,
|
|||
const QString &oldEmoteName)
|
||||
: MessageBuilder()
|
||||
{
|
||||
auto text = QString("renamed %1 emote %2 to %3.")
|
||||
.arg(platform, oldEmoteName, emoteName);
|
||||
QString text;
|
||||
if (actor.isEmpty())
|
||||
{
|
||||
text = "Renamed";
|
||||
}
|
||||
else
|
||||
{
|
||||
text = "renamed";
|
||||
}
|
||||
text +=
|
||||
QString(" %1 emote %2 to %3.").arg(platform, oldEmoteName, emoteName);
|
||||
|
||||
this->emplace<TimestampElement>();
|
||||
this->emplace<TextElement>(actor, MessageElementFlag::Username,
|
||||
MessageColor::System)
|
||||
->setLink({Link::UserInfo, actor});
|
||||
if (!actor.isEmpty())
|
||||
{
|
||||
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);
|
||||
QString finalText;
|
||||
if (actor.isEmpty())
|
||||
{
|
||||
finalText = text;
|
||||
}
|
||||
else
|
||||
{
|
||||
finalText = QString("%1 %2").arg(actor, text);
|
||||
}
|
||||
|
||||
this->message().loginName = actor;
|
||||
this->message().messageText = finalText;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "messages/Image.hpp"
|
||||
#include "messages/ImageSet.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
|
@ -21,6 +22,12 @@ namespace {
|
|||
|
||||
QString emoteLinkFormat("https://betterttv.com/emotes/%1");
|
||||
|
||||
struct CreateEmoteResult {
|
||||
EmoteId id;
|
||||
EmoteName name;
|
||||
Emote emote;
|
||||
};
|
||||
|
||||
Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
||||
const QString &emoteScale)
|
||||
{
|
||||
|
@ -70,6 +77,70 @@ namespace {
|
|||
|
||||
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(
|
||||
const QJsonObject &jsonRoot, const QString &channelDisplayName)
|
||||
{
|
||||
|
@ -80,33 +151,11 @@ namespace {
|
|||
auto jsonEmotes = jsonRoot.value(key).toArray();
|
||||
for (auto jsonEmote_ : jsonEmotes)
|
||||
{
|
||||
auto jsonEmote = jsonEmote_.toObject();
|
||||
auto emote = createChannelEmote(channelDisplayName,
|
||||
jsonEmote_.toObject());
|
||||
|
||||
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)},
|
||||
});
|
||||
|
||||
emotes[name] = cachedOrMake(std::move(emote), id);
|
||||
emotes[emote.name] =
|
||||
cachedOrMake(std::move(emote.emote), emote.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -227,6 +276,78 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
|
|||
.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,
|
||||
const QString &emoteScale)
|
||||
|
|
|
@ -13,6 +13,8 @@ struct Emote;
|
|||
using EmotePtr = std::shared_ptr<const Emote>;
|
||||
class EmoteMap;
|
||||
class Channel;
|
||||
struct BttvLiveUpdateEmoteUpdateAddMessage;
|
||||
struct BttvLiveUpdateEmoteRemoveMessage;
|
||||
|
||||
class BttvEmotes final
|
||||
{
|
||||
|
@ -33,6 +35,41 @@ public:
|
|||
std::function<void(EmoteMap &&)> callback,
|
||||
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:
|
||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||
};
|
||||
|
|
92
src/providers/bttv/BttvLiveUpdates.cpp
Normal file
92
src/providers/bttv/BttvLiveUpdates.cpp
Normal file
|
@ -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
|
57
src/providers/bttv/BttvLiveUpdates.hpp
Normal file
57
src/providers/bttv/BttvLiveUpdates.hpp
Normal file
|
@ -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
|
52
src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp
Normal file
52
src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp
Normal file
|
@ -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
|
38
src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp
Normal file
38
src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp
Normal file
|
@ -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
|
107
src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp
Normal file
107
src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp
Normal file
|
@ -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/MessageThread.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/RecentMessagesApi.hpp"
|
||||
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||
#include "providers/seventv/SeventvEmotes.hpp"
|
||||
|
@ -103,6 +105,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
this->refreshFFZChannelEmotes(false);
|
||||
this->refreshBTTVChannelEmotes(false);
|
||||
this->refreshSevenTVChannelEmotes(false);
|
||||
this->joinBttvChannel();
|
||||
});
|
||||
|
||||
this->connected.connect([this]() {
|
||||
|
@ -121,6 +124,11 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
this->destroyed.connect([this]() {
|
||||
getApp()->twitch->dropSeventvChannel(this->seventvUserID_,
|
||||
this->seventvEmoteSetID_);
|
||||
|
||||
if (getApp()->twitch->bttvLiveUpdates)
|
||||
{
|
||||
getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId());
|
||||
}
|
||||
});
|
||||
|
||||
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
|
||||
|
@ -622,6 +630,66 @@ const QString &TwitchChannel::seventvEmoteSetID() const
|
|||
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(
|
||||
const SeventvEventAPIEmoteAddDispatch &dispatch)
|
||||
{
|
||||
|
|
|
@ -47,6 +47,8 @@ class EmoteMap;
|
|||
class TwitchBadges;
|
||||
class FfzEmotes;
|
||||
class BttvEmotes;
|
||||
struct BttvLiveUpdateEmoteUpdateAddMessage;
|
||||
struct BttvLiveUpdateEmoteRemoveMessage;
|
||||
class SeventvEmotes;
|
||||
struct SeventvEventAPIEmoteAddDispatch;
|
||||
struct SeventvEventAPIEmoteUpdateDispatch;
|
||||
|
@ -139,6 +141,13 @@ public:
|
|||
const QString &seventvUserID() 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. */
|
||||
void addSeventvEmote(const SeventvEventAPIEmoteAddDispatch &dispatch);
|
||||
/** Updates a 7TV channel emote's name in this channel */
|
||||
|
@ -206,6 +215,8 @@ private:
|
|||
void fetchDisplayName();
|
||||
void cleanUpReplyThreads();
|
||||
void showLoginMessage();
|
||||
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
|
||||
void joinBttvChannel() const;
|
||||
|
||||
void setLive(bool newLiveStatus);
|
||||
void setMod(bool value);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
|
@ -30,6 +31,7 @@ using namespace std::chrono_literals;
|
|||
|
||||
namespace {
|
||||
|
||||
const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws";
|
||||
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
|
||||
|
||||
} // namespace
|
||||
|
@ -45,6 +47,14 @@ TwitchIrcServer::TwitchIrcServer()
|
|||
this->initializeIrc();
|
||||
|
||||
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 &&
|
||||
getSettings()->enableSevenTVChannelEmotes)
|
||||
{
|
||||
|
|
|
@ -20,6 +20,7 @@ class Settings;
|
|||
class Paths;
|
||||
class PubSub;
|
||||
class TwitchChannel;
|
||||
class BttvLiveUpdates;
|
||||
class SeventvEventAPI;
|
||||
|
||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||
|
@ -66,6 +67,7 @@ public:
|
|||
IndirectChannel watchingChannel;
|
||||
|
||||
PubSub *pubsub;
|
||||
std::unique_ptr<BttvLiveUpdates> bttvLiveUpdates;
|
||||
std::unique_ptr<SeventvEventAPI> seventvEventAPI;
|
||||
|
||||
const BttvEmotes &getBttvEmotes() const;
|
||||
|
|
|
@ -225,6 +225,7 @@ public:
|
|||
|
||||
BoolSetting enableBTTVGlobalEmotes = {"/emotes/bttv/global", true};
|
||||
BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true};
|
||||
BoolSetting enableBTTVLiveUpdates = {"/emotes/bttv/liveupdates", true};
|
||||
BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true};
|
||||
BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true};
|
||||
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
|
||||
|
|
|
@ -432,6 +432,8 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
|||
s.emojiSet);
|
||||
layout.addCheckbox("Show BTTV global emotes", s.enableBTTVGlobalEmotes);
|
||||
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 channel emotes", s.enableFFZChannelEmotes);
|
||||
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/BasicPubSub.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/SeventvEventAPI.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/BttvLiveUpdates.cpp
|
||||
# Add your new file above this line!
|
||||
)
|
||||
|
||||
|
|
59
tests/src/BttvLiveUpdates.cpp
Normal file
59
tests/src/BttvLiveUpdates.cpp
Normal file
|
@ -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);
|
||||
}
|
Loading…
Reference in a new issue