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:
@@ -87,6 +87,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Test (Ubuntu)
|
- name: Test (Ubuntu)
|
||||||
if: startsWith(matrix.os, 'ubuntu')
|
if: startsWith(matrix.os, 'ubuntu')
|
||||||
|
timeout-minutes: 30
|
||||||
run: |
|
run: |
|
||||||
docker pull kennethreitz/httpbin
|
docker pull kennethreitz/httpbin
|
||||||
docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
- Minor: Added a message for when Chatterino joins a channel (#4616)
|
- Minor: Added a message for when Chatterino joins a channel (#4616)
|
||||||
- Minor: Add accelerators to the right click menu for messages (#4705)
|
- Minor: Add accelerators to the right click menu for messages (#4705)
|
||||||
- Minor: Add pin action to usercards and reply threads. (#4692)
|
- 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: 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)
|
- 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)
|
- 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();
|
::benchmark::RunSpecifiedBenchmarks();
|
||||||
|
|
||||||
settingsDir.remove();
|
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);
|
QApplication::exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -283,8 +283,11 @@ set(SOURCE_FILES
|
|||||||
providers/liveupdates/BasicPubSubManager.hpp
|
providers/liveupdates/BasicPubSubManager.hpp
|
||||||
providers/liveupdates/BasicPubSubWebsocket.hpp
|
providers/liveupdates/BasicPubSubWebsocket.hpp
|
||||||
|
|
||||||
|
providers/seventv/SeventvAPI.cpp
|
||||||
|
providers/seventv/SeventvAPI.hpp
|
||||||
providers/seventv/SeventvBadges.cpp
|
providers/seventv/SeventvBadges.cpp
|
||||||
providers/seventv/SeventvBadges.hpp
|
providers/seventv/SeventvBadges.hpp
|
||||||
|
providers/seventv/SeventvCosmetics.hpp
|
||||||
providers/seventv/SeventvEmotes.cpp
|
providers/seventv/SeventvEmotes.cpp
|
||||||
providers/seventv/SeventvEmotes.hpp
|
providers/seventv/SeventvEmotes.hpp
|
||||||
providers/seventv/SeventvEventAPI.cpp
|
providers/seventv/SeventvEventAPI.cpp
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 "providers/seventv/SeventvBadges.hpp"
|
||||||
|
|
||||||
#include "common/NetworkRequest.hpp"
|
|
||||||
#include "common/NetworkResult.hpp"
|
|
||||||
#include "common/Outcome.hpp"
|
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
|
#include "messages/Image.hpp"
|
||||||
|
#include "providers/seventv/SeventvAPI.hpp"
|
||||||
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
|
||||||
@@ -12,66 +13,68 @@
|
|||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/)
|
boost::optional<EmotePtr> SeventvBadges::getBadge(const UserId &id) const
|
||||||
{
|
|
||||||
this->loadSeventvBadges();
|
|
||||||
}
|
|
||||||
|
|
||||||
boost::optional<EmotePtr> SeventvBadges::getBadge(const UserId &id)
|
|
||||||
{
|
{
|
||||||
std::shared_lock lock(this->mutex_);
|
std::shared_lock lock(this->mutex_);
|
||||||
|
|
||||||
auto it = this->badgeMap_.find(id.string);
|
auto it = this->badgeMap_.find(id.string);
|
||||||
if (it != this->badgeMap_.end())
|
if (it != this->badgeMap_.end())
|
||||||
{
|
{
|
||||||
return this->emotes_[it->second];
|
return it->second;
|
||||||
}
|
}
|
||||||
return boost::none;
|
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
|
const std::unique_lock lock(this->mutex_);
|
||||||
// we'll use this endpoint.
|
|
||||||
static QUrl url("https://7tv.io/v2/cosmetics");
|
|
||||||
|
|
||||||
static QUrlQuery urlQuery;
|
const auto badgeIt = this->knownBadges_.find(badgeID);
|
||||||
// valid user_identifier values: "object_id", "twitch_id", "login"
|
if (badgeIt != this->knownBadges_.end())
|
||||||
urlQuery.addQueryItem("user_identifier", "twitch_id");
|
{
|
||||||
|
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)
|
const auto it = this->badgeMap_.find(userID.string);
|
||||||
.onSuccess([this](const NetworkResult &result) -> Outcome {
|
if (it != this->badgeMap_.end() && it->second->id.string == badgeID)
|
||||||
auto root = result.parseJson();
|
{
|
||||||
|
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;
|
const std::unique_lock lock(this->mutex_);
|
||||||
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(
|
if (this->knownBadges_.find(badgeID) != this->knownBadges_.end())
|
||||||
std::make_shared<const Emote>(std::move(emote)));
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto &user : badge.value("users").toArray())
|
auto emote = Emote{
|
||||||
{
|
.name = EmoteName{},
|
||||||
this->badgeMap_[user.toString()] = index;
|
.images = SeventvEmotes::createImageSet(badgeJson),
|
||||||
}
|
.tooltip = Tooltip{badgeJson["tooltip"].toString()},
|
||||||
++index;
|
.homePage = Url{},
|
||||||
}
|
.id = EmoteId{badgeID},
|
||||||
|
};
|
||||||
|
|
||||||
return Success;
|
if (emote.images.getImage1()->isEmpty())
|
||||||
})
|
{
|
||||||
.execute();
|
return; // Bad images
|
||||||
|
}
|
||||||
|
|
||||||
|
this->knownBadges_[badgeID] =
|
||||||
|
std::make_shared<const Emote>(std::move(emote));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#include "util/QStringHash.hpp"
|
#include "util/QStringHash.hpp"
|
||||||
|
|
||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
@@ -19,18 +19,27 @@ using EmotePtr = std::shared_ptr<const Emote>;
|
|||||||
class SeventvBadges : public Singleton
|
class SeventvBadges : public Singleton
|
||||||
{
|
{
|
||||||
public:
|
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:
|
private:
|
||||||
void loadSeventvBadges();
|
// Mutex for both `badgeMap_` and `knownBadges_`
|
||||||
|
mutable std::shared_mutex mutex_;
|
||||||
|
|
||||||
// Mutex for both `badgeMap_` and `emotes_`
|
// user-id => badge
|
||||||
std::shared_mutex mutex_;
|
std::unordered_map<QString, EmotePtr> badgeMap_;
|
||||||
|
// badge-id => badge
|
||||||
std::unordered_map<QString, int> badgeMap_;
|
std::unordered_map<QString, EmotePtr> knownBadges_;
|
||||||
std::vector<EmotePtr> emotes_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -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 "providers/seventv/SeventvEmotes.hpp"
|
||||||
|
|
||||||
#include "common/NetworkRequest.hpp"
|
#include "common/Literals.hpp"
|
||||||
#include "common/NetworkResult.hpp"
|
#include "common/NetworkResult.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "messages/ImageSet.hpp"
|
#include "messages/ImageSet.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||||
|
#include "providers/seventv/SeventvAPI.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "singletons/Settings.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 CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes.");
|
||||||
const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1");
|
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 {
|
struct CreateEmoteResult {
|
||||||
Emote emote;
|
Emote emote;
|
||||||
EmoteId id;
|
EmoteId id;
|
||||||
@@ -77,71 +74,6 @@ bool isZeroWidthRecommended(const QJsonObject &emoteData)
|
|||||||
return flags.has(SeventvEmoteFlag::ZeroWidth);
|
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)
|
Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal)
|
||||||
{
|
{
|
||||||
return Tooltip{QString("%1<br>%2 7TV Emote<br>By: %3")
|
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,
|
? createAliasedTooltip(emoteName.string, baseEmoteName.string,
|
||||||
author.string, isGlobal)
|
author.string, isGlobal)
|
||||||
: createTooltip(emoteName.string, author.string, isGlobal);
|
: createTooltip(emoteName.string, author.string, isGlobal);
|
||||||
auto imageSet = makeImageSet(emoteData);
|
auto imageSet = SeventvEmotes::createImageSet(emoteData);
|
||||||
|
|
||||||
auto emote =
|
auto emote =
|
||||||
Emote({emoteName, imageSet, tooltip,
|
Emote({emoteName, imageSet, tooltip,
|
||||||
@@ -247,6 +179,7 @@ EmotePtr createUpdatedEmote(const EmotePtr &oldEmote,
|
|||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
using namespace seventv::eventapi;
|
using namespace seventv::eventapi;
|
||||||
|
using namespace literals;
|
||||||
|
|
||||||
SeventvEmotes::SeventvEmotes()
|
SeventvEmotes::SeventvEmotes()
|
||||||
: global_(std::make_shared<EmoteMap>())
|
: global_(std::make_shared<EmoteMap>())
|
||||||
@@ -281,24 +214,21 @@ void SeventvEmotes::loadGlobalEmotes()
|
|||||||
|
|
||||||
qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes";
|
qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes";
|
||||||
|
|
||||||
NetworkRequest(API_URL_GLOBAL_EMOTE_SET, NetworkRequestType::Get)
|
getSeventvAPI().getEmoteSet(
|
||||||
.timeout(30000)
|
u"global"_s,
|
||||||
.onSuccess([this](const NetworkResult &result) -> Outcome {
|
[this](const auto &json) {
|
||||||
QJsonArray parsedEmotes = result.parseJson()["emotes"].toArray();
|
QJsonArray parsedEmotes = json["emotes"].toArray();
|
||||||
|
|
||||||
auto emoteMap = parseEmotes(parsedEmotes, true);
|
auto emoteMap = parseEmotes(parsedEmotes, true);
|
||||||
qCDebug(chatterinoSeventv)
|
qCDebug(chatterinoSeventv)
|
||||||
<< "Loaded" << emoteMap.size() << "7TV Global Emotes";
|
<< "Loaded" << emoteMap.size() << "7TV Global Emotes";
|
||||||
this->setGlobalEmotes(
|
this->setGlobalEmotes(
|
||||||
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||||
|
},
|
||||||
return Success;
|
[](const auto &result) {
|
||||||
})
|
|
||||||
.onError([](const NetworkResult &result) {
|
|
||||||
qCWarning(chatterinoSeventv)
|
qCWarning(chatterinoSeventv)
|
||||||
<< "Couldn't load 7TV global emotes" << result.getData();
|
<< "Couldn't load 7TV global emotes" << result.getData();
|
||||||
})
|
});
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SeventvEmotes::setGlobalEmotes(std::shared_ptr<const EmoteMap> emotes)
|
void SeventvEmotes::setGlobalEmotes(std::shared_ptr<const EmoteMap> emotes)
|
||||||
@@ -313,13 +243,12 @@ void SeventvEmotes::loadChannelEmotes(
|
|||||||
qCDebug(chatterinoSeventv)
|
qCDebug(chatterinoSeventv)
|
||||||
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
|
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
|
||||||
|
|
||||||
NetworkRequest(API_URL_USER.arg(channelId), NetworkRequestType::Get)
|
getSeventvAPI().getUserByTwitchID(
|
||||||
.timeout(20000)
|
channelId,
|
||||||
.onSuccess([callback = std::move(callback), channel, channelId,
|
[callback = std::move(callback), channel, channelId,
|
||||||
manualRefresh](const NetworkResult &result) -> Outcome {
|
manualRefresh](const auto &json) {
|
||||||
auto json = result.parseJson();
|
const auto emoteSet = json["emote_set"].toObject();
|
||||||
auto emoteSet = json["emote_set"].toObject();
|
const auto parsedEmotes = emoteSet["emotes"].toArray();
|
||||||
auto parsedEmotes = emoteSet["emotes"].toArray();
|
|
||||||
|
|
||||||
auto emoteMap = parseEmotes(parsedEmotes, false);
|
auto emoteMap = parseEmotes(parsedEmotes, false);
|
||||||
bool hasEmotes = !emoteMap.empty();
|
bool hasEmotes = !emoteMap.empty();
|
||||||
@@ -350,7 +279,7 @@ void SeventvEmotes::loadChannelEmotes(
|
|||||||
auto shared = channel.lock();
|
auto shared = channel.lock();
|
||||||
if (!shared)
|
if (!shared)
|
||||||
{
|
{
|
||||||
return Success;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manualRefresh)
|
if (manualRefresh)
|
||||||
@@ -366,40 +295,37 @@ void SeventvEmotes::loadChannelEmotes(
|
|||||||
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
|
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Success;
|
},
|
||||||
})
|
[channelId, channel, manualRefresh](const auto &result) {
|
||||||
.onError(
|
auto shared = channel.lock();
|
||||||
[channelId, channel, manualRefresh](const NetworkResult &result) {
|
if (!shared)
|
||||||
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)
|
}
|
||||||
{
|
else
|
||||||
qCWarning(chatterinoSeventv)
|
{
|
||||||
<< "Error occurred fetching 7TV emotes: "
|
// TODO: Auto retry in case of a timeout, with a delay
|
||||||
<< result.parseJson();
|
auto errorString = result.formatError();
|
||||||
if (manualRefresh)
|
qCWarning(chatterinoSeventv)
|
||||||
{
|
<< "Error fetching 7TV emotes for channel" << channelId
|
||||||
shared->addMessage(
|
<< ", error" << errorString;
|
||||||
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
|
shared->addMessage(makeSystemMessage(
|
||||||
}
|
QStringLiteral("Failed to fetch 7TV channel "
|
||||||
}
|
"emotes. (Error: %1)")
|
||||||
else
|
.arg(errorString)));
|
||||||
{
|
}
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::optional<EmotePtr> SeventvEmotes::addEmote(
|
boost::optional<EmotePtr> SeventvEmotes::addEmote(
|
||||||
@@ -479,11 +405,9 @@ void SeventvEmotes::getEmoteSet(
|
|||||||
{
|
{
|
||||||
qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId;
|
qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId;
|
||||||
|
|
||||||
NetworkRequest(API_URL_EMOTE_SET.arg(emoteSetId), NetworkRequestType::Get)
|
getSeventvAPI().getEmoteSet(
|
||||||
.timeout(20000)
|
emoteSetId,
|
||||||
.onSuccess([callback = std::move(successCallback),
|
[callback = std::move(successCallback), emoteSetId](const auto &json) {
|
||||||
emoteSetId](const NetworkResult &result) -> Outcome {
|
|
||||||
auto json = result.parseJson();
|
|
||||||
auto parsedEmotes = json["emotes"].toArray();
|
auto parsedEmotes = json["emotes"].toArray();
|
||||||
|
|
||||||
auto emoteMap = parseEmotes(parsedEmotes, false);
|
auto emoteMap = parseEmotes(parsedEmotes, false);
|
||||||
@@ -492,13 +416,74 @@ void SeventvEmotes::getEmoteSet(
|
|||||||
<< "7TV Emotes from" << emoteSetId;
|
<< "7TV Emotes from" << emoteSetId;
|
||||||
|
|
||||||
callback(std::move(emoteMap), json["name"].toString());
|
callback(std::move(emoteMap), json["name"].toString());
|
||||||
return Success;
|
},
|
||||||
})
|
[emoteSetId, callback = std::move(errorCallback)](const auto &result) {
|
||||||
.onError([emoteSetId, callback = std::move(errorCallback)](
|
|
||||||
const NetworkResult &result) {
|
|
||||||
callback(result.formatError());
|
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
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
#include "common/Atomic.hpp"
|
#include "common/Atomic.hpp"
|
||||||
#include "common/FlagsEnum.hpp"
|
#include "common/FlagsEnum.hpp"
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
class ImageSet;
|
||||||
class Channel;
|
class Channel;
|
||||||
|
|
||||||
namespace seventv::eventapi {
|
namespace seventv::eventapi {
|
||||||
struct EmoteAddDispatch;
|
struct EmoteAddDispatch;
|
||||||
struct EmoteUpdateDispatch;
|
struct EmoteUpdateDispatch;
|
||||||
@@ -61,6 +63,20 @@ struct Emote;
|
|||||||
using EmotePtr = std::shared_ptr<const Emote>;
|
using EmotePtr = std::shared_ptr<const Emote>;
|
||||||
class EmoteMap;
|
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
|
class SeventvEmotes final
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -120,6 +136,13 @@ public:
|
|||||||
std::function<void(EmoteMap &&, QString)> successCallback,
|
std::function<void(EmoteMap &&, QString)> successCallback,
|
||||||
std::function<void(QString)> errorCallback);
|
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:
|
private:
|
||||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
#include "providers/seventv/eventapi/Client.hpp"
|
#include "providers/seventv/eventapi/Client.hpp"
|
||||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||||
#include "providers/seventv/eventapi/Message.hpp"
|
#include "providers/seventv/eventapi/Message.hpp"
|
||||||
|
#include "providers/seventv/SeventvBadges.hpp"
|
||||||
|
#include "providers/seventv/SeventvCosmetics.hpp"
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
|
||||||
@@ -10,6 +13,7 @@
|
|||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
using namespace seventv;
|
||||||
using namespace seventv::eventapi;
|
using namespace seventv::eventapi;
|
||||||
|
|
||||||
SeventvEventAPI::SeventvEventAPI(
|
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)
|
void SeventvEventAPI::unsubscribeEmoteSet(const QString &id)
|
||||||
{
|
{
|
||||||
if (this->subscribedEmoteSets_.erase(id) > 0)
|
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(
|
std::shared_ptr<BasicPubSubClient<Subscription>> SeventvEventAPI::createClient(
|
||||||
liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl)
|
liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl)
|
||||||
{
|
{
|
||||||
@@ -144,9 +186,49 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch)
|
|||||||
this->onUserUpdate(dispatch);
|
this->onUserUpdate(dispatch);
|
||||||
}
|
}
|
||||||
break;
|
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: {
|
default: {
|
||||||
qCDebug(chatterinoSeventvEventAPI)
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
<< "Unknown subscription type:" << (int)dispatch.type
|
<< "Unknown subscription type:"
|
||||||
|
<< magic_enum::enum_name(dispatch.type).data()
|
||||||
<< "body:" << dispatch.body;
|
<< "body:" << dispatch.body;
|
||||||
}
|
}
|
||||||
break;
|
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
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ namespace seventv::eventapi {
|
|||||||
struct EmoteUpdateDispatch;
|
struct EmoteUpdateDispatch;
|
||||||
struct EmoteRemoveDispatch;
|
struct EmoteRemoveDispatch;
|
||||||
struct UserConnectionUpdateDispatch;
|
struct UserConnectionUpdateDispatch;
|
||||||
|
struct CosmeticCreateDispatch;
|
||||||
|
struct EntitlementCreateDeleteDispatch;
|
||||||
} // namespace seventv::eventapi
|
} // namespace seventv::eventapi
|
||||||
|
|
||||||
|
class SeventvBadges;
|
||||||
|
|
||||||
class SeventvEventAPI
|
class SeventvEventAPI
|
||||||
: public BasicPubSubManager<seventv::eventapi::Subscription>
|
: public BasicPubSubManager<seventv::eventapi::Subscription>
|
||||||
{
|
{
|
||||||
@@ -44,11 +48,20 @@ public:
|
|||||||
* @param emoteSetID 7TV emote-set-id, may be empty.
|
* @param emoteSetID 7TV emote-set-id, may be empty.
|
||||||
*/
|
*/
|
||||||
void subscribeUser(const QString &userID, const QString &emoteSetID);
|
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 */
|
/** Unsubscribes from a user by its 7TV user id */
|
||||||
void unsubscribeUser(const QString &id);
|
void unsubscribeUser(const QString &id);
|
||||||
/** Unsubscribes from an emote-set by its id */
|
/** Unsubscribes from an emote-set by its id */
|
||||||
void unsubscribeEmoteSet(const QString &id);
|
void unsubscribeEmoteSet(const QString &id);
|
||||||
|
/** Unsubscribes from cosmetics and entitlements in a Twitch channel */
|
||||||
|
void unsubscribeTwitchChannel(const QString &id);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::shared_ptr<BasicPubSubClient<seventv::eventapi::Subscription>>
|
std::shared_ptr<BasicPubSubClient<seventv::eventapi::Subscription>>
|
||||||
@@ -64,11 +77,19 @@ private:
|
|||||||
|
|
||||||
void onEmoteSetUpdate(const seventv::eventapi::Dispatch &dispatch);
|
void onEmoteSetUpdate(const seventv::eventapi::Dispatch &dispatch);
|
||||||
void onUserUpdate(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 */
|
/** emote-set ids */
|
||||||
std::unordered_set<QString> subscribedEmoteSets_;
|
std::unordered_set<QString> subscribedEmoteSets_;
|
||||||
/** user ids */
|
/** user ids */
|
||||||
std::unordered_set<QString> subscribedUsers_;
|
std::unordered_set<QString> subscribedUsers_;
|
||||||
|
/** Twitch channel ids */
|
||||||
|
std::unordered_set<QString> subscribedTwitchChannels_;
|
||||||
std::chrono::milliseconds heartbeatInterval_;
|
std::chrono::milliseconds heartbeatInterval_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace chatterino::seventv::eventapi {
|
namespace chatterino::seventv::eventapi {
|
||||||
@@ -91,4 +93,45 @@ bool UserConnectionUpdateDispatch::validate() const
|
|||||||
!this->emoteSetID.isEmpty();
|
!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
|
} // namespace chatterino::seventv::eventapi
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "providers/seventv/eventapi/Subscription.hpp"
|
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||||
|
#include "providers/seventv/SeventvCosmetics.hpp"
|
||||||
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
@@ -67,4 +68,26 @@ struct UserConnectionUpdateDispatch {
|
|||||||
bool validate() const;
|
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
|
} // namespace chatterino::seventv::eventapi
|
||||||
|
|||||||
@@ -102,4 +102,34 @@ QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition)
|
|||||||
return dbg;
|
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
|
} // namespace chatterino::seventv::eventapi
|
||||||
|
|||||||
@@ -12,9 +12,22 @@ namespace chatterino::seventv::eventapi {
|
|||||||
|
|
||||||
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types
|
// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types
|
||||||
enum class SubscriptionType {
|
enum class SubscriptionType {
|
||||||
|
AnyEmoteSet,
|
||||||
|
CreateEmoteSet,
|
||||||
UpdateEmoteSet,
|
UpdateEmoteSet,
|
||||||
|
|
||||||
UpdateUser,
|
UpdateUser,
|
||||||
|
|
||||||
|
AnyCosmetic,
|
||||||
|
CreateCosmetic,
|
||||||
|
UpdateCosmetic,
|
||||||
|
DeleteCosmetic,
|
||||||
|
|
||||||
|
AnyEntitlement,
|
||||||
|
CreateEntitlement,
|
||||||
|
UpdateEntitlement,
|
||||||
|
DeleteEntitlement,
|
||||||
|
|
||||||
INVALID,
|
INVALID,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,7 +59,19 @@ struct ObjectIDCondition {
|
|||||||
bool operator!=(const ObjectIDCondition &rhs) const;
|
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 {
|
struct Subscription {
|
||||||
bool operator==(const Subscription &rhs) const;
|
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;
|
using chatterino::seventv::eventapi::SubscriptionType;
|
||||||
switch (value)
|
switch (value)
|
||||||
{
|
{
|
||||||
|
case SubscriptionType::AnyEmoteSet:
|
||||||
|
return "emote_set.*";
|
||||||
|
case SubscriptionType::CreateEmoteSet:
|
||||||
|
return "emote_set.create";
|
||||||
case SubscriptionType::UpdateEmoteSet:
|
case SubscriptionType::UpdateEmoteSet:
|
||||||
return "emote_set.update";
|
return "emote_set.update";
|
||||||
case SubscriptionType::UpdateUser:
|
case SubscriptionType::UpdateUser:
|
||||||
return "user.update";
|
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:
|
default:
|
||||||
return default_tag;
|
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 <>
|
template <>
|
||||||
struct hash<chatterino::seventv::eventapi::Subscription> {
|
struct hash<chatterino::seventv::eventapi::Subscription> {
|
||||||
size_t operator()(
|
size_t operator()(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "common/Channel.hpp"
|
#include "common/Channel.hpp"
|
||||||
#include "common/Env.hpp"
|
#include "common/Env.hpp"
|
||||||
#include "common/NetworkRequest.hpp"
|
#include "common/NetworkResult.hpp"
|
||||||
#include "common/Outcome.hpp"
|
#include "common/Outcome.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
#include "providers/irc/IrcMessageBuilder.hpp"
|
#include "providers/irc/IrcMessageBuilder.hpp"
|
||||||
#include "providers/IvrApi.hpp"
|
#include "providers/IvrApi.hpp"
|
||||||
|
#include "providers/seventv/SeventvAPI.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "providers/twitch/TwitchCommon.hpp"
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
#include "singletons/Emotes.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
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ public:
|
|||||||
const QString &getOAuthClient() const;
|
const QString &getOAuthClient() const;
|
||||||
const QString &getUserId() 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();
|
QColor color();
|
||||||
void setColor(QColor color);
|
void setColor(QColor color);
|
||||||
|
|
||||||
@@ -98,6 +104,8 @@ public:
|
|||||||
void autoModAllow(const QString msgID, ChannelPtr channel);
|
void autoModAllow(const QString msgID, ChannelPtr channel);
|
||||||
void autoModDeny(const QString msgID, ChannelPtr channel);
|
void autoModDeny(const QString msgID, ChannelPtr channel);
|
||||||
|
|
||||||
|
void loadSeventvUserID();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString oauthClient_;
|
QString oauthClient_;
|
||||||
QString oauthToken_;
|
QString oauthToken_;
|
||||||
@@ -115,6 +123,8 @@ private:
|
|||||||
// std::map<UserId, TwitchAccountEmoteData> emotes;
|
// std::map<UserId, TwitchAccountEmoteData> emotes;
|
||||||
UniqueAccess<TwitchAccountEmoteData> emotes_;
|
UniqueAccess<TwitchAccountEmoteData> emotes_;
|
||||||
UniqueAccess<std::unordered_map<QString, EmoteMap>> localEmotes_;
|
UniqueAccess<std::unordered_map<QString, EmoteMap>> localEmotes_;
|
||||||
|
|
||||||
|
QString seventvUserID_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ TwitchAccountManager::TwitchAccountManager()
|
|||||||
this->currentUserChanged.connect([this] {
|
this->currentUserChanged.connect([this] {
|
||||||
auto currentUser = this->getCurrent();
|
auto currentUser = this->getCurrent();
|
||||||
currentUser->loadBlocks();
|
currentUser->loadBlocks();
|
||||||
|
currentUser->loadSeventvUserID();
|
||||||
});
|
});
|
||||||
|
|
||||||
this->accounts.itemRemoved.connect([this](const auto &acc) {
|
this->accounts.itemRemoved.connect([this](const auto &acc) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||||
#include "providers/RecentMessagesApi.hpp"
|
#include "providers/RecentMessagesApi.hpp"
|
||||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||||
|
#include "providers/seventv/SeventvAPI.hpp"
|
||||||
#include "providers/seventv/SeventvEmotes.hpp"
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
#include "providers/seventv/SeventvEventAPI.hpp"
|
#include "providers/seventv/SeventvEventAPI.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
|
|
||||||
#include <IrcConnection>
|
#include <IrcConnection>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
@@ -105,6 +107,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||||||
this->refreshBTTVChannelEmotes(false);
|
this->refreshBTTVChannelEmotes(false);
|
||||||
this->refreshSevenTVChannelEmotes(false);
|
this->refreshSevenTVChannelEmotes(false);
|
||||||
this->joinBttvChannel();
|
this->joinBttvChannel();
|
||||||
|
this->listenSevenTVCosmetics();
|
||||||
getIApp()->getTwitchLiveController()->add(
|
getIApp()->getTwitchLiveController()->add(
|
||||||
std::dynamic_pointer_cast<TwitchChannel>(shared_from_this()));
|
std::dynamic_pointer_cast<TwitchChannel>(shared_from_this()));
|
||||||
});
|
});
|
||||||
@@ -248,6 +251,12 @@ TwitchChannel::~TwitchChannel()
|
|||||||
{
|
{
|
||||||
getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId());
|
getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getApp()->twitch->seventvEventAPI)
|
||||||
|
{
|
||||||
|
getApp()->twitch->seventvEventAPI->unsubscribeTwitchChannel(
|
||||||
|
this->roomId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchChannel::initialize()
|
void TwitchChannel::initialize()
|
||||||
@@ -600,6 +609,7 @@ void TwitchChannel::sendMessage(const QString &message)
|
|||||||
|
|
||||||
bool messageSent = false;
|
bool messageSent = false;
|
||||||
this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent);
|
this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent);
|
||||||
|
this->updateSevenTVActivity();
|
||||||
|
|
||||||
if (messageSent)
|
if (messageSent)
|
||||||
{
|
{
|
||||||
@@ -1564,4 +1574,60 @@ boost::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string)
|
|||||||
return boost::none;
|
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
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -254,6 +254,12 @@ private:
|
|||||||
void showLoginMessage();
|
void showLoginMessage();
|
||||||
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
|
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
|
||||||
void joinBttvChannel() const;
|
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
|
* @brief Sets the live status of this Twitch channel
|
||||||
@@ -372,6 +378,12 @@ private:
|
|||||||
*/
|
*/
|
||||||
size_t seventvUserTwitchConnectionIndex_;
|
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"). */
|
/** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */
|
||||||
QString lastLiveUpdateEmotePlatform_;
|
QString lastLiveUpdateEmotePlatform_;
|
||||||
/** The actor name of the last live emote update. */
|
/** The actor name of the last live emote update. */
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ public:
|
|||||||
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
|
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
|
||||||
BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true};
|
BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true};
|
||||||
BoolSetting enableSevenTVEventAPI = {"/emotes/seventv/eventapi", true};
|
BoolSetting enableSevenTVEventAPI = {"/emotes/seventv/eventapi", true};
|
||||||
|
BoolSetting sendSevenTVActivity = {"/emotes/seventv/sendActivity", true};
|
||||||
|
|
||||||
/// Links
|
/// Links
|
||||||
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
|
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
|
||||||
|
|||||||
@@ -560,6 +560,11 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
|||||||
layout.addCheckbox("Show 7TV channel emotes", s.enableSevenTVChannelEmotes);
|
layout.addCheckbox("Show 7TV channel emotes", s.enableSevenTVChannelEmotes);
|
||||||
layout.addCheckbox("Enable 7TV live emote updates (requires restart)",
|
layout.addCheckbox("Enable 7TV live emote updates (requires restart)",
|
||||||
s.enableSevenTVEventAPI);
|
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.addTitle("Streamer Mode");
|
||||||
layout.addDescription(
|
layout.addDescription(
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ int main(int argc, char **argv)
|
|||||||
chatterino::NetworkManager::deinit();
|
chatterino::NetworkManager::deinit();
|
||||||
|
|
||||||
settingsDir.remove();
|
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);
|
QApplication::exit(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user