mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Use New 7TV Cosmetics System (#4512)
* 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>
This commit is contained in:
parent
8cfa5e866e
commit
33fa3e0a97
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
@ -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 }}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
92
src/providers/seventv/SeventvAPI.cpp
Normal file
92
src/providers/seventv/SeventvAPI.cpp
Normal file
|
@ -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
src/providers/seventv/SeventvAPI.hpp
Normal file
33
src/providers/seventv/SeventvAPI.hpp
Normal file
|
@ -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
|
|
@ -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>
|
||||
|
||||
|
@ -12,66 +13,68 @@
|
|||
|
||||
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)
|
||||
{
|
||||
// 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 badgeIt = this->knownBadges_.find(badgeID);
|
||||
if (badgeIt != this->knownBadges_.end())
|
||||
{
|
||||
this->badgeMap_[userID.string] = badgeIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
url.setQuery(urlQuery);
|
||||
void SeventvBadges::clearBadgeFromUser(const QString &badgeID,
|
||||
const UserId &userID)
|
||||
{
|
||||
const std::unique_lock lock(this->mutex_);
|
||||
|
||||
NetworkRequest(url)
|
||||
.onSuccess([this](const NetworkResult &result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
const auto it = this->badgeMap_.find(userID.string);
|
||||
if (it != this->badgeMap_.end() && it->second->id.string == badgeID)
|
||||
{
|
||||
this->badgeMap_.erase(userID.string);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_lock lock(this->mutex_);
|
||||
void SeventvBadges::registerBadge(const QJsonObject &badgeJson)
|
||||
{
|
||||
const auto badgeID = badgeJson["id"].toString();
|
||||
|
||||
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{}};
|
||||
const std::unique_lock lock(this->mutex_);
|
||||
|
||||
this->emotes_.push_back(
|
||||
std::make_shared<const Emote>(std::move(emote)));
|
||||
if (this->knownBadges_.find(badgeID) != this->knownBadges_.end())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &user : badge.value("users").toArray())
|
||||
{
|
||||
this->badgeMap_[user.toString()] = index;
|
||||
}
|
||||
++index;
|
||||
}
|
||||
auto emote = Emote{
|
||||
.name = EmoteName{},
|
||||
.images = SeventvEmotes::createImageSet(badgeJson),
|
||||
.tooltip = Tooltip{badgeJson["tooltip"].toString()},
|
||||
.homePage = Url{},
|
||||
.id = EmoteId{badgeID},
|
||||
};
|
||||
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
if (emote.images.getImage1()->isEmpty())
|
||||
{
|
||||
return; // Bad images
|
||||
}
|
||||
|
||||
this->knownBadges_[badgeID] =
|
||||
std::make_shared<const Emote>(std::move(emote));
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
@ -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);
|
||||
|
||||
// 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);
|
||||
|
||||
private:
|
||||
void loadSeventvBadges();
|
||||
// Mutex for both `badgeMap_` and `knownBadges_`
|
||||
mutable std::shared_mutex mutex_;
|
||||
|
||||
// Mutex for both `badgeMap_` and `emotes_`
|
||||
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
src/providers/seventv/SeventvCosmetics.hpp
Normal file
35
src/providers/seventv/SeventvCosmetics.hpp
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
@ -36,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;
|
||||
|
@ -77,71 +74,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<ImagePtr, 3> 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<br>%2 7TV Emote<br>By: %3")
|
||||
|
@ -172,7 +104,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,
|
||||
|
@ -247,6 +179,7 @@ EmotePtr createUpdatedEmote(const EmotePtr &oldEmote,
|
|||
namespace chatterino {
|
||||
|
||||
using namespace seventv::eventapi;
|
||||
using namespace literals;
|
||||
|
||||
SeventvEmotes::SeventvEmotes()
|
||||
: global_(std::make_shared<EmoteMap>())
|
||||
|
@ -281,24 +214,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<EmoteMap>(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<const EmoteMap> emotes)
|
||||
|
@ -313,13 +243,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();
|
||||
|
@ -350,7 +279,7 @@ void SeventvEmotes::loadChannelEmotes(
|
|||
auto shared = channel.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return Success;
|
||||
return;
|
||||
}
|
||||
|
||||
if (manualRefresh)
|
||||
|
@ -366,40 +295,37 @@ void SeventvEmotes::loadChannelEmotes(
|
|||
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
|
||||
}
|
||||
}
|
||||
return Success;
|
||||
})
|
||||
.onError(
|
||||
[channelId, channel, manualRefresh](const NetworkResult &result) {
|
||||
auto shared = channel.lock();
|
||||
if (!shared)
|
||||
},
|
||||
[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)
|
||||
{
|
||||
return;
|
||||
shared->addMessage(
|
||||
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
|
||||
}
|
||||
if (result.status() == 404)
|
||||
{
|
||||
qCWarning(chatterinoSeventv)
|
||||
<< "Error occurred fetching 7TV emotes: "
|
||||
<< result.parseJson();
|
||||
if (manualRefresh)
|
||||
{
|
||||
shared->addMessage(
|
||||
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
|
||||
}
|
||||
}
|
||||
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)));
|
||||
}
|
||||
})
|
||||
.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<EmotePtr> SeventvEmotes::addEmote(
|
||||
|
@ -479,11 +405,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);
|
||||
|
@ -492,13 +416,74 @@ 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)
|
||||
{
|
||||
auto host = emoteData["host"].toObject();
|
||||
// "//cdn.7tv[...]"
|
||||
auto baseUrl = host["url"].toString();
|
||||
auto files = host["files"].toArray();
|
||||
|
||||
std::array<ImagePtr, 3> 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
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
#include "common/Atomic.hpp"
|
||||
#include "common/FlagsEnum.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ImageSet;
|
||||
class Channel;
|
||||
|
||||
namespace seventv::eventapi {
|
||||
struct EmoteAddDispatch;
|
||||
struct EmoteUpdateDispatch;
|
||||
|
@ -61,6 +63,20 @@ struct Emote;
|
|||
using EmotePtr = std::shared_ptr<const Emote>;
|
||||
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<SeventvEmoteSetFlag>;
|
||||
|
||||
class SeventvEmotes final
|
||||
{
|
||||
public:
|
||||
|
@ -120,6 +136,13 @@ public:
|
|||
std::function<void(EmoteMap &&, QString)> successCallback,
|
||||
std::function<void(QString)> errorCallback);
|
||||
|
||||
/**
|
||||
* Creates an image set from a 7TV emote or badge.
|
||||
*
|
||||
* @param emoteData { host: { files: [], url } }
|
||||
*/
|
||||
static ImageSet createImageSet(const QJsonObject &emoteData);
|
||||
|
||||
private:
|
||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||
};
|
||||
|
|
|
@ -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 <QJsonArray>
|
||||
|
||||
|
@ -10,6 +13,7 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace seventv;
|
||||
using namespace seventv::eventapi;
|
||||
|
||||
SeventvEventAPI::SeventvEventAPI(
|
||||
|
@ -35,6 +39,25 @@ 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 +76,25 @@ 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<BasicPubSubClient<Subscription>> SeventvEventAPI::createClient(
|
||||
liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl)
|
||||
{
|
||||
|
@ -144,9 +186,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 +343,59 @@ void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch)
|
|||
}
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
|
||||
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: {
|
||||
badges->registerBadge(cosmetic.data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
badges->assignBadgeToUser(entitlement.refID,
|
||||
UserId{entitlement.userID});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
badges->clearBadgeFromUser(entitlement.refID,
|
||||
UserId{entitlement.userID});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -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<seventv::eventapi::Subscription>
|
||||
{
|
||||
|
@ -44,11 +48,20 @@ public:
|
|||
* @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<BasicPubSubClient<seventv::eventapi::Subscription>>
|
||||
|
@ -64,11 +77,19 @@ private:
|
|||
|
||||
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<QString> subscribedEmoteSets_;
|
||||
/** user ids */
|
||||
std::unordered_set<QString> subscribedUsers_;
|
||||
/** Twitch channel ids */
|
||||
std::unordered_set<QString> subscribedTwitchChannels_;
|
||||
std::chrono::milliseconds heartbeatInterval_;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace chatterino::seventv::eventapi {
|
||||
|
@ -91,4 +93,45 @@ bool UserConnectionUpdateDispatch::validate() const
|
|||
!this->emoteSetID.isEmpty();
|
||||
}
|
||||
|
||||
CosmeticCreateDispatch::CosmeticCreateDispatch(const Dispatch &dispatch)
|
||||
: data(dispatch.body["object"]["data"].toObject())
|
||||
, kind(magic_enum::enum_cast<CosmeticKind>(
|
||||
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->refID = obj["ref_id"].toString();
|
||||
this->kind = magic_enum::enum_cast<CosmeticKind>(
|
||||
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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||
#include "providers/seventv/SeventvCosmetics.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<ObjectIDCondition>;
|
||||
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<ObjectIDCondition, ChannelCondition>;
|
||||
|
||||
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<chatterino::seventv::eventapi::ObjectIDCondition> {
|
|||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<chatterino::seventv::eventapi::ChannelCondition> {
|
||||
size_t operator()(
|
||||
const chatterino::seventv::eventapi::ChannelCondition &c) const
|
||||
{
|
||||
return qHash(c.twitchID);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<chatterino::seventv::eventapi::Subscription> {
|
||||
size_t operator()(
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#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"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
|
@ -12,6 +12,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 "singletons/Emotes.hpp"
|
||||
|
@ -445,4 +446,36 @@ void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel)
|
|||
});
|
||||
}
|
||||
|
||||
const QString &TwitchAccount::getSeventvUserID() const
|
||||
{
|
||||
return this->seventvUserID_;
|
||||
}
|
||||
|
||||
void TwitchAccount::loadSeventvUserID()
|
||||
{
|
||||
if (this->isAnon())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!this->seventvUserID_.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
getSeventvAPI().getUserByTwitchID(
|
||||
this->getUserId(),
|
||||
[this](const auto &json) {
|
||||
const auto id = json["user"]["id"].toString();
|
||||
if (!id.isEmpty())
|
||||
{
|
||||
this->seventvUserID_ = id;
|
||||
}
|
||||
return Success;
|
||||
},
|
||||
[](const auto &result) {
|
||||
qCDebug(chatterinoSeventv)
|
||||
<< "Failed to load 7TV user-id:" << result.formatError();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -59,6 +59,12 @@ public:
|
|||
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);
|
||||
|
||||
|
@ -98,6 +104,8 @@ public:
|
|||
void autoModAllow(const QString msgID, ChannelPtr channel);
|
||||
void autoModDeny(const QString msgID, ChannelPtr channel);
|
||||
|
||||
void loadSeventvUserID();
|
||||
|
||||
private:
|
||||
QString oauthClient_;
|
||||
QString oauthToken_;
|
||||
|
@ -115,6 +123,8 @@ private:
|
|||
// std::map<UserId, TwitchAccountEmoteData> emotes;
|
||||
UniqueAccess<TwitchAccountEmoteData> emotes_;
|
||||
UniqueAccess<std::unordered_map<QString, EmoteMap>> localEmotes_;
|
||||
|
||||
QString seventvUserID_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -20,6 +20,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"
|
||||
|
@ -40,6 +41,7 @@
|
|||
|
||||
#include <IrcConnection>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QThread>
|
||||
|
@ -105,6 +107,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
this->refreshBTTVChannelEmotes(false);
|
||||
this->refreshSevenTVChannelEmotes(false);
|
||||
this->joinBttvChannel();
|
||||
this->listenSevenTVCosmetics();
|
||||
getIApp()->getTwitchLiveController()->add(
|
||||
std::dynamic_pointer_cast<TwitchChannel>(shared_from_this()));
|
||||
});
|
||||
|
@ -248,6 +251,12 @@ TwitchChannel::~TwitchChannel()
|
|||
{
|
||||
getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId());
|
||||
}
|
||||
|
||||
if (getApp()->twitch->seventvEventAPI)
|
||||
{
|
||||
getApp()->twitch->seventvEventAPI->unsubscribeTwitchChannel(
|
||||
this->roomId());
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchChannel::initialize()
|
||||
|
@ -600,6 +609,7 @@ void TwitchChannel::sendMessage(const QString &message)
|
|||
|
||||
bool messageSent = false;
|
||||
this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent);
|
||||
this->updateSevenTVActivity();
|
||||
|
||||
if (messageSent)
|
||||
{
|
||||
|
@ -1564,4 +1574,60 @@ boost::optional<CheerEmote> 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();
|
||||
|
||||
getSeventvAPI().updatePresence(
|
||||
this->roomId(), currentSeventvUserID,
|
||||
[chan = weakOf<Channel>(this)]() {
|
||||
const auto self =
|
||||
std::dynamic_pointer_cast<TwitchChannel>(chan.lock());
|
||||
if (!self)
|
||||
{
|
||||
return Success;
|
||||
}
|
||||
self->nextSeventvActivity_ =
|
||||
QDateTime::currentDateTimeUtc().addSecs(60);
|
||||
return Success;
|
||||
},
|
||||
[](const auto &result) {
|
||||
qCDebug(chatterinoSeventv)
|
||||
<< "Failed to update 7TV activity:" << result.formatError();
|
||||
});
|
||||
}
|
||||
|
||||
void TwitchChannel::listenSevenTVCosmetics()
|
||||
{
|
||||
if (getApp()->twitch->seventvEventAPI)
|
||||
{
|
||||
getApp()->twitch->seventvEventAPI->subscribeTwitchChannel(
|
||||
this->roomId());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -254,6 +254,12 @@ private:
|
|||
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();
|
||||
|
||||
/**
|
||||
* @brief Sets the live status of this Twitch channel
|
||||
|
@ -372,6 +378,12 @@ private:
|
|||
*/
|
||||
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. */
|
||||
|
|
|
@ -268,6 +268,7 @@ public:
|
|||
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};
|
||||
|
|
|
@ -560,6 +560,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(
|
||||
|
|
|
@ -38,6 +38,15 @@ int main(int argc, char **argv)
|
|||
chatterino::NetworkManager::deinit();
|
||||
|
||||
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(res);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue