From d3bdf76e6a8b550b9a20f1a364e0ea752b6d6061 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Tue, 4 Apr 2023 16:02:09 +0200 Subject: [PATCH 01/23] feat(seventv): use new cosmetics system --- src/CMakeLists.txt | 1 + src/providers/seventv/SeventvBadges.cpp | 95 ++++++++++--- src/providers/seventv/SeventvBadges.hpp | 17 ++- src/providers/seventv/SeventvCosmetics.hpp | 35 +++++ src/providers/seventv/SeventvEmotes.cpp | 131 +++++++++--------- src/providers/seventv/SeventvEmotes.hpp | 25 +++- src/providers/seventv/SeventvEventAPI.cpp | 121 +++++++++++++++- src/providers/seventv/SeventvEventAPI.hpp | 21 +++ src/providers/seventv/eventapi/Dispatch.cpp | 44 ++++++ src/providers/seventv/eventapi/Dispatch.hpp | 23 +++ .../seventv/eventapi/Subscription.cpp | 30 ++++ .../seventv/eventapi/Subscription.hpp | 56 +++++++- src/providers/twitch/TwitchAccount.cpp | 32 +++++ src/providers/twitch/TwitchAccount.hpp | 10 ++ src/providers/twitch/TwitchAccountManager.cpp | 1 + src/providers/twitch/TwitchChannel.cpp | 79 ++++++++++- src/providers/twitch/TwitchChannel.hpp | 8 ++ src/singletons/Settings.hpp | 1 + src/widgets/settingspages/GeneralPage.cpp | 5 + 19 files changed, 639 insertions(+), 96 deletions(-) create mode 100644 src/providers/seventv/SeventvCosmetics.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e5bb4741384..2aff07e39e4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -260,6 +260,7 @@ set(SOURCE_FILES providers/seventv/SeventvBadges.cpp providers/seventv/SeventvBadges.hpp + providers/seventv/SeventvCosmetics.hpp providers/seventv/SeventvEmotes.cpp providers/seventv/SeventvEmotes.hpp providers/seventv/SeventvEventAPI.cpp diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index 1216d0863e2..bbd3431e9ff 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -4,6 +4,8 @@ #include "common/NetworkResult.hpp" #include "common/Outcome.hpp" #include "messages/Emote.hpp" +#include "messages/Image.hpp" +#include "providers/seventv/SeventvEmotes.hpp" #include #include @@ -17,22 +19,73 @@ void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/) this->loadSeventvBadges(); } -boost::optional SeventvBadges::getBadge(const UserId &id) +boost::optional SeventvBadges::getBadge(const UserId &id) const { std::shared_lock lock(this->mutex_); auto it = this->badgeMap_.find(id.string); if (it != this->badgeMap_.end()) { - return this->emotes_[it->second]; + return it->second; } return boost::none; } +void SeventvBadges::assignBadgeToUser(const QString &badgeID, + const UserId &userID) +{ + std::unique_lock lock(this->mutex_); + + const auto badgeIt = this->knownBadges_.find(badgeID); + if (badgeIt != this->knownBadges_.end()) + { + this->badgeMap_[userID.string] = badgeIt->second; + } +} + +void SeventvBadges::clearBadgeFromUser(const QString &badgeID, + const UserId &userID) +{ + std::unique_lock lock(this->mutex_); + + const auto it = this->badgeMap_.find(userID.string); + if (it != this->badgeMap_.end() && it->second->id.string == badgeID) + { + this->badgeMap_.erase(userID.string); + } +} + +void SeventvBadges::addBadge(const QJsonObject &badgeJson) +{ + const auto badgeID = badgeJson["id"].toString(); + + std::unique_lock lock(this->mutex_); + + if (this->knownBadges_.find(badgeID) != this->knownBadges_.end()) + { + return; + } + + auto emote = Emote{ + .name = EmoteName{}, + .images = SeventvEmotes::createImageSet(badgeJson), + .tooltip = Tooltip{badgeJson["tooltip"].toString()}, + .homePage = Url{}, + .id = EmoteId{badgeID}, + }; + + if (emote.images.getImage1()->isEmpty()) + { + return; // Bad images + } + + this->knownBadges_[badgeID] = + std::make_shared(std::move(emote)); +} + void SeventvBadges::loadSeventvBadges() { - // Cosmetics will work differently in v3, until this is ready - // we'll use this endpoint. + // This endpoint is used as a backup for badges static QUrl url("https://7tv.io/v2/cosmetics"); static QUrlQuery urlQuery; @@ -47,26 +100,28 @@ void SeventvBadges::loadSeventvBadges() std::unique_lock lock(this->mutex_); - int index = 0; for (const auto &jsonBadge : root.value("badges").toArray()) { - auto badge = jsonBadge.toObject(); - auto urls = badge.value("urls").toArray(); - auto emote = - Emote{EmoteName{}, - ImageSet{Url{urls.at(0).toArray().at(1).toString()}, - Url{urls.at(1).toArray().at(1).toString()}, - Url{urls.at(2).toArray().at(1).toString()}}, - Tooltip{badge.value("tooltip").toString()}, Url{}}; - - this->emotes_.push_back( - std::make_shared(std::move(emote))); - - for (const auto &user : badge.value("users").toArray()) + const auto badge = jsonBadge.toObject(); + auto badgeID = badge["id"].toString(); + auto urls = badge["urls"].toArray(); + auto emote = Emote{ + .name = EmoteName{}, + .images = ImageSet{Url{urls[0].toArray()[1].toString()}, + Url{urls[1].toArray()[1].toString()}, + Url{urls[2].toArray()[1].toString()}}, + .tooltip = Tooltip{badge["tooltip"].toString()}, + .homePage = Url{}, + .id = EmoteId{badgeID}, + }; + + auto emotePtr = std::make_shared(std::move(emote)); + this->knownBadges_[badgeID] = emotePtr; + + for (const auto &user : badge["users"].toArray()) { - this->badgeMap_[user.toString()] = index; + this->badgeMap_[user.toString()] = emotePtr; } - ++index; } return Success; diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp index 182d37b1055..d5c3723b410 100644 --- a/src/providers/seventv/SeventvBadges.hpp +++ b/src/providers/seventv/SeventvBadges.hpp @@ -5,6 +5,7 @@ #include "util/QStringHash.hpp" #include +#include #include #include @@ -20,16 +21,22 @@ class SeventvBadges : public Singleton public: void initialize(Settings &settings, Paths &paths) override; - boost::optional getBadge(const UserId &id); + boost::optional getBadge(const UserId &id) const; + + void addBadge(const QJsonObject &badgeJson); + void assignBadgeToUser(const QString &badgeID, const UserId &userID); + void clearBadgeFromUser(const QString &badgeID, const UserId &userID); private: void loadSeventvBadges(); - // Mutex for both `badgeMap_` and `emotes_` - std::shared_mutex mutex_; + // Mutex for both `badgeMap_` and `knownBadges_` + mutable std::shared_mutex mutex_; - std::unordered_map badgeMap_; - std::vector emotes_; + // user-id => badge + std::unordered_map badgeMap_; + // badge-id => badge + std::unordered_map knownBadges_; }; } // namespace chatterino diff --git a/src/providers/seventv/SeventvCosmetics.hpp b/src/providers/seventv/SeventvCosmetics.hpp new file mode 100644 index 00000000000..0d521ac03a8 --- /dev/null +++ b/src/providers/seventv/SeventvCosmetics.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace chatterino::seventv { + +enum class CosmeticKind { + Badge, + Paint, + EmoteSet, + + INVALID, +}; + +} // namespace chatterino::seventv + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name( + chatterino::seventv::CosmeticKind value) noexcept +{ + using chatterino::seventv::CosmeticKind; + switch (value) + { + case CosmeticKind::Badge: + return "BADGE"; + case CosmeticKind::Paint: + return "PAINT"; + case CosmeticKind::EmoteSet: + return "EMOTE_SET"; + + default: + return default_tag; + } +} diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 2f7883abc65..3b6d5a65530 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -77,71 +77,6 @@ bool isZeroWidthRecommended(const QJsonObject &emoteData) return flags.has(SeventvEmoteFlag::ZeroWidth); } -ImageSet makeImageSet(const QJsonObject &emoteData) -{ - auto host = emoteData["host"].toObject(); - // "//cdn.7tv[...]" - auto baseUrl = host["url"].toString(); - auto files = host["files"].toArray(); - - // TODO: emit four images - std::array sizes; - double baseWidth = 0.0; - int nextSize = 0; - - for (auto fileItem : files) - { - if (nextSize >= sizes.size()) - { - break; - } - - auto file = fileItem.toObject(); - if (file["format"].toString() != "WEBP") - { - continue; // We only use webp - } - - double width = file["width"].toDouble(); - double scale = 1.0; // in relation to first image - if (baseWidth > 0.0) - { - scale = baseWidth / width; - } - else - { - // => this is the first image - baseWidth = width; - } - - auto image = Image::fromUrl( - {QString("https:%1/%2").arg(baseUrl, file["name"].toString())}, - scale); - - sizes.at(nextSize) = image; - nextSize++; - } - - if (nextSize < sizes.size()) - { - // this should be really rare - // this means we didn't get all sizes of an emote - if (nextSize == 0) - { - qCDebug(chatterinoSeventv) - << "Got file list without any eligible files"; - // When this emote is typed, chatterino will crash. - return ImageSet{}; - } - for (; nextSize < sizes.size(); nextSize++) - { - sizes.at(nextSize) = Image::getEmpty(); - } - } - - return ImageSet{sizes[0], sizes[1], sizes[2]}; -} - Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal) { return Tooltip{QString("%1
%2 7TV Emote
By: %3") @@ -172,7 +107,7 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote, ? createAliasedTooltip(emoteName.string, baseEmoteName.string, author.string, isGlobal) : createTooltip(emoteName.string, author.string, isGlobal); - auto imageSet = makeImageSet(emoteData); + auto imageSet = SeventvEmotes::createImageSet(emoteData); auto emote = Emote({emoteName, imageSet, tooltip, @@ -508,4 +443,68 @@ void SeventvEmotes::getEmoteSet( .execute(); } +ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData) +{ + auto host = emoteData["host"].toObject(); + // "//cdn.7tv[...]" + auto baseUrl = host["url"].toString(); + auto files = host["files"].toArray(); + + std::array sizes; + double baseWidth = 0.0; + size_t nextSize = 0; + + for (auto fileItem : files) + { + if (nextSize >= sizes.size()) + { + break; + } + + auto file = fileItem.toObject(); + if (file["format"].toString() != "WEBP") + { + continue; // We only use webp + } + + double width = file["width"].toDouble(); + double scale = 1.0; // in relation to first image + if (baseWidth > 0.0) + { + scale = baseWidth / width; + } + else + { + // => this is the first image + baseWidth = width; + } + + auto image = Image::fromUrl( + {QString("https:%1/%2").arg(baseUrl, file["name"].toString())}, + scale); + + sizes.at(nextSize) = image; + nextSize++; + } + + if (nextSize < sizes.size()) + { + // this should be really rare + // this means we didn't get all sizes of an emote + if (nextSize == 0) + { + qCDebug(chatterinoSeventv) + << "Got file list without any eligible files"; + // When this emote is typed, chatterino will crash. + return ImageSet{}; + } + for (; nextSize < sizes.size(); nextSize++) + { + sizes.at(nextSize) = Image::getEmpty(); + } + } + + return ImageSet{sizes[0], sizes[1], sizes[2]}; +} + } // namespace chatterino diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index f978337be43..3b214f4f1b7 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -5,12 +5,14 @@ #include "common/Atomic.hpp" #include "common/FlagsEnum.hpp" +#include + #include namespace chatterino { +class ImageSet; class Channel; - namespace seventv::eventapi { struct EmoteAddDispatch; struct EmoteUpdateDispatch; @@ -61,6 +63,20 @@ struct Emote; using EmotePtr = std::shared_ptr; class EmoteMap; +enum class SeventvEmoteSetKind : uint8_t { + Global, + Personal, + Channel, +}; + +enum class SeventvEmoteSetFlag : uint32_t { + Immutable = (1 << 0), + Privileged = (1 << 1), + Personal = (1 << 2), + Commercial = (1 << 3), +}; +using SeventvEmoteSetFlags = FlagsEnum; + class SeventvEmotes final { public: @@ -119,6 +135,13 @@ class SeventvEmotes final std::function successCallback, std::function errorCallback); + /** + * Creates an image set from a 7TV emote or badge. + * + * @param emoteData { host: { files: [], url } } + */ + static ImageSet createImageSet(const QJsonObject &emoteData); + private: Atomic> global_; }; diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index 5cec6ed30a3..14eb8dc5b87 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -1,8 +1,11 @@ #include "providers/seventv/SeventvEventAPI.hpp" +#include "Application.hpp" #include "providers/seventv/eventapi/Client.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Message.hpp" +#include "providers/seventv/SeventvBadges.hpp" +#include "providers/seventv/SeventvCosmetics.hpp" #include @@ -10,6 +13,7 @@ namespace chatterino { +using namespace seventv; using namespace seventv::eventapi; SeventvEventAPI::SeventvEventAPI( @@ -35,6 +39,19 @@ void SeventvEventAPI::subscribeUser(const QString &userID, } } +void SeventvEventAPI::subscribeTwitchChannel(const QString &id) +{ + if (this->subscribedTwitchChannels_.insert(id).second) + { + this->subscribe( + {ChannelCondition{id}, SubscriptionType::CreateCosmetic}); + this->subscribe( + {ChannelCondition{id}, SubscriptionType::CreateEntitlement}); + this->subscribe( + {ChannelCondition{id}, SubscriptionType::DeleteEntitlement}); + } +} + void SeventvEventAPI::unsubscribeEmoteSet(const QString &id) { if (this->subscribedEmoteSets_.erase(id) > 0) @@ -53,6 +70,19 @@ void SeventvEventAPI::unsubscribeUser(const QString &id) } } +void SeventvEventAPI::unsubscribeTwitchChannel(const QString &id) +{ + if (this->subscribedTwitchChannels_.erase(id) > 0) + { + this->unsubscribe( + {ChannelCondition{id}, SubscriptionType::CreateCosmetic}); + this->unsubscribe( + {ChannelCondition{id}, SubscriptionType::CreateEntitlement}); + this->unsubscribe( + {ChannelCondition{id}, SubscriptionType::DeleteEntitlement}); + } +} + std::shared_ptr> SeventvEventAPI::createClient( liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl) { @@ -144,9 +174,49 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch) this->onUserUpdate(dispatch); } break; + case SubscriptionType::CreateCosmetic: { + const CosmeticCreateDispatch cosmetic(dispatch); + if (cosmetic.validate()) + { + this->onCosmeticCreate(cosmetic); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid cosmetic dispatch" << dispatch.body; + } + } + break; + case SubscriptionType::CreateEntitlement: { + const EntitlementCreateDeleteDispatch entitlement(dispatch); + if (entitlement.validate()) + { + this->onEntitlementCreate(entitlement); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid entitlement create dispatch" << dispatch.body; + } + } + break; + case SubscriptionType::DeleteEntitlement: { + const EntitlementCreateDeleteDispatch entitlement(dispatch); + if (entitlement.validate()) + { + this->onEntitlementDelete(entitlement); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid entitlement delete dispatch" << dispatch.body; + } + } + break; default: { qCDebug(chatterinoSeventvEventAPI) - << "Unknown subscription type:" << (int)dispatch.type + << "Unknown subscription type:" + << magic_enum::enum_name(dispatch.type).data() << "body:" << dispatch.body; } break; @@ -261,4 +331,53 @@ void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch) } } +// NOLINTBEGIN(readability-convert-member-functions-to-static) + +// We're using `Application::instance`, because we're not in the GUI thread. +// `seventvBadges` does its own locking. + +void SeventvEventAPI::onCosmeticCreate(const CosmeticCreateDispatch &cosmetic) +{ + switch (cosmetic.kind) + { + case CosmeticKind::Badge: { + Application::instance->seventvBadges->addBadge(cosmetic.data); + } + break; + default: + break; + } +} + +void SeventvEventAPI::onEntitlementCreate( + const EntitlementCreateDeleteDispatch &entitlement) +{ + switch (entitlement.kind) + { + case CosmeticKind::Badge: { + Application::instance->seventvBadges->assignBadgeToUser( + entitlement.refID, UserId{entitlement.userID}); + } + break; + default: + break; + } +} + +void SeventvEventAPI::onEntitlementDelete( + const EntitlementCreateDeleteDispatch &entitlement) +{ + switch (entitlement.kind) + { + case CosmeticKind::Badge: { + Application::instance->seventvBadges->clearBadgeFromUser( + entitlement.refID, UserId{entitlement.userID}); + } + break; + default: + break; + } +} +// NOLINTEND(readability-convert-member-functions-to-static) + } // namespace chatterino diff --git a/src/providers/seventv/SeventvEventAPI.hpp b/src/providers/seventv/SeventvEventAPI.hpp index 5672e59b8d7..a07c3821b63 100644 --- a/src/providers/seventv/SeventvEventAPI.hpp +++ b/src/providers/seventv/SeventvEventAPI.hpp @@ -15,8 +15,12 @@ namespace seventv::eventapi { struct EmoteUpdateDispatch; struct EmoteRemoveDispatch; struct UserConnectionUpdateDispatch; + struct CosmeticCreateDispatch; + struct EntitlementCreateDeleteDispatch; } // namespace seventv::eventapi +class SeventvBadges; + class SeventvEventAPI : public BasicPubSubManager { @@ -44,11 +48,20 @@ class SeventvEventAPI * @param emoteSetID 7TV emote-set-id, may be empty. */ void subscribeUser(const QString &userID, const QString &emoteSetID); + /** + * Subscribes to cosmetics and entitlements in a twitch channel + * if not already subscribed. + * + * @param id Twitch channel id + */ + void subscribeTwitchChannel(const QString &id); /** Unsubscribes from a user by its 7TV user id */ void unsubscribeUser(const QString &id); /** Unsubscribes from an emote-set by its id */ void unsubscribeEmoteSet(const QString &id); + /** Unsubscribes from cosmetics and entitlements in a Twitch channel */ + void unsubscribeTwitchChannel(const QString &id); protected: std::shared_ptr> @@ -64,11 +77,19 @@ class SeventvEventAPI void onEmoteSetUpdate(const seventv::eventapi::Dispatch &dispatch); void onUserUpdate(const seventv::eventapi::Dispatch &dispatch); + void onCosmeticCreate( + const seventv::eventapi::CosmeticCreateDispatch &cosmetic); + void onEntitlementCreate( + const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement); + void onEntitlementDelete( + const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement); /** emote-set ids */ std::unordered_set subscribedEmoteSets_; /** user ids */ std::unordered_set subscribedUsers_; + /** Twitch channel ids */ + std::unordered_set subscribedTwitchChannels_; std::chrono::milliseconds heartbeatInterval_; }; diff --git a/src/providers/seventv/eventapi/Dispatch.cpp b/src/providers/seventv/eventapi/Dispatch.cpp index bb4b4fa1da9..e2cd3aa5ffb 100644 --- a/src/providers/seventv/eventapi/Dispatch.cpp +++ b/src/providers/seventv/eventapi/Dispatch.cpp @@ -1,5 +1,7 @@ #include "providers/seventv/eventapi/Dispatch.hpp" +#include + #include namespace chatterino::seventv::eventapi { @@ -91,4 +93,46 @@ bool UserConnectionUpdateDispatch::validate() const !this->emoteSetID.isEmpty(); } +CosmeticCreateDispatch::CosmeticCreateDispatch(const Dispatch &dispatch) + : data(dispatch.body["object"]["data"].toObject()) + , kind(magic_enum::enum_cast( + dispatch.body["object"]["kind"].toString().toStdString()) + .value_or(CosmeticKind::INVALID)) +{ +} + +bool CosmeticCreateDispatch::validate() const +{ + return !this->data.empty() && this->kind != CosmeticKind::INVALID; +} + +EntitlementCreateDeleteDispatch::EntitlementCreateDeleteDispatch( + const Dispatch &dispatch) +{ + const auto obj = dispatch.body["object"].toObject(); + this->userID = QString(); + this->refID = obj["ref_id"].toString(); + this->kind = magic_enum::enum_cast( + obj["kind"].toString().toStdString()) + .value_or(CosmeticKind::INVALID); + + const auto userConnections = obj["user"]["connections"].toArray(); + for (const auto &connectionJson : userConnections) + { + const auto connection = connectionJson.toObject(); + if (connection["platform"].toString() == "TWITCH") + { + this->userID = connection["id"].toString(); + this->userName = connection["username"].toString(); + break; + } + } +} + +bool EntitlementCreateDeleteDispatch::validate() const +{ + return !this->userID.isEmpty() && !this->userName.isEmpty() && + !this->refID.isEmpty() && this->kind != CosmeticKind::INVALID; +} + } // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Dispatch.hpp b/src/providers/seventv/eventapi/Dispatch.hpp index 666f5c28ac6..04bad159bec 100644 --- a/src/providers/seventv/eventapi/Dispatch.hpp +++ b/src/providers/seventv/eventapi/Dispatch.hpp @@ -1,6 +1,7 @@ #pragma once #include "providers/seventv/eventapi/Subscription.hpp" +#include "providers/seventv/SeventvCosmetics.hpp" #include #include @@ -67,4 +68,26 @@ struct UserConnectionUpdateDispatch { bool validate() const; }; +struct CosmeticCreateDispatch { + QJsonObject data; + CosmeticKind kind; + + CosmeticCreateDispatch(const Dispatch &dispatch); + + bool validate() const; +}; + +struct EntitlementCreateDeleteDispatch { + /** id of the user */ + QString userID; + QString userName; + /** id of the entitlement */ + QString refID; + CosmeticKind kind; + + EntitlementCreateDeleteDispatch(const Dispatch &dispatch); + + bool validate() const; +}; + } // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Subscription.cpp b/src/providers/seventv/eventapi/Subscription.cpp index 1de1f667e7f..91d330c5e61 100644 --- a/src/providers/seventv/eventapi/Subscription.cpp +++ b/src/providers/seventv/eventapi/Subscription.cpp @@ -102,4 +102,34 @@ QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition) return dbg; } +ChannelCondition::ChannelCondition(QString twitchID) + : twitchID(std::move(twitchID)) +{ +} + +QJsonObject ChannelCondition::encode() const +{ + QJsonObject obj; + obj["ctx"] = "channel"; + obj["platform"] = "TWITCH"; + obj["id"] = this->twitchID; + return obj; +} + +QDebug &operator<<(QDebug &dbg, const ChannelCondition &condition) +{ + dbg << "{ twitchID:" << condition.twitchID << '}'; + return dbg; +} + +bool ChannelCondition::operator==(const ChannelCondition &rhs) const +{ + return this->twitchID == rhs.twitchID; +} + +bool ChannelCondition::operator!=(const ChannelCondition &rhs) const +{ + return !(*this == rhs); +} + } // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Subscription.hpp b/src/providers/seventv/eventapi/Subscription.hpp index 53143fbd861..1a36811a56b 100644 --- a/src/providers/seventv/eventapi/Subscription.hpp +++ b/src/providers/seventv/eventapi/Subscription.hpp @@ -12,9 +12,22 @@ namespace chatterino::seventv::eventapi { // https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types enum class SubscriptionType { + AnyEmoteSet, + CreateEmoteSet, UpdateEmoteSet, + UpdateUser, + AnyCosmetic, + CreateCosmetic, + UpdateCosmetic, + DeleteCosmetic, + + AnyEntitlement, + CreateEntitlement, + UpdateEntitlement, + DeleteEntitlement, + INVALID, }; @@ -46,7 +59,19 @@ struct ObjectIDCondition { bool operator!=(const ObjectIDCondition &rhs) const; }; -using Condition = std::variant; +struct ChannelCondition { + ChannelCondition(QString twitchID); + + QString twitchID; + + QJsonObject encode() const; + + friend QDebug &operator<<(QDebug &dbg, const ChannelCondition &condition); + bool operator==(const ChannelCondition &rhs) const; + bool operator!=(const ChannelCondition &rhs) const; +}; + +using Condition = std::variant; struct Subscription { bool operator==(const Subscription &rhs) const; @@ -70,10 +95,30 @@ constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< using chatterino::seventv::eventapi::SubscriptionType; switch (value) { + case SubscriptionType::AnyEmoteSet: + return "emote_set.*"; + case SubscriptionType::CreateEmoteSet: + return "emote_set.create"; case SubscriptionType::UpdateEmoteSet: return "emote_set.update"; case SubscriptionType::UpdateUser: return "user.update"; + case SubscriptionType::AnyCosmetic: + return "cosmetic.*"; + case SubscriptionType::CreateCosmetic: + return "cosmetic.create"; + case SubscriptionType::UpdateCosmetic: + return "cosmetic.update"; + case SubscriptionType::DeleteCosmetic: + return "cosmetic.delete"; + case SubscriptionType::AnyEntitlement: + return "entitlement.*"; + case SubscriptionType::CreateEntitlement: + return "entitlement.create"; + case SubscriptionType::UpdateEntitlement: + return "entitlement.update"; + case SubscriptionType::DeleteEntitlement: + return "entitlement.delete"; default: return default_tag; @@ -91,6 +136,15 @@ struct hash { } }; +template <> +struct hash { + size_t operator()( + const chatterino::seventv::eventapi::ChannelCondition &c) const + { + return qHash(c.twitchID); + } +}; + template <> struct hash { size_t operator()( diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 789ed8339f0..bae4eb0b634 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -4,6 +4,7 @@ #include "common/Channel.hpp" #include "common/Env.hpp" #include "common/NetworkRequest.hpp" +#include "common/NetworkResult.hpp" #include "common/Outcome.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" @@ -442,4 +443,35 @@ void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel) }); } +const QString &TwitchAccount::getSeventvUserID() const +{ + return this->seventvUserID_; +} + +void TwitchAccount::loadSeventvUserID() +{ + if (!this->seventvUserID_.isEmpty() || this->isAnon()) + { + return; + } + + static const QString seventvUserInfoUrl = + QStringLiteral("https://7tv.io/v3/users/twitch/%1"); + + NetworkRequest(seventvUserInfoUrl.arg(this->getUserId()), + NetworkRequestType::Get) + .timeout(20000) + .onSuccess([this](const auto &response) { + const auto json = response.parseJson(); + const auto id = json["user"]["id"].toString(); + if (!id.isEmpty()) + { + this->seventvUserID_ = id; + } + return Success; + }) + .concurrent() + .execute(); +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index bfb8b98ca66..f1dea9add71 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -56,6 +56,12 @@ class TwitchAccount : public Account const QString &getOAuthClient() const; const QString &getUserId() const; + /** + * The Seventv user-id of the current user. + * Empty if there's no associated Seventv user with this twitch user. + */ + const QString &getSeventvUserID() const; + QColor color(); void setColor(QColor color); @@ -93,6 +99,8 @@ class TwitchAccount : public Account void autoModAllow(const QString msgID, ChannelPtr channel); void autoModDeny(const QString msgID, ChannelPtr channel); + void loadSeventvUserID(); + private: QString oauthClient_; QString oauthToken_; @@ -109,6 +117,8 @@ class TwitchAccount : public Account // std::map emotes; UniqueAccess emotes_; UniqueAccess> localEmotes_; + + QString seventvUserID_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index 6b02d3bb245..65a5f3a39d8 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -17,6 +17,7 @@ TwitchAccountManager::TwitchAccountManager() this->currentUserChanged.connect([this] { auto currentUser = this->getCurrent(); currentUser->loadBlocks(); + currentUser->loadSeventvUserID(); }); this->accounts.itemRemoved.connect([this](const auto &acc) { diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 89972c8d023..336e1f24872 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -106,6 +107,7 @@ TwitchChannel::TwitchChannel(const QString &name) this->refreshBTTVChannelEmotes(false); this->refreshSevenTVChannelEmotes(false); this->joinBttvChannel(); + this->listenSevenTVCosmetics(); }); this->connected.connect([this]() { @@ -168,6 +170,12 @@ TwitchChannel::~TwitchChannel() { getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId()); } + + if (getApp()->twitch->seventvEventAPI) + { + getApp()->twitch->seventvEventAPI->unsubscribeTwitchChannel( + this->roomId()); + } } void TwitchChannel::initialize() @@ -436,6 +444,7 @@ void TwitchChannel::sendMessage(const QString &message) bool messageSent = false; this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent); + this->updateSevenTVActivity(); if (messageSent) { @@ -1255,8 +1264,8 @@ void TwitchChannel::addReplyThread(const std::shared_ptr &thread) this->threads_[thread->rootId()] = thread; } -const std::unordered_map> - &TwitchChannel::threads() const +const std::unordered_map> & + TwitchChannel::threads() const { return this->threads_; } @@ -1576,4 +1585,70 @@ boost::optional TwitchChannel::cheerEmote(const QString &string) return boost::none; } +void TwitchChannel::updateSevenTVActivity() +{ + static const QString seventvActivityUrl = + QStringLiteral("https://7tv.io/v3/users/%1/presences"); + + const auto currentSeventvUserID = + getApp()->accounts->twitch.getCurrent()->getSeventvUserID(); + if (currentSeventvUserID.isEmpty()) + { + return; + } + + if (!getSettings()->enableSevenTVEventAPI || + !getSettings()->sendSevenTVActivity) + { + return; + } + + if (this->nextSeventvActivity_.isValid() && + QDateTime::currentDateTimeUtc() < this->nextSeventvActivity_) + { + return; + } + // Make sure to not send activity again before receiving the response + this->nextSeventvActivity_ = this->nextSeventvActivity_.addSecs(300); + + qCDebug(chatterinoSeventv) << "Sending activity in" << this->getName(); + + QJsonObject payload; + payload["kind"] = 1; // UserPresenceKindChannel + + QJsonObject data; + data["id"] = this->roomId(); + data["platform"] = "TWITCH"; + + payload["data"] = data; + + NetworkRequest(seventvActivityUrl.arg(currentSeventvUserID), + NetworkRequestType::Post) + .header("Content-Type", "application/json") + .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + .onSuccess([chan = weakOf(this)](const auto &response) { + const auto self = + std::dynamic_pointer_cast(chan.lock()); + if (!self) + { + return Success; + } + const auto json = response.parseJson(); + self->nextSeventvActivity_ = + QDateTime::currentDateTimeUtc().addSecs(10); + return Success; + }) + .concurrent() + .execute(); +} + +void TwitchChannel::listenSevenTVCosmetics() +{ + if (getApp()->twitch->seventvEventAPI) + { + getApp()->twitch->seventvEventAPI->subscribeTwitchChannel( + this->roomId()); + } +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 746b54ed625..9393c66a89e 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -226,6 +226,8 @@ class TwitchChannel : public Channel, public ChannelChatters void showLoginMessage(); /** Joins (subscribes to) a Twitch channel for updates on BTTV. */ void joinBttvChannel() const; + void updateSevenTVActivity(); + void listenSevenTVCosmetics(); void setLive(bool newLiveStatus); void setMod(bool value); @@ -330,6 +332,12 @@ class TwitchChannel : public Channel, public ChannelChatters */ size_t seventvUserTwitchConnectionIndex_; + /** + * The next moment in time to signal activity in this channel to 7TV. + * Or: Up until this moment we don't need to send activity. + */ + QDateTime nextSeventvActivity_; + /** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */ QString lastLiveUpdateEmotePlatform_; /** The actor name of the last live emote update. */ diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 467681412e3..9c8b75944c2 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -232,6 +232,7 @@ class Settings : public ABSettings, public ConcurrentSettings BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true}; BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true}; BoolSetting enableSevenTVEventAPI = {"/emotes/seventv/eventapi", true}; + BoolSetting sendSevenTVActivity = {"/emotes/seventv/sendActivity", true}; /// Links BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 27e9d94a717..23b816c5187 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -444,6 +444,11 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addCheckbox("Show 7TV channel emotes", s.enableSevenTVChannelEmotes); layout.addCheckbox("Enable 7TV live emote updates (requires restart)", s.enableSevenTVEventAPI); + layout.addCheckbox("Send activity to 7TV", s.sendSevenTVActivity, false, + "When enabled, Chatterino will signal an activity to " + "7TV when you send a chat mesage. This is used for " + "badges, paints, and personal emotes. When disabled, no " + "activity is sent and others won't see your cosmetics."); layout.addTitle("Streamer Mode"); layout.addDescription( From c4cf423435d7a6501e4c42c4ee8a5f5fb7991a25 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Tue, 4 Apr 2023 16:13:22 +0200 Subject: [PATCH 02/23] chore: add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca71d500c0f..7dc2f4aaaf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Minor: Added support for FrankerFaceZ animated emotes. (#4434) - Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463) - Minor: Added the ability to reply to a message by `Shift + Right Click`ing the username. (#4424) +- Minor: 7TV badges now automatically update upon changing. (#4512) - Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314) - Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314) - Bugfix: Fixed an issue where context-menu items for zero-width emotes displayed the wrong provider. (#4460) From a8de55c36fa722c4fe9bfcd3a385d0be2bc323b2 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Tue, 4 Apr 2023 16:18:35 +0200 Subject: [PATCH 03/23] fix: old `clang-format` --- src/providers/twitch/TwitchChannel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 336e1f24872..9165805a3fb 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1264,8 +1264,8 @@ void TwitchChannel::addReplyThread(const std::shared_ptr &thread) this->threads_[thread->rootId()] = thread; } -const std::unordered_map> & - TwitchChannel::threads() const +const std::unordered_map> + &TwitchChannel::threads() const { return this->threads_; } From ec60f52b1dd759b039422163d0edfd1da765da93 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 11:59:18 +0200 Subject: [PATCH 04/23] fix: small suggestions pt1 --- src/providers/seventv/SeventvEventAPI.cpp | 58 ++++++++++++++------- src/providers/seventv/eventapi/Dispatch.cpp | 1 - src/providers/twitch/TwitchAccount.cpp | 7 ++- src/providers/twitch/TwitchChannel.cpp | 6 +-- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index 14eb8dc5b87..5c78853f2a5 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -43,12 +43,18 @@ void SeventvEventAPI::subscribeTwitchChannel(const QString &id) { if (this->subscribedTwitchChannels_.insert(id).second) { - this->subscribe( - {ChannelCondition{id}, SubscriptionType::CreateCosmetic}); - this->subscribe( - {ChannelCondition{id}, SubscriptionType::CreateEntitlement}); - this->subscribe( - {ChannelCondition{id}, SubscriptionType::DeleteEntitlement}); + this->subscribe({ + ChannelCondition{id}, + SubscriptionType::CreateCosmetic, + }); + this->subscribe({ + ChannelCondition{id}, + SubscriptionType::CreateEntitlement, + }); + this->subscribe({ + ChannelCondition{id}, + SubscriptionType::DeleteEntitlement, + }); } } @@ -74,12 +80,18 @@ void SeventvEventAPI::unsubscribeTwitchChannel(const QString &id) { if (this->subscribedTwitchChannels_.erase(id) > 0) { - this->unsubscribe( - {ChannelCondition{id}, SubscriptionType::CreateCosmetic}); - this->unsubscribe( - {ChannelCondition{id}, SubscriptionType::CreateEntitlement}); - this->unsubscribe( - {ChannelCondition{id}, SubscriptionType::DeleteEntitlement}); + this->unsubscribe({ + ChannelCondition{id}, + SubscriptionType::CreateCosmetic, + }); + this->unsubscribe({ + ChannelCondition{id}, + SubscriptionType::CreateEntitlement, + }); + this->unsubscribe({ + ChannelCondition{id}, + SubscriptionType::DeleteEntitlement, + }); } } @@ -333,15 +345,15 @@ void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch) // NOLINTBEGIN(readability-convert-member-functions-to-static) -// We're using `Application::instance`, because we're not in the GUI thread. -// `seventvBadges` does its own locking. - void SeventvEventAPI::onCosmeticCreate(const CosmeticCreateDispatch &cosmetic) { + // We're using `Application::instance` instead of getApp(), because we're not in the GUI thread. + // `seventvBadges` does its own locking. + auto *badges = Application::instance->seventvBadges; switch (cosmetic.kind) { case CosmeticKind::Badge: { - Application::instance->seventvBadges->addBadge(cosmetic.data); + badges->addBadge(cosmetic.data); } break; default: @@ -352,11 +364,14 @@ void SeventvEventAPI::onCosmeticCreate(const CosmeticCreateDispatch &cosmetic) void SeventvEventAPI::onEntitlementCreate( const EntitlementCreateDeleteDispatch &entitlement) { + // We're using `Application::instance` instead of getApp(), because we're not in the GUI thread. + // `seventvBadges` does its own locking. + auto *badges = Application::instance->seventvBadges; switch (entitlement.kind) { case CosmeticKind::Badge: { - Application::instance->seventvBadges->assignBadgeToUser( - entitlement.refID, UserId{entitlement.userID}); + badges->assignBadgeToUser(entitlement.refID, + UserId{entitlement.userID}); } break; default: @@ -367,11 +382,14 @@ void SeventvEventAPI::onEntitlementCreate( void SeventvEventAPI::onEntitlementDelete( const EntitlementCreateDeleteDispatch &entitlement) { + // We're using `Application::instance` instead of getApp(), because we're not in the GUI thread. + // `seventvBadges` does its own locking. + auto *badges = Application::instance->seventvBadges; switch (entitlement.kind) { case CosmeticKind::Badge: { - Application::instance->seventvBadges->clearBadgeFromUser( - entitlement.refID, UserId{entitlement.userID}); + badges->clearBadgeFromUser(entitlement.refID, + UserId{entitlement.userID}); } break; default: diff --git a/src/providers/seventv/eventapi/Dispatch.cpp b/src/providers/seventv/eventapi/Dispatch.cpp index e2cd3aa5ffb..03fbdac970c 100644 --- a/src/providers/seventv/eventapi/Dispatch.cpp +++ b/src/providers/seventv/eventapi/Dispatch.cpp @@ -110,7 +110,6 @@ EntitlementCreateDeleteDispatch::EntitlementCreateDeleteDispatch( const Dispatch &dispatch) { const auto obj = dispatch.body["object"].toObject(); - this->userID = QString(); this->refID = obj["ref_id"].toString(); this->kind = magic_enum::enum_cast( obj["kind"].toString().toStdString()) diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 466b76d6c29..3aca2ac73bd 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -452,7 +452,11 @@ const QString &TwitchAccount::getSeventvUserID() const void TwitchAccount::loadSeventvUserID() { - if (!this->seventvUserID_.isEmpty() || this->isAnon()) + if (this->isAnon()) + { + return; + } + if (!this->seventvUserID_.isEmpty()) { return; } @@ -472,7 +476,6 @@ void TwitchAccount::loadSeventvUserID() } return Success; }) - .concurrent() .execute(); } diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 7389a166369..f9f4eb29c80 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1261,8 +1261,8 @@ void TwitchChannel::addReplyThread(const std::shared_ptr &thread) this->threads_[thread->rootId()] = thread; } -const std::unordered_map> - &TwitchChannel::threads() const +const std::unordered_map> & + TwitchChannel::threads() const { return this->threads_; } @@ -1651,12 +1651,10 @@ void TwitchChannel::updateSevenTVActivity() { return Success; } - const auto json = response.parseJson(); self->nextSeventvActivity_ = QDateTime::currentDateTimeUtc().addSecs(10); return Success; }) - .concurrent() .execute(); } From 81c3f624d079eb27352b76876c321e7175505f1c Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 13:21:10 +0200 Subject: [PATCH 05/23] refactor: add 7tv api wrapper --- src/CMakeLists.txt | 2 + src/providers/seventv/SeventvApi.cpp | 112 +++++++++++++++++++++++ src/providers/seventv/SeventvApi.hpp | 35 ++++++++ src/providers/seventv/SeventvBadges.cpp | 28 ++---- src/providers/seventv/SeventvEmotes.cpp | 114 +++++++++++------------- src/providers/twitch/TwitchAccount.cpp | 20 ++--- src/providers/twitch/TwitchChannel.cpp | 25 ++---- 7 files changed, 227 insertions(+), 109 deletions(-) create mode 100644 src/providers/seventv/SeventvApi.cpp create mode 100644 src/providers/seventv/SeventvApi.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2e6db64ae8b..5c78bfc662c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -280,6 +280,8 @@ set(SOURCE_FILES providers/liveupdates/BasicPubSubManager.hpp providers/liveupdates/BasicPubSubWebsocket.hpp + providers/seventv/SeventvApi.cpp + providers/seventv/SeventvApi.hpp providers/seventv/SeventvBadges.cpp providers/seventv/SeventvBadges.hpp providers/seventv/SeventvCosmetics.hpp diff --git a/src/providers/seventv/SeventvApi.cpp b/src/providers/seventv/SeventvApi.cpp new file mode 100644 index 00000000000..95767577cde --- /dev/null +++ b/src/providers/seventv/SeventvApi.cpp @@ -0,0 +1,112 @@ +#include "providers/seventv/SeventvApi.hpp" + +#include "common/Literals.hpp" +#include "common/NetworkRequest.hpp" +#include "common/NetworkResult.hpp" +#include "common/Outcome.hpp" + +namespace { +using namespace chatterino::literals; + +const QString API_URL_USER = u"https://7tv.io/v3/users/twitch/%1"_s; +const QString API_URL_EMOTE_SET = u"https://7tv.io/v3/emote-sets/%1"_s; +const QString API_URL_PRESENCES = u"https://7tv.io/v3/users/%1/presences"_s; + +// V2 +const QString API_URL_COSMETICS = + u"https://7tv.io/v2/cosmetics?user_identifier=twitch_id"_s; + +} // namespace + +// NOLINTBEGIN(readability-convert-member-functions-to-static) +namespace chatterino { + +void SeventvApi::getUserByTwitchID( + const QString &twitchID, SuccessCallback &&onSuccess, + ErrorCallback &&onError) +{ + NetworkRequest(API_URL_USER.arg(twitchID), NetworkRequestType::Get) + .timeout(20000) + .onSuccess([callback = std::move(onSuccess)]( + const NetworkResult &result) -> Outcome { + auto json = result.parseJson(); + callback(json); + return Success; + }) + .onError([callback = std::move(onError)](const NetworkResult &result) { + callback(result); + }) + .execute(); +} + +void SeventvApi::getEmoteSet(const QString &emoteSet, + SuccessCallback &&onSuccess, + ErrorCallback &&onError) +{ + NetworkRequest(API_URL_EMOTE_SET.arg(emoteSet), NetworkRequestType::Get) + .timeout(25000) + .onSuccess([callback = std::move(onSuccess)]( + const NetworkResult &result) -> Outcome { + auto json = result.parseJson(); + callback(json); + return Success; + }) + .onError([callback = std::move(onError)](const NetworkResult &result) { + callback(result); + }) + .execute(); +} + +void SeventvApi::getCosmetics(SuccessCallback &&onSuccess, + ErrorCallback &&onError) +{ + NetworkRequest(API_URL_COSMETICS) + .timeout(20000) + .onSuccess([callback = std::move(onSuccess)]( + const NetworkResult &result) -> Outcome { + auto json = result.parseJson(); + callback(json); + return Success; + }) + .onError([callback = std::move(onError)](const NetworkResult &result) { + callback(result); + }) + .execute(); +} + +void SeventvApi::updatePresence(const QString &twitchChannelID, + const QString &seventvUserID, + SuccessCallback<> &&onSuccess, + ErrorCallback &&onError) +{ + QJsonObject payload{ + {u"kind"_s, 1}, // UserPresenceKindChannel + {u"data"_s, + QJsonObject{ + {u"id"_s, twitchChannelID}, + {u"platform"_s, u"TWITCH"_s}, + }}, + }; + + NetworkRequest(API_URL_PRESENCES.arg(seventvUserID), + NetworkRequestType::Post) + .json(payload) + .timeout(10000) + .onSuccess([callback = std::move(onSuccess)](const auto &) -> Outcome { + callback(); + return Success; + }) + .onError([callback = std::move(onError)](const NetworkResult &result) { + callback(result); + }) + .execute(); +} + +SeventvApi &getSeventvApi() +{ + static SeventvApi instance; + return instance; +} + +} // namespace chatterino +// NOLINTEND(readability-convert-member-functions-to-static) diff --git a/src/providers/seventv/SeventvApi.hpp b/src/providers/seventv/SeventvApi.hpp new file mode 100644 index 00000000000..dc115192723 --- /dev/null +++ b/src/providers/seventv/SeventvApi.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +class QString; +class QJsonObject; + +namespace chatterino { + +class NetworkResult; + +class SeventvApi +{ + using ErrorCallback = std::function; + template + using SuccessCallback = std::function; + +public: + void getUserByTwitchID(const QString &twitchID, + SuccessCallback &&onSuccess, + ErrorCallback &&onError); + void getEmoteSet(const QString &emoteSet, + SuccessCallback &&onSuccess, + ErrorCallback &&onError); + void getCosmetics(SuccessCallback &&onSuccess, + ErrorCallback &&onError); + + void updatePresence(const QString &twitchChannelID, + const QString &seventvUserID, + SuccessCallback<> &&onSuccess, ErrorCallback &&onError); +}; + +SeventvApi &getSeventvApi(); + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index bbd3431e9ff..baff6dab579 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -1,17 +1,17 @@ #include "providers/seventv/SeventvBadges.hpp" -#include "common/NetworkRequest.hpp" -#include "common/NetworkResult.hpp" -#include "common/Outcome.hpp" #include "messages/Emote.hpp" #include "messages/Image.hpp" +#include "providers/seventv/SeventvApi.hpp" #include "providers/seventv/SeventvEmotes.hpp" +#include #include #include #include + namespace chatterino { void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/) @@ -86,21 +86,11 @@ void SeventvBadges::addBadge(const QJsonObject &badgeJson) void SeventvBadges::loadSeventvBadges() { // This endpoint is used as a backup for badges - static QUrl url("https://7tv.io/v2/cosmetics"); - - static QUrlQuery urlQuery; - // valid user_identifier values: "object_id", "twitch_id", "login" - urlQuery.addQueryItem("user_identifier", "twitch_id"); - - url.setQuery(urlQuery); - - NetworkRequest(url) - .onSuccess([this](const NetworkResult &result) -> Outcome { - auto root = result.parseJson(); - + getSeventvApi().getCosmetics( + [this](const auto &json) -> void { std::unique_lock lock(this->mutex_); - for (const auto &jsonBadge : root.value("badges").toArray()) + for (const auto &jsonBadge : json.value("badges").toArray()) { const auto badge = jsonBadge.toObject(); auto badgeID = badge["id"].toString(); @@ -123,10 +113,8 @@ void SeventvBadges::loadSeventvBadges() this->badgeMap_[user.toString()] = emotePtr; } } - - return Success; - }) - .execute(); + }, + [](const auto &) -> void { /* ignored */ }); } } // namespace chatterino diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index cf05b1a1a78..707dc18c1cc 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -1,6 +1,6 @@ #include "providers/seventv/SeventvEmotes.hpp" -#include "common/NetworkRequest.hpp" +#include "common/Literals.hpp" #include "common/NetworkResult.hpp" #include "common/QLogging.hpp" #include "messages/Emote.hpp" @@ -8,6 +8,7 @@ #include "messages/ImageSet.hpp" #include "messages/MessageBuilder.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/SeventvApi.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Settings.hpp" @@ -182,6 +183,7 @@ EmotePtr createUpdatedEmote(const EmotePtr &oldEmote, namespace chatterino { using namespace seventv::eventapi; +using namespace literals; SeventvEmotes::SeventvEmotes() : global_(std::make_shared()) @@ -216,24 +218,21 @@ void SeventvEmotes::loadGlobalEmotes() qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes"; - NetworkRequest(API_URL_GLOBAL_EMOTE_SET, NetworkRequestType::Get) - .timeout(30000) - .onSuccess([this](const NetworkResult &result) -> Outcome { - QJsonArray parsedEmotes = result.parseJson()["emotes"].toArray(); + getSeventvApi().getEmoteSet( + u"global"_s, + [this](const auto &json) { + QJsonArray parsedEmotes = json["emotes"].toArray(); auto emoteMap = parseEmotes(parsedEmotes, true); qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size() << "7TV Global Emotes"; this->setGlobalEmotes( std::make_shared(std::move(emoteMap))); - - return Success; - }) - .onError([](const NetworkResult &result) { + }, + [](const auto &result) { qCWarning(chatterinoSeventv) << "Couldn't load 7TV global emotes" << result.getData(); - }) - .execute(); + }); } void SeventvEmotes::setGlobalEmotes(std::shared_ptr emotes) @@ -248,13 +247,12 @@ void SeventvEmotes::loadChannelEmotes( qCDebug(chatterinoSeventv) << "Reloading 7TV Channel Emotes" << channelId << manualRefresh; - NetworkRequest(API_URL_USER.arg(channelId), NetworkRequestType::Get) - .timeout(20000) - .onSuccess([callback = std::move(callback), channel, channelId, - manualRefresh](const NetworkResult &result) -> Outcome { - auto json = result.parseJson(); - auto emoteSet = json["emote_set"].toObject(); - auto parsedEmotes = emoteSet["emotes"].toArray(); + getSeventvApi().getUserByTwitchID( + channelId, + [callback = std::move(callback), channel, channelId, + manualRefresh](const auto &json) { + const auto emoteSet = json["emote_set"].toObject(); + const auto parsedEmotes = emoteSet["emotes"].toArray(); auto emoteMap = parseEmotes(parsedEmotes, false); bool hasEmotes = !emoteMap.empty(); @@ -285,7 +283,7 @@ void SeventvEmotes::loadChannelEmotes( auto shared = channel.lock(); if (!shared) { - return Success; + return; } if (manualRefresh) @@ -301,40 +299,37 @@ void SeventvEmotes::loadChannelEmotes( makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); } } - return Success; - }) - .onError( - [channelId, channel, manualRefresh](const NetworkResult &result) { - auto shared = channel.lock(); - if (!shared) - { - return; - } - if (result.status() == 404) - { - qCWarning(chatterinoSeventv) - << "Error occurred fetching 7TV emotes: " - << result.parseJson(); - if (manualRefresh) - { - shared->addMessage( - makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); - } - } - else + }, + [channelId, channel, manualRefresh](const auto &result) { + auto shared = channel.lock(); + if (!shared) + { + return; + } + if (result.status() == 404) + { + qCWarning(chatterinoSeventv) + << "Error occurred fetching 7TV emotes: " + << result.parseJson(); + if (manualRefresh) { - // TODO: Auto retry in case of a timeout, with a delay - auto errorString = result.formatError(); - qCWarning(chatterinoSeventv) - << "Error fetching 7TV emotes for channel" << channelId - << ", error" << errorString; - shared->addMessage(makeSystemMessage( - QStringLiteral("Failed to fetch 7TV channel " - "emotes. (Error: %1)") - .arg(errorString))); + shared->addMessage( + makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); } - }) - .execute(); + } + else + { + // TODO: Auto retry in case of a timeout, with a delay + auto errorString = result.formatError(); + qCWarning(chatterinoSeventv) + << "Error fetching 7TV emotes for channel" << channelId + << ", error" << errorString; + shared->addMessage(makeSystemMessage( + QStringLiteral("Failed to fetch 7TV channel " + "emotes. (Error: %1)") + .arg(errorString))); + } + }); } boost::optional SeventvEmotes::addEmote( @@ -414,11 +409,9 @@ void SeventvEmotes::getEmoteSet( { qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId; - NetworkRequest(API_URL_EMOTE_SET.arg(emoteSetId), NetworkRequestType::Get) - .timeout(20000) - .onSuccess([callback = std::move(successCallback), - emoteSetId](const NetworkResult &result) -> Outcome { - auto json = result.parseJson(); + getSeventvApi().getEmoteSet( + emoteSetId, + [callback = std::move(successCallback), emoteSetId](const auto &json) { auto parsedEmotes = json["emotes"].toArray(); auto emoteMap = parseEmotes(parsedEmotes, false); @@ -427,13 +420,10 @@ void SeventvEmotes::getEmoteSet( << "7TV Emotes from" << emoteSetId; callback(std::move(emoteMap), json["name"].toString()); - return Success; - }) - .onError([emoteSetId, callback = std::move(errorCallback)]( - const NetworkResult &result) { + }, + [emoteSetId, callback = std::move(errorCallback)](const auto &result) { callback(result.formatError()); - }) - .execute(); + }); } ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData) diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 3aca2ac73bd..96e595bc548 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -3,7 +3,6 @@ #include "Application.hpp" #include "common/Channel.hpp" #include "common/Env.hpp" -#include "common/NetworkRequest.hpp" #include "common/NetworkResult.hpp" #include "common/Outcome.hpp" #include "common/QLogging.hpp" @@ -12,6 +11,7 @@ #include "messages/MessageBuilder.hpp" #include "providers/irc/IrcMessageBuilder.hpp" #include "providers/IvrApi.hpp" +#include "providers/seventv/SeventvApi.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchUser.hpp" @@ -461,22 +461,20 @@ void TwitchAccount::loadSeventvUserID() return; } - static const QString seventvUserInfoUrl = - QStringLiteral("https://7tv.io/v3/users/twitch/%1"); - - NetworkRequest(seventvUserInfoUrl.arg(this->getUserId()), - NetworkRequestType::Get) - .timeout(20000) - .onSuccess([this](const auto &response) { - const auto json = response.parseJson(); + getSeventvApi().getUserByTwitchID( + this->getUserId(), + [this](const auto &json) { const auto id = json["user"]["id"].toString(); if (!id.isEmpty()) { this->seventvUserID_ = id; } return Success; - }) - .execute(); + }, + [](const auto &result) { + qCDebug(chatterinoSeventv) + << "Failed to load 7TV user-id:" << result.formatError(); + }); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index f9f4eb29c80..abd7d908ef1 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -19,6 +19,7 @@ #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include "providers/RecentMessagesApi.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/SeventvApi.hpp" #include "providers/seventv/SeventvEmotes.hpp" #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/api/Helix.hpp" @@ -1631,20 +1632,9 @@ void TwitchChannel::updateSevenTVActivity() qCDebug(chatterinoSeventv) << "Sending activity in" << this->getName(); - QJsonObject payload; - payload["kind"] = 1; // UserPresenceKindChannel - - QJsonObject data; - data["id"] = this->roomId(); - data["platform"] = "TWITCH"; - - payload["data"] = data; - - NetworkRequest(seventvActivityUrl.arg(currentSeventvUserID), - NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) - .onSuccess([chan = weakOf(this)](const auto &response) { + getSeventvApi().updatePresence( + this->roomId(), currentSeventvUserID, + [chan = weakOf(this)]() { const auto self = std::dynamic_pointer_cast(chan.lock()); if (!self) @@ -1654,8 +1644,11 @@ void TwitchChannel::updateSevenTVActivity() self->nextSeventvActivity_ = QDateTime::currentDateTimeUtc().addSecs(10); return Success; - }) - .execute(); + }, + [](const auto &result) { + qCDebug(chatterinoSeventv) + << "Failed to update 7TV activity:" << result.formatError(); + }); } void TwitchChannel::listenSevenTVCosmetics() From c3dfabfe65f7c6a32d64089773084112f0ca2780 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 13:23:33 +0200 Subject: [PATCH 06/23] fix: small clang-tidy things --- src/providers/seventv/SeventvBadges.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index baff6dab579..975034ba14a 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -11,7 +11,6 @@ #include - namespace chatterino { void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/) @@ -34,7 +33,7 @@ boost::optional SeventvBadges::getBadge(const UserId &id) const void SeventvBadges::assignBadgeToUser(const QString &badgeID, const UserId &userID) { - std::unique_lock lock(this->mutex_); + const std::unique_lock lock(this->mutex_); const auto badgeIt = this->knownBadges_.find(badgeID); if (badgeIt != this->knownBadges_.end()) @@ -46,7 +45,7 @@ void SeventvBadges::assignBadgeToUser(const QString &badgeID, void SeventvBadges::clearBadgeFromUser(const QString &badgeID, const UserId &userID) { - std::unique_lock lock(this->mutex_); + const std::unique_lock lock(this->mutex_); const auto it = this->badgeMap_.find(userID.string); if (it != this->badgeMap_.end() && it->second->id.string == badgeID) @@ -59,7 +58,7 @@ void SeventvBadges::addBadge(const QJsonObject &badgeJson) { const auto badgeID = badgeJson["id"].toString(); - std::unique_lock lock(this->mutex_); + const std::unique_lock lock(this->mutex_); if (this->knownBadges_.find(badgeID) != this->knownBadges_.end()) { From 837e0756854c90406d20126541755edc33f184a7 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 13:26:59 +0200 Subject: [PATCH 07/23] fix: remove unused constants --- src/providers/seventv/SeventvEmotes.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 707dc18c1cc..52874d370f8 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -37,10 +37,6 @@ using namespace seventv::eventapi; const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes."); const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1"); -const QString API_URL_USER("https://7tv.io/v3/users/twitch/%1"); -const QString API_URL_GLOBAL_EMOTE_SET("https://7tv.io/v3/emote-sets/global"); -const QString API_URL_EMOTE_SET("https://7tv.io/v3/emote-sets/%1"); - struct CreateEmoteResult { Emote emote; EmoteId id; From 68cbee4d3034aed616be1c258b0ef4b196e06486 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 13:41:05 +0200 Subject: [PATCH 08/23] fix: old clangtidy --- src/providers/twitch/TwitchChannel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index abd7d908ef1..1bc80ac1f9a 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1262,8 +1262,8 @@ void TwitchChannel::addReplyThread(const std::shared_ptr &thread) this->threads_[thread->rootId()] = thread; } -const std::unordered_map> & - TwitchChannel::threads() const +const std::unordered_map> + &TwitchChannel::threads() const { return this->threads_; } From 6f21f49474323f409dec483c9b9174db82d03096 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 15:44:29 +0200 Subject: [PATCH 09/23] refactor: rename --- src/CMakeLists.txt | 4 ++-- .../seventv/{SeventvApi.cpp => SeventvAPI.cpp} | 14 +++++++------- .../seventv/{SeventvApi.hpp => SeventvAPI.hpp} | 4 ++-- src/providers/seventv/SeventvBadges.cpp | 4 ++-- src/providers/seventv/SeventvEmotes.cpp | 8 ++++---- src/providers/twitch/TwitchAccount.cpp | 4 ++-- src/providers/twitch/TwitchChannel.cpp | 4 ++-- 7 files changed, 21 insertions(+), 21 deletions(-) rename src/providers/seventv/{SeventvApi.cpp => SeventvAPI.cpp} (91%) rename src/providers/seventv/{SeventvApi.hpp => SeventvAPI.hpp} (95%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c78bfc662c..5d15bd0b12e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -280,8 +280,8 @@ set(SOURCE_FILES providers/liveupdates/BasicPubSubManager.hpp providers/liveupdates/BasicPubSubWebsocket.hpp - providers/seventv/SeventvApi.cpp - providers/seventv/SeventvApi.hpp + providers/seventv/SeventvAPI.cpp + providers/seventv/SeventvAPI.hpp providers/seventv/SeventvBadges.cpp providers/seventv/SeventvBadges.hpp providers/seventv/SeventvCosmetics.hpp diff --git a/src/providers/seventv/SeventvApi.cpp b/src/providers/seventv/SeventvAPI.cpp similarity index 91% rename from src/providers/seventv/SeventvApi.cpp rename to src/providers/seventv/SeventvAPI.cpp index 95767577cde..292fd3d9f93 100644 --- a/src/providers/seventv/SeventvApi.cpp +++ b/src/providers/seventv/SeventvAPI.cpp @@ -1,4 +1,4 @@ -#include "providers/seventv/SeventvApi.hpp" +#include "providers/seventv/SeventvAPI.hpp" #include "common/Literals.hpp" #include "common/NetworkRequest.hpp" @@ -21,7 +21,7 @@ const QString API_URL_COSMETICS = // NOLINTBEGIN(readability-convert-member-functions-to-static) namespace chatterino { -void SeventvApi::getUserByTwitchID( +void SeventvAPI::getUserByTwitchID( const QString &twitchID, SuccessCallback &&onSuccess, ErrorCallback &&onError) { @@ -39,7 +39,7 @@ void SeventvApi::getUserByTwitchID( .execute(); } -void SeventvApi::getEmoteSet(const QString &emoteSet, +void SeventvAPI::getEmoteSet(const QString &emoteSet, SuccessCallback &&onSuccess, ErrorCallback &&onError) { @@ -57,7 +57,7 @@ void SeventvApi::getEmoteSet(const QString &emoteSet, .execute(); } -void SeventvApi::getCosmetics(SuccessCallback &&onSuccess, +void SeventvAPI::getCosmetics(SuccessCallback &&onSuccess, ErrorCallback &&onError) { NetworkRequest(API_URL_COSMETICS) @@ -74,7 +74,7 @@ void SeventvApi::getCosmetics(SuccessCallback &&onSuccess, .execute(); } -void SeventvApi::updatePresence(const QString &twitchChannelID, +void SeventvAPI::updatePresence(const QString &twitchChannelID, const QString &seventvUserID, SuccessCallback<> &&onSuccess, ErrorCallback &&onError) @@ -102,9 +102,9 @@ void SeventvApi::updatePresence(const QString &twitchChannelID, .execute(); } -SeventvApi &getSeventvApi() +SeventvAPI &getSeventvAPI() { - static SeventvApi instance; + static SeventvAPI instance; return instance; } diff --git a/src/providers/seventv/SeventvApi.hpp b/src/providers/seventv/SeventvAPI.hpp similarity index 95% rename from src/providers/seventv/SeventvApi.hpp rename to src/providers/seventv/SeventvAPI.hpp index dc115192723..1a44dacc3d8 100644 --- a/src/providers/seventv/SeventvApi.hpp +++ b/src/providers/seventv/SeventvAPI.hpp @@ -9,7 +9,7 @@ namespace chatterino { class NetworkResult; -class SeventvApi +class SeventvAPI { using ErrorCallback = std::function; template @@ -30,6 +30,6 @@ class SeventvApi SuccessCallback<> &&onSuccess, ErrorCallback &&onError); }; -SeventvApi &getSeventvApi(); +SeventvAPI &getSeventvAPI(); } // namespace chatterino diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index 975034ba14a..8313a9fa098 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -2,7 +2,7 @@ #include "messages/Emote.hpp" #include "messages/Image.hpp" -#include "providers/seventv/SeventvApi.hpp" +#include "providers/seventv/SeventvAPI.hpp" #include "providers/seventv/SeventvEmotes.hpp" #include @@ -85,7 +85,7 @@ void SeventvBadges::addBadge(const QJsonObject &badgeJson) void SeventvBadges::loadSeventvBadges() { // This endpoint is used as a backup for badges - getSeventvApi().getCosmetics( + getSeventvAPI().getCosmetics( [this](const auto &json) -> void { std::unique_lock lock(this->mutex_); diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 52874d370f8..6d64cd37a58 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -8,7 +8,7 @@ #include "messages/ImageSet.hpp" #include "messages/MessageBuilder.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" -#include "providers/seventv/SeventvApi.hpp" +#include "providers/seventv/SeventvAPI.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Settings.hpp" @@ -214,7 +214,7 @@ void SeventvEmotes::loadGlobalEmotes() qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes"; - getSeventvApi().getEmoteSet( + getSeventvAPI().getEmoteSet( u"global"_s, [this](const auto &json) { QJsonArray parsedEmotes = json["emotes"].toArray(); @@ -243,7 +243,7 @@ void SeventvEmotes::loadChannelEmotes( qCDebug(chatterinoSeventv) << "Reloading 7TV Channel Emotes" << channelId << manualRefresh; - getSeventvApi().getUserByTwitchID( + getSeventvAPI().getUserByTwitchID( channelId, [callback = std::move(callback), channel, channelId, manualRefresh](const auto &json) { @@ -405,7 +405,7 @@ void SeventvEmotes::getEmoteSet( { qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId; - getSeventvApi().getEmoteSet( + getSeventvAPI().getEmoteSet( emoteSetId, [callback = std::move(successCallback), emoteSetId](const auto &json) { auto parsedEmotes = json["emotes"].toArray(); diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 96e595bc548..dfb33dc9277 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -11,7 +11,7 @@ #include "messages/MessageBuilder.hpp" #include "providers/irc/IrcMessageBuilder.hpp" #include "providers/IvrApi.hpp" -#include "providers/seventv/SeventvApi.hpp" +#include "providers/seventv/SeventvAPI.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchUser.hpp" @@ -461,7 +461,7 @@ void TwitchAccount::loadSeventvUserID() return; } - getSeventvApi().getUserByTwitchID( + getSeventvAPI().getUserByTwitchID( this->getUserId(), [this](const auto &json) { const auto id = json["user"]["id"].toString(); diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 1bc80ac1f9a..458d78529e7 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -19,7 +19,7 @@ #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include "providers/RecentMessagesApi.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" -#include "providers/seventv/SeventvApi.hpp" +#include "providers/seventv/SeventvAPI.hpp" #include "providers/seventv/SeventvEmotes.hpp" #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/api/Helix.hpp" @@ -1632,7 +1632,7 @@ void TwitchChannel::updateSevenTVActivity() qCDebug(chatterinoSeventv) << "Sending activity in" << this->getName(); - getSeventvApi().updatePresence( + getSeventvAPI().updatePresence( this->roomId(), currentSeventvUserID, [chan = weakOf(this)]() { const auto self = From cabbc8e4c34559880a3387f6c6017d3d8af83f71 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 15:47:20 +0200 Subject: [PATCH 10/23] fix: increase interval to 60s --- src/providers/twitch/TwitchChannel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 458d78529e7..af83e13555b 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1642,7 +1642,7 @@ void TwitchChannel::updateSevenTVActivity() return Success; } self->nextSeventvActivity_ = - QDateTime::currentDateTimeUtc().addSecs(10); + QDateTime::currentDateTimeUtc().addSecs(60); return Success; }, [](const auto &result) { From 4c70c57f55486a7fc7c5ef3b38e390ce1c8a0771 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 15:47:36 +0200 Subject: [PATCH 11/23] fix: newline --- src/providers/seventv/SeventvAPI.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/providers/seventv/SeventvAPI.cpp b/src/providers/seventv/SeventvAPI.cpp index 292fd3d9f93..3432803e2f1 100644 --- a/src/providers/seventv/SeventvAPI.cpp +++ b/src/providers/seventv/SeventvAPI.cpp @@ -6,6 +6,7 @@ #include "common/Outcome.hpp" namespace { + using namespace chatterino::literals; const QString API_URL_USER = u"https://7tv.io/v3/users/twitch/%1"_s; From 1888512e1e3187116a6864560befa971364dd89c Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 15:48:02 +0200 Subject: [PATCH 12/23] fix: Twitch --- src/providers/seventv/SeventvEventAPI.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/seventv/SeventvEventAPI.hpp b/src/providers/seventv/SeventvEventAPI.hpp index a07c3821b63..6a482731897 100644 --- a/src/providers/seventv/SeventvEventAPI.hpp +++ b/src/providers/seventv/SeventvEventAPI.hpp @@ -49,7 +49,7 @@ class SeventvEventAPI */ void subscribeUser(const QString &userID, const QString &emoteSetID); /** - * Subscribes to cosmetics and entitlements in a twitch channel + * Subscribes to cosmetics and entitlements in a Twitch channel * if not already subscribed. * * @param id Twitch channel id From 4ee66beb1459b71264853ec1d84f98187f689621 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 2 Jul 2023 15:51:02 +0200 Subject: [PATCH 13/23] docs: add comment --- src/providers/twitch/TwitchChannel.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 9393c66a89e..bd53037c58e 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -226,6 +226,10 @@ class TwitchChannel : public Channel, public ChannelChatters void showLoginMessage(); /** Joins (subscribes to) a Twitch channel for updates on BTTV. */ void joinBttvChannel() const; + /** + * Indicates an activity to 7TV in this channel for this user. + * This is done at most once every 60s. + */ void updateSevenTVActivity(); void listenSevenTVCosmetics(); From 029ac0338f53b3322f060d37d73b1ac85d00875e Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 22 Jul 2023 14:10:33 +0200 Subject: [PATCH 14/23] fix: remove v2 badges endpoint --- src/providers/seventv/SeventvBadges.cpp | 39 ------------------------- src/providers/seventv/SeventvBadges.hpp | 4 --- 2 files changed, 43 deletions(-) diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index 8313a9fa098..024a4569ae9 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -13,11 +13,6 @@ namespace chatterino { -void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/) -{ - this->loadSeventvBadges(); -} - boost::optional SeventvBadges::getBadge(const UserId &id) const { std::shared_lock lock(this->mutex_); @@ -82,38 +77,4 @@ void SeventvBadges::addBadge(const QJsonObject &badgeJson) std::make_shared(std::move(emote)); } -void SeventvBadges::loadSeventvBadges() -{ - // This endpoint is used as a backup for badges - getSeventvAPI().getCosmetics( - [this](const auto &json) -> void { - std::unique_lock lock(this->mutex_); - - for (const auto &jsonBadge : json.value("badges").toArray()) - { - const auto badge = jsonBadge.toObject(); - auto badgeID = badge["id"].toString(); - auto urls = badge["urls"].toArray(); - auto emote = Emote{ - .name = EmoteName{}, - .images = ImageSet{Url{urls[0].toArray()[1].toString()}, - Url{urls[1].toArray()[1].toString()}, - Url{urls[2].toArray()[1].toString()}}, - .tooltip = Tooltip{badge["tooltip"].toString()}, - .homePage = Url{}, - .id = EmoteId{badgeID}, - }; - - auto emotePtr = std::make_shared(std::move(emote)); - this->knownBadges_[badgeID] = emotePtr; - - for (const auto &user : badge["users"].toArray()) - { - this->badgeMap_[user.toString()] = emotePtr; - } - } - }, - [](const auto &) -> void { /* ignored */ }); -} - } // namespace chatterino diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp index 36b8f5cfbc4..592b3bc515d 100644 --- a/src/providers/seventv/SeventvBadges.hpp +++ b/src/providers/seventv/SeventvBadges.hpp @@ -20,8 +20,6 @@ using EmotePtr = std::shared_ptr; class SeventvBadges : public Singleton { public: - void initialize(Settings &settings, Paths &paths) override; - boost::optional getBadge(const UserId &id) const; void addBadge(const QJsonObject &badgeJson); @@ -29,8 +27,6 @@ class SeventvBadges : public Singleton void clearBadgeFromUser(const QString &badgeID, const UserId &userID); private: - void loadSeventvBadges(); - // Mutex for both `badgeMap_` and `knownBadges_` mutable std::shared_mutex mutex_; From 31ab5cd46becb0f698be889e44fc6d537d958943 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 22 Jul 2023 22:10:22 +0200 Subject: [PATCH 15/23] fix: deadlock This is actually really sad. --- benchmarks/src/main.cpp | 4 +++- tests/src/main.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp index 5f8f678709f..5e101c22b87 100644 --- a/benchmarks/src/main.cpp +++ b/benchmarks/src/main.cpp @@ -22,7 +22,9 @@ int main(int argc, char **argv) ::benchmark::RunSpecifiedBenchmarks(); settingsDir.remove(); - QApplication::exit(0); + // This should be QApplication::exit(0); + // but using this will deadlock in ~QHostInfoLookupManager (if the twitch account was changed) + _exit(0); }); return QApplication::exec(); diff --git a/tests/src/main.cpp b/tests/src/main.cpp index c54ea158f5e..6ce8d3396ad 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -38,7 +38,9 @@ int main(int argc, char **argv) chatterino::NetworkManager::deinit(); settingsDir.remove(); - QApplication::exit(res); + // This should be QApplication::exit(res); + // but using this will deadlock in ~QHostInfoLookupManager (if the twitch account was changed) + _exit(res); }); return QApplication::exec(); From 509689c425279d7b7c94bb5a14a162ce1632f813 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 22 Jul 2023 22:11:23 +0200 Subject: [PATCH 16/23] fix: remove api entry --- src/providers/seventv/SeventvAPI.cpp | 21 --------------------- src/providers/seventv/SeventvAPI.hpp | 2 -- 2 files changed, 23 deletions(-) diff --git a/src/providers/seventv/SeventvAPI.cpp b/src/providers/seventv/SeventvAPI.cpp index 3432803e2f1..265c420e103 100644 --- a/src/providers/seventv/SeventvAPI.cpp +++ b/src/providers/seventv/SeventvAPI.cpp @@ -13,10 +13,6 @@ const QString API_URL_USER = u"https://7tv.io/v3/users/twitch/%1"_s; const QString API_URL_EMOTE_SET = u"https://7tv.io/v3/emote-sets/%1"_s; const QString API_URL_PRESENCES = u"https://7tv.io/v3/users/%1/presences"_s; -// V2 -const QString API_URL_COSMETICS = - u"https://7tv.io/v2/cosmetics?user_identifier=twitch_id"_s; - } // namespace // NOLINTBEGIN(readability-convert-member-functions-to-static) @@ -58,23 +54,6 @@ void SeventvAPI::getEmoteSet(const QString &emoteSet, .execute(); } -void SeventvAPI::getCosmetics(SuccessCallback &&onSuccess, - ErrorCallback &&onError) -{ - NetworkRequest(API_URL_COSMETICS) - .timeout(20000) - .onSuccess([callback = std::move(onSuccess)]( - const NetworkResult &result) -> Outcome { - auto json = result.parseJson(); - callback(json); - return Success; - }) - .onError([callback = std::move(onError)](const NetworkResult &result) { - callback(result); - }) - .execute(); -} - void SeventvAPI::updatePresence(const QString &twitchChannelID, const QString &seventvUserID, SuccessCallback<> &&onSuccess, diff --git a/src/providers/seventv/SeventvAPI.hpp b/src/providers/seventv/SeventvAPI.hpp index 1a44dacc3d8..fd75345f525 100644 --- a/src/providers/seventv/SeventvAPI.hpp +++ b/src/providers/seventv/SeventvAPI.hpp @@ -22,8 +22,6 @@ class SeventvAPI void getEmoteSet(const QString &emoteSet, SuccessCallback &&onSuccess, ErrorCallback &&onError); - void getCosmetics(SuccessCallback &&onSuccess, - ErrorCallback &&onError); void updatePresence(const QString &twitchChannelID, const QString &seventvUserID, From 80bfbdc36a89b00ded62b8cd1e904ff177c4f5bf Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 22 Jul 2023 22:16:30 +0200 Subject: [PATCH 17/23] fix: old clang-format --- src/providers/twitch/TwitchChannel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 8726b195299..158c1eac2e4 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1230,8 +1230,8 @@ void TwitchChannel::addReplyThread(const std::shared_ptr &thread) this->threads_[thread->rootId()] = thread; } -const std::unordered_map> & - TwitchChannel::threads() const +const std::unordered_map> + &TwitchChannel::threads() const { return this->threads_; } From 9f4fbbaacd36bb3dd6bf71a7fc1dc344ecacffa4 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 23 Jul 2023 11:48:16 +0200 Subject: [PATCH 18/23] Sort functions in SeventvBadges.hpp/cpp --- src/providers/seventv/SeventvBadges.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp index 592b3bc515d..686402e4a6e 100644 --- a/src/providers/seventv/SeventvBadges.hpp +++ b/src/providers/seventv/SeventvBadges.hpp @@ -22,10 +22,12 @@ class SeventvBadges : public Singleton public: boost::optional getBadge(const UserId &id) const; - void addBadge(const QJsonObject &badgeJson); void assignBadgeToUser(const QString &badgeID, const UserId &userID); + void clearBadgeFromUser(const QString &badgeID, const UserId &userID); + void addBadge(const QJsonObject &badgeJson); + private: // Mutex for both `badgeMap_` and `knownBadges_` mutable std::shared_mutex mutex_; From 4f6ea9d4105c213345ab75c1a379c3519868b301 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 23 Jul 2023 11:51:04 +0200 Subject: [PATCH 19/23] Remove unused vector include --- src/providers/seventv/SeventvBadges.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp index 686402e4a6e..9f7cde1d744 100644 --- a/src/providers/seventv/SeventvBadges.hpp +++ b/src/providers/seventv/SeventvBadges.hpp @@ -10,7 +10,6 @@ #include #include #include -#include namespace chatterino { From fc0c28854094fb44f850f5c13693c11f96df2139 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 23 Jul 2023 11:52:29 +0200 Subject: [PATCH 20/23] Add comments to SeventvBadges.hpp functions --- src/providers/seventv/SeventvBadges.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp index 9f7cde1d744..426417b5259 100644 --- a/src/providers/seventv/SeventvBadges.hpp +++ b/src/providers/seventv/SeventvBadges.hpp @@ -19,12 +19,17 @@ using EmotePtr = std::shared_ptr; class SeventvBadges : public Singleton { public: + // Return the badge, if any, that is assigned to the user boost::optional getBadge(const UserId &id) const; + // Assign the given badge to the user void assignBadgeToUser(const QString &badgeID, const UserId &userID); + // Remove the given badge from the user void clearBadgeFromUser(const QString &badgeID, const UserId &userID); + // Register a new badge + // The json object will contain all information about the badge, like its ID & its images void addBadge(const QJsonObject &badgeJson); private: From 298f6755d9204705bae7fe1b6f4ccc03b0a7a060 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 23 Jul 2023 11:53:05 +0200 Subject: [PATCH 21/23] Rename `addBadge` to `registerBadge` --- src/providers/seventv/SeventvBadges.cpp | 2 +- src/providers/seventv/SeventvBadges.hpp | 4 ++-- src/providers/seventv/SeventvEventAPI.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index 024a4569ae9..e54682baa75 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -49,7 +49,7 @@ void SeventvBadges::clearBadgeFromUser(const QString &badgeID, } } -void SeventvBadges::addBadge(const QJsonObject &badgeJson) +void SeventvBadges::registerBadge(const QJsonObject &badgeJson) { const auto badgeID = badgeJson["id"].toString(); diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp index 426417b5259..a6ed981ee87 100644 --- a/src/providers/seventv/SeventvBadges.hpp +++ b/src/providers/seventv/SeventvBadges.hpp @@ -28,9 +28,9 @@ class SeventvBadges : public Singleton // Remove the given badge from the user void clearBadgeFromUser(const QString &badgeID, const UserId &userID); - // Register a new badge + // Register a new known badge // The json object will contain all information about the badge, like its ID & its images - void addBadge(const QJsonObject &badgeJson); + void registerBadge(const QJsonObject &badgeJson); private: // Mutex for both `badgeMap_` and `knownBadges_` diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index 5c78853f2a5..5b1ebaf77ab 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -353,7 +353,7 @@ void SeventvEventAPI::onCosmeticCreate(const CosmeticCreateDispatch &cosmetic) switch (cosmetic.kind) { case CosmeticKind::Badge: { - badges->addBadge(cosmetic.data); + badges->registerBadge(cosmetic.data); } break; default: From 86d341e048a8cd9b7ad07010fcb30ca7699392f2 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 23 Jul 2023 12:47:26 +0200 Subject: [PATCH 22/23] fix: cleanup eventloop --- benchmarks/src/main.cpp | 13 ++++++++++--- tests/src/main.cpp | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp index 5e101c22b87..32e0636faec 100644 --- a/benchmarks/src/main.cpp +++ b/benchmarks/src/main.cpp @@ -22,9 +22,16 @@ int main(int argc, char **argv) ::benchmark::RunSpecifiedBenchmarks(); settingsDir.remove(); - // This should be QApplication::exit(0); - // but using this will deadlock in ~QHostInfoLookupManager (if the twitch account was changed) - _exit(0); + + // Pick up the last events from the eventloop + // Using a loop to catch events queueing other events (e.g. deletions) + for (size_t i = 0; i < 32; i++) + { + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + + QApplication::exit(0); }); return QApplication::exec(); diff --git a/tests/src/main.cpp b/tests/src/main.cpp index 6ce8d3396ad..91f4b8b8862 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -38,9 +38,16 @@ int main(int argc, char **argv) chatterino::NetworkManager::deinit(); settingsDir.remove(); - // This should be QApplication::exit(res); - // but using this will deadlock in ~QHostInfoLookupManager (if the twitch account was changed) - _exit(res); + + // Pick up the last events from the eventloop + // Using a loop to catch events queueing other events (e.g. deletions) + for (size_t i = 0; i < 32; i++) + { + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + + QApplication::exit(res); }); return QApplication::exec(); From 70a0f94a3cd2142755de5e66e33f625a524417a5 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 23 Jul 2023 12:51:24 +0200 Subject: [PATCH 23/23] ci(test): add timeout --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a1d85e80d2..b89793600cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,6 +98,7 @@ jobs: - name: Test (Ubuntu) if: startsWith(matrix.os, 'ubuntu') + timeout-minutes: 30 run: | docker pull kennethreitz/httpbin docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}