feat: Live Emote Updates for 7TV (#4090)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2022-11-13 12:07:41 +01:00
committed by GitHub
parent 8fa89b4073
commit 39f7d8ac4c
35 changed files with 1833 additions and 54 deletions
+209 -1
View File
@@ -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_;
+78
View File
@@ -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_;
+88
View File
@@ -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
+17
View File
@@ -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;