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 all 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 .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Minor: Added a message for when Chatterino joins a channel (#4616)
- Minor: Add accelerators to the right click menu for messages (#4705)
- Minor: Add pin action to usercards and reply threads. (#4692)
- Minor: 7TV badges now automatically update upon changing. (#4512)
- Minor: Stream status requests are now batched. (#4713)
- Minor: Added `/c2-theme-autoreload` command to automatically reload a custom theme. This is useful for when you're developing your own theme. (#4718)
- Bugfix: Increased amount of blocked users loaded from 100 to 1,000. (#4721)
Expand Down
9 changes: 9 additions & 0 deletions benchmarks/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ int main(int argc, char **argv)
::benchmark::RunSpecifiedBenchmarks();

settingsDir.remove();

// 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);
});

Expand Down
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,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
92 changes: 92 additions & 0 deletions src/providers/seventv/SeventvAPI.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#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;

} // 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::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)
33 changes: 33 additions & 0 deletions src/providers/seventv/SeventvAPI.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#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 updatePresence(const QString &twitchChannelID,
const QString &seventvUserID,
SuccessCallback<> &&onSuccess, ErrorCallback &&onError);
};

SeventvAPI &getSeventvAPI();

} // namespace chatterino
107 changes: 55 additions & 52 deletions src/providers/seventv/SeventvBadges.cpp
Original file line number Diff line number Diff line change
@@ -1,77 +1,80 @@
#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>

#include <map>

namespace chatterino {

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)
{
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)
{
this->badgeMap_.erase(userID.string);
}
}

void SeventvBadges::registerBadge(const QJsonObject &badgeJson)
{
// Cosmetics will work differently in v3, until this is ready
// we'll use this endpoint.
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();

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<const Emote>(std::move(emote)));

for (const auto &user : badge.value("users").toArray())
{
this->badgeMap_[user.toString()] = index;
}
++index;
}

return Success;
})
.execute();
const auto badgeID = badgeJson["id"].toString();

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));
}

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

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

#include <memory>
#include <shared_mutex>
#include <unordered_map>
#include <vector>

namespace chatterino {

Expand All @@ -19,18 +19,27 @@ using EmotePtr = std::shared_ptr<const Emote>;
class SeventvBadges : public Singleton
{
public:
void initialize(Settings &settings, Paths &paths) override;
// Return the badge, if any, that is assigned to the user
boost::optional<EmotePtr> getBadge(const UserId &id) const;

boost::optional<EmotePtr> getBadge(const UserId &id);
// Assign the given badge to the user
void assignBadgeToUser(const QString &badgeID, const UserId &userID);

private:
void loadSeventvBadges();
// Remove the given badge from the user
void clearBadgeFromUser(const QString &badgeID, const UserId &userID);

// Register a new known badge
// The json object will contain all information about the badge, like its ID & its images
void registerBadge(const QJsonObject &badgeJson);

// Mutex for both `badgeMap_` and `emotes_`
std::shared_mutex mutex_;
private:
// 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
Loading