feat: Live Emote Updates for 7TV (#4090)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/bttv/LoadBttvChannelEmote.hpp"
|
||||
#include "providers/seventv/SeventvEmotes.hpp"
|
||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/PubSubManager.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
@@ -104,6 +105,11 @@ TwitchChannel::TwitchChannel(const QString &name)
|
||||
this->loadRecentMessagesReconnect();
|
||||
});
|
||||
|
||||
this->destroyed.connect([this]() {
|
||||
getApp()->twitch->dropSeventvChannel(this->seventvUserID_,
|
||||
this->seventvEmoteSetID_);
|
||||
});
|
||||
|
||||
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
|
||||
if (msg->replyThread)
|
||||
{
|
||||
@@ -237,11 +243,16 @@ void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
|
||||
|
||||
SeventvEmotes::loadChannelEmotes(
|
||||
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())
|
||||
{
|
||||
this->seventvEmotes_.set(std::make_shared<EmoteMap>(
|
||||
std::forward<decltype(emoteMap)>(emoteMap)));
|
||||
this->updateSeventvData(channelInfo.userID,
|
||||
channelInfo.emoteSetID);
|
||||
this->seventvUserTwitchConnectionIndex_ =
|
||||
channelInfo.twitchConnectionIndex;
|
||||
}
|
||||
},
|
||||
manualRefresh);
|
||||
@@ -589,6 +600,203 @@ std::shared_ptr<const EmoteMap> TwitchChannel::seventvEmotes() const
|
||||
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()
|
||||
{
|
||||
return this->subscriptionUrl_;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "common/Outcome.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "messages/MessageThread.hpp"
|
||||
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||
#include "providers/twitch/ChannelPointReward.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
@@ -119,6 +120,23 @@ public:
|
||||
virtual void refreshFFZChannelEmotes(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
|
||||
boost::optional<EmotePtr> ffzCustomModBadge() const;
|
||||
boost::optional<EmotePtr> ffzCustomVipBadge() const;
|
||||
@@ -187,6 +205,41 @@ private:
|
||||
|
||||
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
|
||||
const QString subscriptionUrl_;
|
||||
const QString channelUrl_;
|
||||
@@ -225,6 +278,31 @@ private:
|
||||
QElapsedTimer clipCreationTimer_;
|
||||
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_;
|
||||
std::vector<boost::signals2::scoped_connection> bSignals_;
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/PubSubManager.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchHelpers.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
@@ -25,6 +27,12 @@ using namespace std::chrono_literals;
|
||||
|
||||
#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv"
|
||||
|
||||
namespace {
|
||||
|
||||
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TwitchIrcServer::TwitchIrcServer()
|
||||
@@ -36,6 +44,12 @@ TwitchIrcServer::TwitchIrcServer()
|
||||
this->initializeIrc();
|
||||
|
||||
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) {
|
||||
// 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
|
||||
|
||||
@@ -19,6 +19,7 @@ class Settings;
|
||||
class Paths;
|
||||
class PubSub;
|
||||
class TwitchChannel;
|
||||
class SeventvEventAPI;
|
||||
|
||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||
{
|
||||
@@ -41,6 +42,21 @@ public:
|
||||
void reloadSevenTVGlobalEmotes();
|
||||
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;
|
||||
|
||||
const ChannelPtr whispersChannel;
|
||||
@@ -49,6 +65,7 @@ public:
|
||||
IndirectChannel watchingChannel;
|
||||
|
||||
PubSub *pubsub;
|
||||
std::unique_ptr<SeventvEventAPI> seventvEventAPI;
|
||||
|
||||
const BttvEmotes &getBttvEmotes() const;
|
||||
const FfzEmotes &getFfzEmotes() const;
|
||||
|
||||
Reference in New Issue
Block a user