Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use New 7TV Cosmetics System #4512

Merged
merged 26 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d3bdf76
feat(seventv): use new cosmetics system
Nerixyz Apr 4, 2023
c4cf423
chore: add changelog entry
Nerixyz Apr 4, 2023
a8de55c
fix: old `clang-format`
Nerixyz Apr 4, 2023
5ee024c
Merge branch 'master' into feat/v3-cosmetics
Felanbird Jul 2, 2023
ec60f52
fix: small suggestions pt1
Nerixyz Jul 2, 2023
81c3f62
refactor: add 7tv api wrapper
Nerixyz Jul 2, 2023
c3dfabf
fix: small clang-tidy things
Nerixyz Jul 2, 2023
837e075
fix: remove unused constants
Nerixyz Jul 2, 2023
68cbee4
fix: old clangtidy
Nerixyz Jul 2, 2023
6f21f49
refactor: rename
Nerixyz Jul 2, 2023
cabbc8e
fix: increase interval to 60s
Nerixyz Jul 2, 2023
4c70c57
fix: newline
Nerixyz Jul 2, 2023
1888512
fix: Twitch
Nerixyz Jul 2, 2023
4ee66be
docs: add comment
Nerixyz Jul 2, 2023
029ac03
fix: remove v2 badges endpoint
Nerixyz Jul 22, 2023
25f6c90
Merge remote-tracking branch 'upstream/master' into feat/v3-cosmetics
Nerixyz Jul 22, 2023
31ab5cd
fix: deadlock
Nerixyz Jul 22, 2023
509689c
fix: remove api entry
Nerixyz Jul 22, 2023
80bfbdc
fix: old clang-format
Nerixyz Jul 22, 2023
9f4fbba
Sort functions in SeventvBadges.hpp/cpp
pajlada Jul 23, 2023
4f6ea9d
Remove unused vector include
pajlada Jul 23, 2023
fc0c288
Add comments to SeventvBadges.hpp functions
pajlada Jul 23, 2023
298f675
Rename `addBadge` to `registerBadge`
pajlada Jul 23, 2023
86d341e
fix: cleanup eventloop
Nerixyz Jul 23, 2023
70a0f94
ci(test): add timeout
Nerixyz Jul 23, 2023
cd6a2a6
Merge branch 'master' into feat/v3-cosmetics
pajlada Jul 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680)
- Minor: Added a message for when Chatterino joins a channel (#4616)
- Minor: Add pin action to usercards and reply threads. (#4692)
- Minor: 7TV badges now automatically update upon changing. (#4512)
- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667)
- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677)
- Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675)
Expand Down
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,11 @@ 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
providers/seventv/SeventvEmotes.cpp
providers/seventv/SeventvEmotes.hpp
providers/seventv/SeventvEventAPI.cpp
Expand Down
113 changes: 113 additions & 0 deletions src/providers/seventv/SeventvAPI.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#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<const QJsonObject &> &&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<const QJsonObject &> &&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<const QJsonObject &> &&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)
35 changes: 35 additions & 0 deletions src/providers/seventv/SeventvAPI.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <functional>

class QString;
class QJsonObject;

namespace chatterino {

class NetworkResult;

class SeventvAPI
{
using ErrorCallback = std::function<void(const NetworkResult &)>;
template <typename... T>
using SuccessCallback = std::function<void(T...)>;

public:
void getUserByTwitchID(const QString &twitchID,
SuccessCallback<const QJsonObject &> &&onSuccess,
ErrorCallback &&onError);
void getEmoteSet(const QString &emoteSet,
SuccessCallback<const QJsonObject &> &&onSuccess,
ErrorCallback &&onError);
void getCosmetics(SuccessCallback<const QJsonObject &> &&onSuccess,
ErrorCallback &&onError);

void updatePresence(const QString &twitchChannelID,
const QString &seventvUserID,
SuccessCallback<> &&onSuccess, ErrorCallback &&onError);
};

SeventvAPI &getSeventvAPI();

} // namespace chatterino
116 changes: 79 additions & 37 deletions src/providers/seventv/SeventvBadges.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#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 <QJsonArray>
#include <QUrl>
#include <QUrlQuery>

Expand All @@ -17,61 +18,102 @@ void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/)
this->loadSeventvBadges();
}

boost::optional<EmotePtr> SeventvBadges::getBadge(const UserId &id)
boost::optional<EmotePtr> 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::loadSeventvBadges()
void SeventvBadges::assignBadgeToUser(const QString &badgeID,
const UserId &userID)
{
const 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)
{
// Cosmetics will work differently in v3, until this is ready
// we'll use this endpoint.
static QUrl url("https://7tv.io/v2/cosmetics");
const std::unique_lock lock(this->mutex_);

static QUrlQuery urlQuery;
// valid user_identifier values: "object_id", "twitch_id", "login"
urlQuery.addQueryItem("user_identifier", "twitch_id");
const auto it = this->badgeMap_.find(userID.string);
if (it != this->badgeMap_.end() && it->second->id.string == badgeID)
{
this->badgeMap_.erase(userID.string);
}
}

url.setQuery(urlQuery);
void SeventvBadges::addBadge(const QJsonObject &badgeJson)
pajlada marked this conversation as resolved.
Show resolved Hide resolved
{
const auto badgeID = badgeJson["id"].toString();

NetworkRequest(url)
.onSuccess([this](const NetworkResult &result) -> Outcome {
auto root = result.parseJson();
const 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<const Emote>(std::move(emote));
}

void SeventvBadges::loadSeventvBadges()
Nerixyz marked this conversation as resolved.
Show resolved Hide resolved
{
// This endpoint is used as a backup for badges
getSeventvAPI().getCosmetics(
[this](const auto &json) -> void {
std::unique_lock lock(this->mutex_);

int index = 0;
for (const auto &jsonBadge : root.value("badges").toArray())
for (const auto &jsonBadge : json.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<const Emote>(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<const Emote>(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;
})
.execute();
},
[](const auto &) -> void { /* ignored */ });
}

} // namespace chatterino
17 changes: 12 additions & 5 deletions src/providers/seventv/SeventvBadges.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "util/QStringHash.hpp"

#include <boost/optional.hpp>
#include <QJsonObject>

#include <memory>
#include <shared_mutex>
Expand All @@ -21,16 +22,22 @@ class SeventvBadges : public Singleton
public:
void initialize(Settings &settings, Paths &paths) override;

boost::optional<EmotePtr> getBadge(const UserId &id);
boost::optional<EmotePtr> 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<QString, int> badgeMap_;
std::vector<EmotePtr> emotes_;
// user-id => badge
std::unordered_map<QString, EmotePtr> badgeMap_;
// badge-id => badge
std::unordered_map<QString, EmotePtr> knownBadges_;
};

} // namespace chatterino
35 changes: 35 additions & 0 deletions src/providers/seventv/SeventvCosmetics.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <magic_enum.hpp>

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>(
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;
}
}
Loading