Skip to content

Commit

Permalink
Use New 7TV Cosmetics System (#4512)
Browse files Browse the repository at this point in the history
* feat(seventv): use new cosmetics system

* chore: add changelog entry

* fix: old `clang-format`

* fix: small suggestions pt1

* refactor: add 7tv api wrapper

* fix: small clang-tidy things

* fix: remove unused constants

* fix: old clangtidy

* refactor: rename

* fix: increase interval to 60s

* fix: newline

* fix: Twitch

* docs: add comment

* fix: remove v2 badges endpoint

* fix: deadlock

This is actually really sad.

* fix: remove api entry

* fix: old clang-format

* Sort functions in SeventvBadges.hpp/cpp

* Remove unused vector include

* Add comments to SeventvBadges.hpp functions

* Rename `addBadge` to `registerBadge`

* fix: cleanup eventloop

* ci(test): add timeout

---------

Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com>
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
  • Loading branch information
3 people authored Jul 29, 2023
1 parent 8cfa5e8 commit 33fa3e0
Show file tree
Hide file tree
Showing 25 changed files with 836 additions and 197 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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

0 comments on commit 33fa3e0

Please sign in to comment.