mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Twitch API: v5 to Helix migration (#1560)
There's a document in src/providers/twitch/api which describes how we interact with the Twitch API. Keeping this up to date might be a healthy way for us to ensure we keep using the right APIs for the right job.
This commit is contained in:
parent
2e39dd4d9b
commit
9a8b85e338
|
@ -173,14 +173,14 @@ SOURCES += \
|
|||
src/providers/irc/IrcConnection2.cpp \
|
||||
src/providers/irc/IrcServer.cpp \
|
||||
src/providers/LinkResolver.cpp \
|
||||
src/providers/twitch/api/Helix.cpp \
|
||||
src/providers/twitch/api/Kraken.cpp \
|
||||
src/providers/twitch/IrcMessageHandler.cpp \
|
||||
src/providers/twitch/PartialTwitchUser.cpp \
|
||||
src/providers/twitch/PubsubActions.cpp \
|
||||
src/providers/twitch/PubsubClient.cpp \
|
||||
src/providers/twitch/PubsubHelpers.cpp \
|
||||
src/providers/twitch/TwitchAccount.cpp \
|
||||
src/providers/twitch/TwitchAccountManager.cpp \
|
||||
src/providers/twitch/TwitchApi.cpp \
|
||||
src/providers/twitch/TwitchBadge.cpp \
|
||||
src/providers/twitch/TwitchBadges.cpp \
|
||||
src/providers/twitch/TwitchChannel.cpp \
|
||||
|
@ -372,15 +372,15 @@ HEADERS += \
|
|||
src/providers/irc/IrcConnection2.hpp \
|
||||
src/providers/irc/IrcServer.hpp \
|
||||
src/providers/LinkResolver.hpp \
|
||||
src/providers/twitch/api/Helix.hpp \
|
||||
src/providers/twitch/api/Kraken.hpp \
|
||||
src/providers/twitch/EmoteValue.hpp \
|
||||
src/providers/twitch/IrcMessageHandler.hpp \
|
||||
src/providers/twitch/PartialTwitchUser.hpp \
|
||||
src/providers/twitch/PubsubActions.hpp \
|
||||
src/providers/twitch/PubsubClient.hpp \
|
||||
src/providers/twitch/PubsubHelpers.hpp \
|
||||
src/providers/twitch/TwitchAccount.hpp \
|
||||
src/providers/twitch/TwitchAccountManager.hpp \
|
||||
src/providers/twitch/TwitchApi.hpp \
|
||||
src/providers/twitch/TwitchBadge.hpp \
|
||||
src/providers/twitch/TwitchBadges.hpp \
|
||||
src/providers/twitch/TwitchChannel.hpp \
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
#include "providers/twitch/TwitchApi.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
@ -364,18 +364,17 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
return "";
|
||||
}
|
||||
|
||||
TwitchApi::findUserId(
|
||||
target, [user, channel, target](QString userId) {
|
||||
if (userId.isEmpty())
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
return;
|
||||
}
|
||||
user->followUser(userId, [channel, target]() {
|
||||
getHelix()->getUserByName(
|
||||
target,
|
||||
[user, channel, target](const auto &targetUser) {
|
||||
user->followUser(targetUser.id, [channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"You successfully followed " + target));
|
||||
});
|
||||
},
|
||||
[channel, target] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
});
|
||||
|
||||
return "";
|
||||
|
@ -400,18 +399,17 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
return "";
|
||||
}
|
||||
|
||||
TwitchApi::findUserId(
|
||||
target, [user, channel, target](QString userId) {
|
||||
if (userId.isEmpty())
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
return;
|
||||
}
|
||||
user->unfollowUser(userId, [channel, target]() {
|
||||
getHelix()->getUserByName(
|
||||
target,
|
||||
[user, channel, target](const auto &targetUser) {
|
||||
user->unfollowUser(targetUser.id, [channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"You successfully unfollowed " + target));
|
||||
});
|
||||
},
|
||||
[channel, target] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
});
|
||||
|
||||
return "";
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
#include "common/NetworkRequest.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "controllers/notifications/NotificationModel.hpp"
|
||||
#include "providers/twitch/TwitchApi.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "singletons/Toasts.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
@ -142,68 +142,52 @@ void NotificationController::fetchFakeChannels()
|
|||
void NotificationController::getFakeTwitchChannelLiveStatus(
|
||||
const QString &channelName)
|
||||
{
|
||||
TwitchApi::findUserId(channelName, [channelName, this](QString roomID) {
|
||||
if (roomID.isEmpty())
|
||||
{
|
||||
getHelix()->getStreamByName(
|
||||
channelName,
|
||||
[channelName, this](bool live, const auto &stream) {
|
||||
qDebug() << "[TwitchChannel" << channelName
|
||||
<< "] Refreshing live status";
|
||||
|
||||
if (!live)
|
||||
{
|
||||
// Stream is offline
|
||||
this->removeFakeChannel(channelName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream is online
|
||||
auto i = std::find(fakeTwitchChannels.begin(),
|
||||
fakeTwitchChannels.end(), channelName);
|
||||
|
||||
if (i != fakeTwitchChannels.end())
|
||||
{
|
||||
// We have already pushed the live state of this stream
|
||||
// Could not find stream in fake twitch channels!
|
||||
return;
|
||||
}
|
||||
|
||||
if (Toasts::isEnabled())
|
||||
{
|
||||
getApp()->toasts->sendChannelNotification(channelName,
|
||||
Platform::Twitch);
|
||||
}
|
||||
if (getSettings()->notificationPlaySound)
|
||||
{
|
||||
getApp()->notifications->playSound();
|
||||
}
|
||||
if (getSettings()->notificationFlashTaskbar)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
|
||||
// Indicate that we have pushed notifications for this stream
|
||||
fakeTwitchChannels.push_back(channelName);
|
||||
},
|
||||
[channelName, this] {
|
||||
qDebug() << "[TwitchChannel" << channelName
|
||||
<< "] Refreshing live status (Missing ID)";
|
||||
removeFakeChannel(channelName);
|
||||
return;
|
||||
}
|
||||
qDebug() << "[TwitchChannel" << channelName
|
||||
<< "] Refreshing live status";
|
||||
|
||||
QString url("https://api.twitch.tv/kraken/streams/" + roomID);
|
||||
NetworkRequest::twitchRequest(url)
|
||||
.onSuccess([this, channelName](auto result) -> Outcome {
|
||||
rapidjson::Document document = result.parseRapidJson();
|
||||
if (!document.IsObject())
|
||||
{
|
||||
qDebug() << "[TwitchChannel:refreshLiveStatus] root is not "
|
||||
"an object";
|
||||
return Failure;
|
||||
}
|
||||
|
||||
if (!document.HasMember("stream"))
|
||||
{
|
||||
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing "
|
||||
"stream in root";
|
||||
return Failure;
|
||||
}
|
||||
|
||||
const auto &stream = document["stream"];
|
||||
|
||||
if (!stream.IsObject())
|
||||
{
|
||||
// Stream is offline (stream is most likely null)
|
||||
// removeFakeChannel(channelName);
|
||||
return Failure;
|
||||
}
|
||||
// Stream is live
|
||||
auto i = std::find(fakeTwitchChannels.begin(),
|
||||
fakeTwitchChannels.end(), channelName);
|
||||
|
||||
if (!(i != fakeTwitchChannels.end()))
|
||||
{
|
||||
fakeTwitchChannels.push_back(channelName);
|
||||
if (Toasts::isEnabled())
|
||||
{
|
||||
getApp()->toasts->sendChannelNotification(
|
||||
channelName, Platform::Twitch);
|
||||
}
|
||||
if (getSettings()->notificationPlaySound)
|
||||
{
|
||||
getApp()->notifications->playSound();
|
||||
}
|
||||
if (getSettings()->notificationFlashTaskbar)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
}
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
this->removeFakeChannel(channelName);
|
||||
});
|
||||
}
|
||||
|
||||
void NotificationController::removeFakeChannel(const QString channelName)
|
||||
|
|
|
@ -43,6 +43,7 @@ private:
|
|||
void removeFakeChannel(const QString channelName);
|
||||
void getFakeTwitchChannelLiveStatus(const QString &channelName);
|
||||
|
||||
// fakeTwitchChannels is a list of streams who are live that we have already sent out a notification for
|
||||
std::vector<QString> fakeTwitchChannels;
|
||||
QTimer *liveStatusTimer_;
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
#include "common/Args.hpp"
|
||||
#include "common/Modes.hpp"
|
||||
#include "common/Version.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/IncognitoBrowser.hpp"
|
||||
|
@ -36,6 +38,9 @@ int main(int argc, char **argv)
|
|||
}
|
||||
else
|
||||
{
|
||||
Helix::initialize();
|
||||
Kraken::initialize();
|
||||
|
||||
Paths *paths{};
|
||||
|
||||
try
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
#include "providers/twitch/PartialTwitchUser.hpp"
|
||||
|
||||
#include "common/Common.hpp"
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <cassert>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
PartialTwitchUser PartialTwitchUser::byName(const QString &username)
|
||||
{
|
||||
PartialTwitchUser user;
|
||||
user.username_ = username;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
PartialTwitchUser PartialTwitchUser::byId(const QString &id)
|
||||
{
|
||||
PartialTwitchUser user;
|
||||
user.id_ = id;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
void PartialTwitchUser::getId(std::function<void(QString)> successCallback,
|
||||
const QObject *caller)
|
||||
{
|
||||
getId(
|
||||
successCallback, [] {}, caller);
|
||||
}
|
||||
void PartialTwitchUser::getId(std::function<void(QString)> successCallback,
|
||||
std::function<void()> failureCallback,
|
||||
const QObject *caller)
|
||||
{
|
||||
assert(!this->username_.isEmpty());
|
||||
|
||||
NetworkRequest("https://api.twitch.tv/kraken/users?login=" +
|
||||
this->username_)
|
||||
.caller(caller)
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
if (!root.value("users").isArray())
|
||||
{
|
||||
qDebug()
|
||||
<< "API Error while getting user id, users is not an array";
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
|
||||
auto users = root.value("users").toArray();
|
||||
if (users.size() != 1)
|
||||
{
|
||||
qDebug() << "API Error while getting user id, users array size "
|
||||
"is not 1";
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
if (!users[0].isObject())
|
||||
{
|
||||
qDebug() << "API Error while getting user id, first user is "
|
||||
"not an object";
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
auto firstUser = users[0].toObject();
|
||||
auto id = firstUser.value("_id");
|
||||
if (!id.isString())
|
||||
{
|
||||
qDebug() << "API Error: while getting user id, first user "
|
||||
"object `_id` key is not a string";
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
successCallback(id.toString());
|
||||
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,30 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// Experimental class to test a method of calling APIs on twitch users
|
||||
class PartialTwitchUser
|
||||
{
|
||||
PartialTwitchUser() = default;
|
||||
|
||||
QString username_;
|
||||
QString id_;
|
||||
|
||||
public:
|
||||
static PartialTwitchUser byName(const QString &username);
|
||||
static PartialTwitchUser byId(const QString &id);
|
||||
|
||||
void getId(std::function<void(QString)> successCallback,
|
||||
const QObject *caller = nullptr);
|
||||
|
||||
void getId(std::function<void(QString)> successCallback,
|
||||
std::function<void()> failureCallback,
|
||||
const QObject *caller = nullptr);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -6,8 +6,8 @@
|
|||
#include "common/Env.hpp"
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "providers/twitch/PartialTwitchUser.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
||||
|
@ -151,12 +151,14 @@ void TwitchAccount::ignore(
|
|||
const QString &targetName,
|
||||
std::function<void(IgnoreResult, const QString &)> onFinished)
|
||||
{
|
||||
const auto onIdFetched = [this, targetName,
|
||||
onFinished](QString targetUserId) {
|
||||
this->ignoreByID(targetUserId, targetName, onFinished); //
|
||||
const auto onUserFetched = [this, targetName,
|
||||
onFinished](const auto &user) {
|
||||
this->ignoreByID(user.id, targetName, onFinished); //
|
||||
};
|
||||
|
||||
PartialTwitchUser::byName(targetName).getId(onIdFetched);
|
||||
const auto onUserFetchFailed = [] {};
|
||||
|
||||
getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed);
|
||||
}
|
||||
|
||||
void TwitchAccount::ignoreByID(
|
||||
|
@ -226,12 +228,14 @@ void TwitchAccount::unignore(
|
|||
const QString &targetName,
|
||||
std::function<void(UnignoreResult, const QString &message)> onFinished)
|
||||
{
|
||||
const auto onIdFetched = [this, targetName,
|
||||
onFinished](QString targetUserId) {
|
||||
this->unignoreByID(targetUserId, targetName, onFinished); //
|
||||
const auto onUserFetched = [this, targetName,
|
||||
onFinished](const auto &user) {
|
||||
this->unignoreByID(user.id, targetName, onFinished); //
|
||||
};
|
||||
|
||||
PartialTwitchUser::byName(targetName).getId(onIdFetched);
|
||||
const auto onUserFetchFailed = [] {};
|
||||
|
||||
getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed);
|
||||
}
|
||||
|
||||
void TwitchAccount::unignoreByID(
|
||||
|
@ -270,28 +274,18 @@ void TwitchAccount::unignoreByID(
|
|||
void TwitchAccount::checkFollow(const QString targetUserID,
|
||||
std::function<void(FollowResult)> onFinished)
|
||||
{
|
||||
QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() +
|
||||
"/follows/channels/" + targetUserID);
|
||||
const auto onResponse = [onFinished](bool following, const auto &record) {
|
||||
if (!following)
|
||||
{
|
||||
onFinished(FollowResult_NotFollowing);
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkRequest(url)
|
||||
onFinished(FollowResult_Following);
|
||||
};
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](NetworkResult result) {
|
||||
if (result.status() == 203)
|
||||
{
|
||||
onFinished(FollowResult_NotFollowing);
|
||||
}
|
||||
else
|
||||
{
|
||||
onFinished(FollowResult_Failed);
|
||||
}
|
||||
})
|
||||
.onSuccess([=](auto result) -> Outcome {
|
||||
auto document = result.parseRapidJson();
|
||||
onFinished(FollowResult_Following);
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
getHelix()->getUserFollow(this->getUserId(), targetUserID, onResponse,
|
||||
[] {});
|
||||
}
|
||||
|
||||
void TwitchAccount::followUser(const QString userID,
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include "common/Common.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -141,6 +143,8 @@ void TwitchAccountManager::load()
|
|||
if (user)
|
||||
{
|
||||
qDebug() << "Twitch user updated to" << newUsername;
|
||||
getHelix()->update(user->getOAuthClient(), user->getOAuthToken());
|
||||
getKraken()->update(user->getOAuthClient(), user->getOAuthToken());
|
||||
this->currentUser_ = user;
|
||||
}
|
||||
else
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
#include "providers/twitch/TwitchApi.hpp"
|
||||
|
||||
#include "common/Common.hpp"
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QThread>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void TwitchApi::findUserId(const QString user,
|
||||
std::function<void(QString)> successCallback)
|
||||
{
|
||||
QString requestUrl("https://api.twitch.tv/kraken/users?login=" + user);
|
||||
|
||||
NetworkRequest(requestUrl)
|
||||
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
if (!root.value("users").isArray())
|
||||
{
|
||||
qDebug()
|
||||
<< "API Error while getting user id, users is not an array";
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
auto users = root.value("users").toArray();
|
||||
if (users.size() != 1)
|
||||
{
|
||||
qDebug() << "API Error while getting user id, users array size "
|
||||
"is not 1";
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
if (!users[0].isObject())
|
||||
{
|
||||
qDebug() << "API Error while getting user id, first user is "
|
||||
"not an object";
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
auto firstUser = users[0].toObject();
|
||||
auto id = firstUser.value("_id");
|
||||
if (!id.isString())
|
||||
{
|
||||
qDebug() << "API Error: while getting user id, first user "
|
||||
"object `_id` key is not a string";
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
successCallback(id.toString());
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
void TwitchApi::findUserName(const QString userid,
|
||||
std::function<void(QString)> successCallback)
|
||||
{
|
||||
QString requestUrl("https://api.twitch.tv/kraken/users/" + userid);
|
||||
|
||||
NetworkRequest(requestUrl)
|
||||
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto name = root.value("name");
|
||||
if (!name.isString())
|
||||
{
|
||||
qDebug() << "API Error: while getting user name, `name` is not "
|
||||
"a string";
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
successCallback(name.toString());
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,19 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchApi
|
||||
{
|
||||
public:
|
||||
static void findUserId(const QString user,
|
||||
std::function<void(QString)> callback);
|
||||
static void findUserName(const QString userid,
|
||||
std::function<void(QString)> callback);
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -14,6 +14,8 @@
|
|||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "providers/twitch/TwitchParseCheerEmotes.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Toasts.hpp"
|
||||
|
@ -21,6 +23,7 @@
|
|||
#include "util/PostToThread.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <IrcConnection>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
@ -454,35 +457,25 @@ void TwitchChannel::refreshTitle()
|
|||
}
|
||||
this->titleRefreshedTime_ = QTime::currentTime();
|
||||
|
||||
QString url("https://api.twitch.tv/kraken/channels/" + roomID);
|
||||
NetworkRequest::twitchRequest(url)
|
||||
.onSuccess(
|
||||
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
||||
ChannelPtr shared = weak.lock();
|
||||
if (!shared)
|
||||
return Failure;
|
||||
const auto onSuccess = [this,
|
||||
weak = weakOf<Channel>(this)](const auto &channel) {
|
||||
ChannelPtr shared = weak.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto document = result.parseRapidJson();
|
||||
{
|
||||
auto status = this->streamStatus_.access();
|
||||
status->title = channel.status;
|
||||
}
|
||||
|
||||
auto statusIt = document.FindMember("status");
|
||||
this->liveStatusChanged.invoke();
|
||||
};
|
||||
|
||||
if (statusIt == document.MemberEnd())
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
const auto onFailure = [] {};
|
||||
|
||||
{
|
||||
auto status = this->streamStatus_.access();
|
||||
if (!rj::getSafe(statusIt->value, status->title))
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
}
|
||||
|
||||
this->liveStatusChanged.invoke();
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
getKraken()->getChannel(roomID, onSuccess, onFailure);
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshLiveStatus()
|
||||
|
@ -497,106 +490,73 @@ void TwitchChannel::refreshLiveStatus()
|
|||
return;
|
||||
}
|
||||
|
||||
QString url("https://api.twitch.tv/kraken/streams/" + roomID);
|
||||
getHelix()->getStreamById(
|
||||
roomID,
|
||||
[this, weak = weakOf<Channel>(this)](bool live, const auto &stream) {
|
||||
ChannelPtr shared = weak.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// auto request = makeGetStreamRequest(roomID, QThread::currentThread());
|
||||
NetworkRequest::twitchRequest(url)
|
||||
|
||||
.onSuccess(
|
||||
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
||||
ChannelPtr shared = weak.lock();
|
||||
if (!shared)
|
||||
return Failure;
|
||||
|
||||
return this->parseLiveStatus(result.parseRapidJson());
|
||||
})
|
||||
.execute();
|
||||
this->parseLiveStatus(live, stream);
|
||||
},
|
||||
[] {
|
||||
// failure
|
||||
});
|
||||
}
|
||||
|
||||
Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
|
||||
void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream)
|
||||
{
|
||||
if (!document.IsObject())
|
||||
if (!live)
|
||||
{
|
||||
qDebug() << "[TwitchChannel:refreshLiveStatus] root is not an object";
|
||||
return Failure;
|
||||
}
|
||||
|
||||
if (!document.HasMember("stream"))
|
||||
{
|
||||
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing stream in root";
|
||||
return Failure;
|
||||
}
|
||||
|
||||
const auto &stream = document["stream"];
|
||||
|
||||
if (!stream.IsObject())
|
||||
{
|
||||
// Stream is offline (stream is most likely null)
|
||||
this->setLive(false);
|
||||
return Failure;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stream.HasMember("viewers") || !stream.HasMember("game") ||
|
||||
!stream.HasMember("channel") || !stream.HasMember("created_at"))
|
||||
{
|
||||
qDebug()
|
||||
<< "[TwitchChannel:refreshLiveStatus] Missing members in stream";
|
||||
this->setLive(false);
|
||||
return Failure;
|
||||
}
|
||||
|
||||
const rapidjson::Value &streamChannel = stream["channel"];
|
||||
|
||||
if (!streamChannel.IsObject() || !streamChannel.HasMember("status"))
|
||||
{
|
||||
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing member "
|
||||
"\"status\" in channel";
|
||||
return Failure;
|
||||
}
|
||||
|
||||
// Stream is live
|
||||
|
||||
{
|
||||
auto status = this->streamStatus_.access();
|
||||
status->viewerCount = stream["viewers"].GetUint();
|
||||
status->game = stream["game"].GetString();
|
||||
status->title = streamChannel["status"].GetString();
|
||||
QDateTime since = QDateTime::fromString(
|
||||
stream["created_at"].GetString(), Qt::ISODate);
|
||||
status->viewerCount = stream.viewerCount;
|
||||
if (status->gameId != stream.gameId)
|
||||
{
|
||||
status->gameId = stream.gameId;
|
||||
|
||||
// Resolve game ID to game name
|
||||
getHelix()->getGameById(
|
||||
stream.gameId,
|
||||
[this, weak = weakOf<Channel>(this)](const auto &game) {
|
||||
ChannelPtr shared = weak.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
auto status = this->streamStatus_.access();
|
||||
status->game = game.name;
|
||||
}
|
||||
|
||||
this->liveStatusChanged.invoke();
|
||||
},
|
||||
[] {
|
||||
// failure
|
||||
});
|
||||
}
|
||||
status->game = stream.gameId;
|
||||
status->title = stream.title;
|
||||
QDateTime since = QDateTime::fromString(stream.startedAt, Qt::ISODate);
|
||||
auto diff = since.secsTo(QDateTime::currentDateTime());
|
||||
status->uptime = QString::number(diff / 3600) + "h " +
|
||||
QString::number(diff % 3600 / 60) + "m";
|
||||
|
||||
status->rerun = false;
|
||||
if (stream.HasMember("stream_type"))
|
||||
{
|
||||
status->streamType = stream["stream_type"].GetString();
|
||||
}
|
||||
else
|
||||
{
|
||||
status->streamType = QString();
|
||||
}
|
||||
|
||||
if (stream.HasMember("broadcast_platform"))
|
||||
{
|
||||
const auto &broadcastPlatformValue = stream["broadcast_platform"];
|
||||
|
||||
if (broadcastPlatformValue.IsString())
|
||||
{
|
||||
const char *broadcastPlatform =
|
||||
stream["broadcast_platform"].GetString();
|
||||
if (strcmp(broadcastPlatform, "rerun") == 0)
|
||||
{
|
||||
status->rerun = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
status->streamType = stream.type;
|
||||
}
|
||||
setLive(true);
|
||||
|
||||
this->setLive(true);
|
||||
|
||||
// Signal all listeners that the stream status has been updated
|
||||
this->liveStatusChanged.invoke();
|
||||
|
||||
return Success;
|
||||
}
|
||||
|
||||
void TwitchChannel::loadRecentMessages()
|
||||
|
|
|
@ -8,14 +8,15 @@
|
|||
#include "common/UniqueAccess.hpp"
|
||||
#include "common/UsernameSet.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <IrcConnection>
|
||||
#include <QColor>
|
||||
#include <QRegularExpression>
|
||||
#include <boost/optional.hpp>
|
||||
#include <mutex>
|
||||
#include <pajlada/signals/signalholder.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -43,6 +44,7 @@ public:
|
|||
unsigned viewerCount = 0;
|
||||
QString title;
|
||||
QString game;
|
||||
QString gameId;
|
||||
QString uptime;
|
||||
QString streamType;
|
||||
};
|
||||
|
@ -120,7 +122,7 @@ protected:
|
|||
private:
|
||||
// Methods
|
||||
void refreshLiveStatus();
|
||||
Outcome parseLiveStatus(const rapidjson::Document &document);
|
||||
void parseLiveStatus(bool live, const HelixStream &stream);
|
||||
void refreshPubsub();
|
||||
void refreshChatters();
|
||||
void refreshBadges();
|
||||
|
|
|
@ -278,6 +278,7 @@ namespace {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Look through the results of
|
||||
|
|
368
src/providers/twitch/api/Helix.cpp
Normal file
368
src/providers/twitch/api/Helix.cpp
Normal file
|
@ -0,0 +1,368 @@
|
|||
#include "providers/twitch/api/Helix.hpp"
|
||||
|
||||
#include "common/Outcome.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
static Helix *instance = nullptr;
|
||||
|
||||
void Helix::fetchUsers(QStringList userIds, QStringList userLogins,
|
||||
ResultCallback<std::vector<HelixUser>> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
QUrlQuery urlQuery;
|
||||
|
||||
for (const auto &id : userIds)
|
||||
{
|
||||
urlQuery.addQueryItem("id", id);
|
||||
}
|
||||
|
||||
for (const auto &login : userLogins)
|
||||
{
|
||||
urlQuery.addQueryItem("login", login);
|
||||
}
|
||||
|
||||
// TODO: set on success and on error
|
||||
this->makeRequest("users", urlQuery)
|
||||
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto data = root.value("data");
|
||||
|
||||
if (!data.isArray())
|
||||
{
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
|
||||
std::vector<HelixUser> users;
|
||||
|
||||
for (const auto &jsonUser : data.toArray())
|
||||
{
|
||||
users.emplace_back(jsonUser.toObject());
|
||||
}
|
||||
|
||||
successCallback(users);
|
||||
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
// TODO: make better xd
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
void Helix::getUserByName(QString userId,
|
||||
ResultCallback<HelixUser> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
QStringList userIds;
|
||||
QStringList userLogins{userId};
|
||||
|
||||
this->fetchUsers(
|
||||
userIds, userLogins,
|
||||
[successCallback,
|
||||
failureCallback](const std::vector<HelixUser> &users) {
|
||||
if (users.empty())
|
||||
{
|
||||
failureCallback();
|
||||
return;
|
||||
}
|
||||
successCallback(users[0]);
|
||||
},
|
||||
failureCallback);
|
||||
}
|
||||
|
||||
void Helix::getUserById(QString userId,
|
||||
ResultCallback<HelixUser> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
QStringList userIds{userId};
|
||||
QStringList userLogins;
|
||||
|
||||
this->fetchUsers(
|
||||
userIds, userLogins,
|
||||
[successCallback, failureCallback](const auto &users) {
|
||||
if (users.empty())
|
||||
{
|
||||
failureCallback();
|
||||
return;
|
||||
}
|
||||
successCallback(users[0]);
|
||||
},
|
||||
failureCallback);
|
||||
}
|
||||
|
||||
void Helix::fetchUsersFollows(
|
||||
QString fromId, QString toId,
|
||||
ResultCallback<HelixUsersFollowsResponse> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
assert(!fromId.isEmpty() || !toId.isEmpty());
|
||||
|
||||
QUrlQuery urlQuery;
|
||||
|
||||
if (!fromId.isEmpty())
|
||||
{
|
||||
urlQuery.addQueryItem("from_id", fromId);
|
||||
}
|
||||
|
||||
if (!toId.isEmpty())
|
||||
{
|
||||
urlQuery.addQueryItem("to_id", toId);
|
||||
}
|
||||
|
||||
// TODO: set on success and on error
|
||||
this->makeRequest("users/follows", urlQuery)
|
||||
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
if (root.empty())
|
||||
{
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
successCallback(HelixUsersFollowsResponse(root));
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
// TODO: make better xd
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
void Helix::getUserFollowers(
|
||||
QString userId, ResultCallback<HelixUsersFollowsResponse> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
this->fetchUsersFollows("", userId, successCallback, failureCallback);
|
||||
}
|
||||
|
||||
void Helix::getUserFollow(
|
||||
QString userId, QString targetId,
|
||||
ResultCallback<bool, HelixUsersFollowsRecord> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
this->fetchUsersFollows(
|
||||
userId, targetId,
|
||||
[successCallback](const auto &response) {
|
||||
if (response.data.empty())
|
||||
{
|
||||
successCallback(false, HelixUsersFollowsRecord());
|
||||
return;
|
||||
}
|
||||
|
||||
successCallback(true, response.data[0]);
|
||||
},
|
||||
failureCallback);
|
||||
}
|
||||
|
||||
void Helix::fetchStreams(
|
||||
QStringList userIds, QStringList userLogins,
|
||||
ResultCallback<std::vector<HelixStream>> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
QUrlQuery urlQuery;
|
||||
|
||||
for (const auto &id : userIds)
|
||||
{
|
||||
urlQuery.addQueryItem("user_id", id);
|
||||
}
|
||||
|
||||
for (const auto &login : userLogins)
|
||||
{
|
||||
urlQuery.addQueryItem("user_login", login);
|
||||
}
|
||||
|
||||
// TODO: set on success and on error
|
||||
this->makeRequest("streams", urlQuery)
|
||||
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto data = root.value("data");
|
||||
|
||||
if (!data.isArray())
|
||||
{
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
|
||||
std::vector<HelixStream> streams;
|
||||
|
||||
for (const auto &jsonStream : data.toArray())
|
||||
{
|
||||
streams.emplace_back(jsonStream.toObject());
|
||||
}
|
||||
|
||||
successCallback(streams);
|
||||
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
// TODO: make better xd
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
void Helix::getStreamById(QString userId,
|
||||
ResultCallback<bool, HelixStream> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
QStringList userIds{userId};
|
||||
QStringList userLogins;
|
||||
|
||||
this->fetchStreams(
|
||||
userIds, userLogins,
|
||||
[successCallback, failureCallback](const auto &streams) {
|
||||
if (streams.empty())
|
||||
{
|
||||
successCallback(false, HelixStream());
|
||||
return;
|
||||
}
|
||||
successCallback(true, streams[0]);
|
||||
},
|
||||
failureCallback);
|
||||
}
|
||||
|
||||
void Helix::getStreamByName(QString userName,
|
||||
ResultCallback<bool, HelixStream> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
QStringList userIds;
|
||||
QStringList userLogins{userName};
|
||||
|
||||
this->fetchStreams(
|
||||
userIds, userLogins,
|
||||
[successCallback, failureCallback](const auto &streams) {
|
||||
if (streams.empty())
|
||||
{
|
||||
successCallback(false, HelixStream());
|
||||
return;
|
||||
}
|
||||
successCallback(true, streams[0]);
|
||||
},
|
||||
failureCallback);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
void Helix::fetchGames(QStringList gameIds, QStringList gameNames,
|
||||
ResultCallback<std::vector<HelixGame>> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
assert((gameIds.length() + gameNames.length()) > 0);
|
||||
|
||||
QUrlQuery urlQuery;
|
||||
|
||||
for (const auto &id : gameIds)
|
||||
{
|
||||
urlQuery.addQueryItem("id", id);
|
||||
}
|
||||
|
||||
for (const auto &login : gameNames)
|
||||
{
|
||||
urlQuery.addQueryItem("name", login);
|
||||
}
|
||||
|
||||
// TODO: set on success and on error
|
||||
this->makeRequest("games", urlQuery)
|
||||
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto data = root.value("data");
|
||||
|
||||
if (!data.isArray())
|
||||
{
|
||||
failureCallback();
|
||||
return Failure;
|
||||
}
|
||||
|
||||
std::vector<HelixGame> games;
|
||||
|
||||
for (const auto &jsonStream : data.toArray())
|
||||
{
|
||||
games.emplace_back(jsonStream.toObject());
|
||||
}
|
||||
|
||||
successCallback(games);
|
||||
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
// TODO: make better xd
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
void Helix::getGameById(QString gameId,
|
||||
ResultCallback<HelixGame> successCallback,
|
||||
HelixFailureCallback failureCallback)
|
||||
{
|
||||
QStringList gameIds{gameId};
|
||||
QStringList gameNames;
|
||||
|
||||
this->fetchGames(
|
||||
gameIds, gameNames,
|
||||
[successCallback, failureCallback](const auto &games) {
|
||||
if (games.empty())
|
||||
{
|
||||
failureCallback();
|
||||
return;
|
||||
}
|
||||
successCallback(games[0]);
|
||||
},
|
||||
failureCallback);
|
||||
}
|
||||
|
||||
NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
|
||||
{
|
||||
assert(!url.startsWith("/"));
|
||||
|
||||
if (this->clientId.isEmpty())
|
||||
{
|
||||
qDebug()
|
||||
<< "Helix::makeRequest called without a client ID set BabyRage";
|
||||
// return boost::none;
|
||||
}
|
||||
|
||||
if (this->oauthToken.isEmpty())
|
||||
{
|
||||
qDebug()
|
||||
<< "Helix::makeRequest called without an oauth token set BabyRage";
|
||||
// return boost::none;
|
||||
}
|
||||
|
||||
const QString baseUrl("https://api.twitch.tv/helix/");
|
||||
|
||||
QUrl fullUrl(baseUrl + url);
|
||||
|
||||
fullUrl.setQuery(urlQuery);
|
||||
|
||||
return NetworkRequest(fullUrl)
|
||||
.timeout(5 * 1000)
|
||||
.header("Accept", "application/json")
|
||||
.header("Client-ID", this->clientId)
|
||||
.header("Authorization", "Bearer " + this->oauthToken);
|
||||
}
|
||||
|
||||
void Helix::update(QString clientId, QString oauthToken)
|
||||
{
|
||||
this->clientId = clientId;
|
||||
this->oauthToken = oauthToken;
|
||||
}
|
||||
|
||||
void Helix::initialize()
|
||||
{
|
||||
assert(instance == nullptr);
|
||||
|
||||
instance = new Helix();
|
||||
}
|
||||
|
||||
Helix *getHelix()
|
||||
{
|
||||
assert(instance != nullptr);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
198
src/providers/twitch/api/Helix.hpp
Normal file
198
src/providers/twitch/api/Helix.hpp
Normal file
|
@ -0,0 +1,198 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/NetworkRequest.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <boost/noncopyable.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using HelixFailureCallback = std::function<void()>;
|
||||
template <typename... T>
|
||||
using ResultCallback = std::function<void(T...)>;
|
||||
|
||||
struct HelixUser {
|
||||
QString id;
|
||||
QString login;
|
||||
QString displayName;
|
||||
QString description;
|
||||
QString profileImageUrl;
|
||||
int viewCount;
|
||||
|
||||
explicit HelixUser(QJsonObject jsonObject)
|
||||
: id(jsonObject.value("id").toString())
|
||||
, login(jsonObject.value("login").toString())
|
||||
, displayName(jsonObject.value("display_name").toString())
|
||||
, description(jsonObject.value("description").toString())
|
||||
, profileImageUrl(jsonObject.value("profile_image_url").toString())
|
||||
, viewCount(jsonObject.value("view_count").toInt())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct HelixUsersFollowsRecord {
|
||||
QString fromId;
|
||||
QString fromName;
|
||||
QString toId;
|
||||
QString toName;
|
||||
QString followedAt; // date time object
|
||||
|
||||
HelixUsersFollowsRecord()
|
||||
: fromId("")
|
||||
, fromName("")
|
||||
, toId("")
|
||||
, toName("")
|
||||
, followedAt("")
|
||||
{
|
||||
}
|
||||
|
||||
explicit HelixUsersFollowsRecord(QJsonObject jsonObject)
|
||||
: fromId(jsonObject.value("from_id").toString())
|
||||
, fromName(jsonObject.value("from_name").toString())
|
||||
, toId(jsonObject.value("to_id").toString())
|
||||
, toName(jsonObject.value("to_name").toString())
|
||||
, followedAt(jsonObject.value("followed_at").toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct HelixUsersFollowsResponse {
|
||||
int total;
|
||||
std::vector<HelixUsersFollowsRecord> data;
|
||||
explicit HelixUsersFollowsResponse(QJsonObject jsonObject)
|
||||
: total(jsonObject.value("total").toInt())
|
||||
{
|
||||
const auto &jsonData = jsonObject.value("data").toArray();
|
||||
std::transform(jsonData.begin(), jsonData.end(),
|
||||
std::back_inserter(this->data),
|
||||
[](const QJsonValue &record) {
|
||||
return HelixUsersFollowsRecord(record.toObject());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
struct HelixStream {
|
||||
QString id; // stream id
|
||||
QString userId;
|
||||
QString userName;
|
||||
QString gameId;
|
||||
QString type;
|
||||
QString title;
|
||||
int viewerCount;
|
||||
QString startedAt;
|
||||
QString language;
|
||||
QString thumbnailUrl;
|
||||
|
||||
HelixStream()
|
||||
: id("")
|
||||
, userId("")
|
||||
, userName("")
|
||||
, gameId("")
|
||||
, type("")
|
||||
, title("")
|
||||
, viewerCount()
|
||||
, startedAt("")
|
||||
, language("")
|
||||
, thumbnailUrl("")
|
||||
{
|
||||
}
|
||||
|
||||
explicit HelixStream(QJsonObject jsonObject)
|
||||
: id(jsonObject.value("id").toString())
|
||||
, userId(jsonObject.value("user_id").toString())
|
||||
, userName(jsonObject.value("user_name").toString())
|
||||
, gameId(jsonObject.value("game_id").toString())
|
||||
, type(jsonObject.value("type").toString())
|
||||
, title(jsonObject.value("title").toString())
|
||||
, viewerCount(jsonObject.value("viewer_count").toInt())
|
||||
, startedAt(jsonObject.value("started_at").toString())
|
||||
, language(jsonObject.value("language").toString())
|
||||
, thumbnailUrl(jsonObject.value("thumbnail_url").toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct HelixGame {
|
||||
QString id; // stream id
|
||||
QString name;
|
||||
QString boxArtUrl;
|
||||
|
||||
explicit HelixGame(QJsonObject jsonObject)
|
||||
: id(jsonObject.value("id").toString())
|
||||
, name(jsonObject.value("name").toString())
|
||||
, boxArtUrl(jsonObject.value("box_art_url").toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class Helix final : boost::noncopyable
|
||||
{
|
||||
public:
|
||||
// https://dev.twitch.tv/docs/api/reference#get-users
|
||||
void fetchUsers(QStringList userIds, QStringList userLogins,
|
||||
ResultCallback<std::vector<HelixUser>> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
void getUserByName(QString userName,
|
||||
ResultCallback<HelixUser> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
void getUserById(QString userId, ResultCallback<HelixUser> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
// https://dev.twitch.tv/docs/api/reference#get-users-follows
|
||||
void fetchUsersFollows(
|
||||
QString fromId, QString toId,
|
||||
ResultCallback<HelixUsersFollowsResponse> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
void getUserFollowers(
|
||||
QString userId,
|
||||
ResultCallback<HelixUsersFollowsResponse> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
void getUserFollow(
|
||||
QString userId, QString targetId,
|
||||
ResultCallback<bool, HelixUsersFollowsRecord> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
// https://dev.twitch.tv/docs/api/reference#get-streams
|
||||
void fetchStreams(QStringList userIds, QStringList userLogins,
|
||||
ResultCallback<std::vector<HelixStream>> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
void getStreamById(QString userId,
|
||||
ResultCallback<bool, HelixStream> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
void getStreamByName(QString userName,
|
||||
ResultCallback<bool, HelixStream> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
// https://dev.twitch.tv/docs/api/reference#get-games
|
||||
void fetchGames(QStringList gameIds, QStringList gameNames,
|
||||
ResultCallback<std::vector<HelixGame>> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
void getGameById(QString gameId, ResultCallback<HelixGame> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
void update(QString clientId, QString oauthToken);
|
||||
|
||||
static void initialize();
|
||||
|
||||
private:
|
||||
NetworkRequest makeRequest(QString url, QUrlQuery urlQuery);
|
||||
|
||||
QString clientId;
|
||||
QString oauthToken;
|
||||
};
|
||||
|
||||
Helix *getHelix();
|
||||
|
||||
} // namespace chatterino
|
104
src/providers/twitch/api/Kraken.cpp
Normal file
104
src/providers/twitch/api/Kraken.cpp
Normal file
|
@ -0,0 +1,104 @@
|
|||
#include "providers/twitch/api/Kraken.hpp"
|
||||
|
||||
#include "common/Outcome.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
static Kraken *instance = nullptr;
|
||||
|
||||
void Kraken::getChannel(QString userId,
|
||||
ResultCallback<KrakenChannel> successCallback,
|
||||
KrakenFailureCallback failureCallback)
|
||||
{
|
||||
assert(!userId.isEmpty());
|
||||
|
||||
this->makeRequest("channels/" + userId, {})
|
||||
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
|
||||
successCallback(root);
|
||||
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
// TODO: make better xd
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
void Kraken::getUser(QString userId, ResultCallback<KrakenUser> successCallback,
|
||||
KrakenFailureCallback failureCallback)
|
||||
{
|
||||
assert(!userId.isEmpty());
|
||||
|
||||
this->makeRequest("users/" + userId, {})
|
||||
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
|
||||
successCallback(root);
|
||||
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
// TODO: make better xd
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
NetworkRequest Kraken::makeRequest(QString url, QUrlQuery urlQuery)
|
||||
{
|
||||
assert(!url.startsWith("/"));
|
||||
|
||||
if (this->clientId.isEmpty())
|
||||
{
|
||||
qDebug()
|
||||
<< "Kraken::makeRequest called without a client ID set BabyRage";
|
||||
}
|
||||
|
||||
const QString baseUrl("https://api.twitch.tv/kraken/");
|
||||
|
||||
QUrl fullUrl(baseUrl + url);
|
||||
|
||||
fullUrl.setQuery(urlQuery);
|
||||
|
||||
if (!this->oauthToken.isEmpty())
|
||||
{
|
||||
return NetworkRequest(fullUrl)
|
||||
.timeout(5 * 1000)
|
||||
.header("Accept", "application/vnd.twitchtv.v5+json")
|
||||
.header("Client-ID", this->clientId)
|
||||
.header("Authorization", "OAuth " + this->oauthToken);
|
||||
}
|
||||
|
||||
return NetworkRequest(fullUrl)
|
||||
.timeout(5 * 1000)
|
||||
.header("Accept", "application/vnd.twitchtv.v5+json")
|
||||
.header("Client-ID", this->clientId);
|
||||
}
|
||||
|
||||
void Kraken::update(QString clientId, QString oauthToken)
|
||||
{
|
||||
this->clientId = clientId;
|
||||
this->oauthToken = oauthToken;
|
||||
}
|
||||
|
||||
void Kraken::initialize()
|
||||
{
|
||||
assert(instance == nullptr);
|
||||
|
||||
instance = new Kraken();
|
||||
|
||||
getKraken()->update(getDefaultClientID(), "");
|
||||
}
|
||||
|
||||
Kraken *getKraken()
|
||||
{
|
||||
assert(instance != nullptr);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
61
src/providers/twitch/api/Kraken.hpp
Normal file
61
src/providers/twitch/api/Kraken.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/NetworkRequest.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrlQuery>
|
||||
#include <boost/noncopyable.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using KrakenFailureCallback = std::function<void()>;
|
||||
template <typename... T>
|
||||
using ResultCallback = std::function<void(T...)>;
|
||||
|
||||
struct KrakenChannel {
|
||||
const QString status;
|
||||
|
||||
KrakenChannel(QJsonObject jsonObject)
|
||||
: status(jsonObject.value("status").toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct KrakenUser {
|
||||
const QString createdAt;
|
||||
|
||||
KrakenUser(QJsonObject jsonObject)
|
||||
: createdAt(jsonObject.value("created_at").toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class Kraken final : boost::noncopyable
|
||||
{
|
||||
public:
|
||||
// https://dev.twitch.tv/docs/v5/reference/users#follow-channel
|
||||
void getChannel(QString userId,
|
||||
ResultCallback<KrakenChannel> resultCallback,
|
||||
KrakenFailureCallback failureCallback);
|
||||
|
||||
// https://dev.twitch.tv/docs/v5/reference/users#get-user-by-id
|
||||
void getUser(QString userId, ResultCallback<KrakenUser> resultCallback,
|
||||
KrakenFailureCallback failureCallback);
|
||||
|
||||
void update(QString clientId, QString oauthToken);
|
||||
|
||||
static void initialize();
|
||||
|
||||
private:
|
||||
NetworkRequest makeRequest(QString url, QUrlQuery urlQuery);
|
||||
|
||||
QString clientId;
|
||||
QString oauthToken;
|
||||
};
|
||||
|
||||
Kraken *getKraken();
|
||||
|
||||
} // namespace chatterino
|
125
src/providers/twitch/api/README.md
Normal file
125
src/providers/twitch/api/README.md
Normal file
|
@ -0,0 +1,125 @@
|
|||
# Twitch API
|
||||
this folder describes what sort of API requests we do, what permissions are required for the requests etc
|
||||
|
||||
## Kraken (V5)
|
||||
We use a bunch of Kraken (V5) in Chatterino2.
|
||||
|
||||
### Get User
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-by-id
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We implement this in `providers/twitch/api/Kraken.cpp getUser`
|
||||
Used in:
|
||||
* `UserInfoPopup` to get the "created at" date of a user
|
||||
|
||||
### Get Channel
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/channels#get-channel
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We implement this in `providers/twitch/api/Kraken.cpp getChannel`
|
||||
Used in:
|
||||
* `TwitchChannel::refreshTitle` to check the current stream title/game of offline channels
|
||||
|
||||
### Follow Channel
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#follow-channel
|
||||
Requires `user_follows_edit` scope
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We implement this API in `providers/twitch/TwitchAccount.cpp followUser`
|
||||
|
||||
### Unfollow Channel
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#unfollow-channel
|
||||
Requires `user_follows_edit` scope
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We implement this API in `providers/twitch/TwitchAccount.cpp unfollowUser`
|
||||
|
||||
|
||||
### Get Cheermotes
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/bits#get-cheermotes
|
||||
|
||||
Migration path: **Not checked**
|
||||
|
||||
* We implement this API in `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000`
|
||||
|
||||
### Get User Block List
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-block-list
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We use this in `providers/twitch/TwitchAccount.cpp loadIgnores`
|
||||
|
||||
### Block User
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#block-user
|
||||
Requires `user_blocks_edit` scope
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We use this in `providers/twitch/TwitchAccount.cpp ignoreByID`
|
||||
|
||||
### Unblock User
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#unblock-user
|
||||
Requires `user_blocks_edit` scope
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We use this in `providers/twitch/TwitchAccount.cpp unignoreByID`
|
||||
|
||||
### Get User Emotes
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-emotes
|
||||
Requires `user_subscriptions` scope
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
* We use this in `providers/twitch/TwitchAccount.cpp loadEmotes` to figure out which emotes a user is allowed to use!
|
||||
|
||||
### AUTOMOD APPROVE
|
||||
**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2
|
||||
|
||||
* We use this in `providers/twitch/TwitchAccount.cpp autoModAllow` to approve an automod deny/allow question
|
||||
|
||||
### AUTOMOD DENY
|
||||
**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2
|
||||
|
||||
* We use this in `providers/twitch/TwitchAccount.cpp autoModDeny` to deny an automod deny/allow question
|
||||
|
||||
## Helix
|
||||
Full Helix API reference: https://dev.twitch.tv/docs/api/reference
|
||||
|
||||
### Get Users
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-users
|
||||
|
||||
* We implement this in `providers/twitch/api/Helix.cpp fetchUsers`.
|
||||
Used in:
|
||||
* `UserInfoPopup` to get ID and viewcount of username we clicked
|
||||
* `CommandController` to power any commands that need to get a user ID
|
||||
* `Toasts` to get the profile picture of a streamer who just went live
|
||||
* `TwitchAccount` ignore and unignore features to translate user name to user ID
|
||||
|
||||
### Get Users Follows
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-users-follows
|
||||
|
||||
* We implement this in `providers/twitch/api/Helix.cpp fetchUsersFollows`
|
||||
Used in:
|
||||
* `UserInfoPopup` to get number of followers a user has
|
||||
|
||||
### Get Streams
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-streams
|
||||
|
||||
* We implement this in `providers/twitch/api/Helix.cpp fetchStreams`
|
||||
Used in:
|
||||
* `TwitchChannel` to get live status, game, title, and viewer count of a channel
|
||||
* `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
|
||||
|
||||
## TMI
|
||||
The TMI api is undocumented.
|
||||
|
||||
### Get Chatters
|
||||
**Undocumented**
|
||||
|
||||
* We use this in `widgets/splits/Split.cpp showViewerList`
|
||||
* We use this in `providers/twitch/TwitchChannel.cpp refreshChatters`
|
|
@ -7,6 +7,7 @@
|
|||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "util/StreamLink.hpp"
|
||||
#include "widgets/helper/CommonTexts.hpp"
|
||||
|
@ -85,14 +86,17 @@ void Toasts::sendChannelNotification(const QString &channelName, Platform p)
|
|||
}
|
||||
else
|
||||
{
|
||||
this->fetchChannelAvatar(
|
||||
getHelix()->getUserByName(
|
||||
channelName,
|
||||
[channelName, sendChannelNotification](QString avatarLink) {
|
||||
[channelName, sendChannelNotification](const auto &user) {
|
||||
DownloadManager *manager = new DownloadManager();
|
||||
manager->setFile(avatarLink, channelName);
|
||||
manager->setFile(user.profileImageUrl, channelName);
|
||||
manager->connect(manager,
|
||||
&DownloadManager::downloadComplete,
|
||||
sendChannelNotification);
|
||||
},
|
||||
[] {
|
||||
// on failure
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -206,53 +210,4 @@ void Toasts::sendWindowsNotification(const QString &channelName, Platform p)
|
|||
|
||||
#endif
|
||||
|
||||
void Toasts::fetchChannelAvatar(const QString channelName,
|
||||
std::function<void(QString)> successCallback)
|
||||
{
|
||||
QString requestUrl("https://api.twitch.tv/kraken/users?login=" +
|
||||
channelName);
|
||||
|
||||
NetworkRequest(requestUrl)
|
||||
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
if (!root.value("users").isArray())
|
||||
{
|
||||
// log("API Error while getting user id, users is not an array");
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
auto users = root.value("users").toArray();
|
||||
if (users.size() != 1)
|
||||
{
|
||||
// log("API Error while getting user id, users array size is not
|
||||
// 1");
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
if (!users[0].isObject())
|
||||
{
|
||||
// log("API Error while getting user id, first user is not an
|
||||
// object");
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
auto firstUser = users[0].toObject();
|
||||
auto avatar = firstUser.value("logo");
|
||||
if (!avatar.isString())
|
||||
{
|
||||
// log("API Error: while getting user avatar, first user object "
|
||||
// "`avatar` key "
|
||||
// "is not a "
|
||||
// "string");
|
||||
successCallback("");
|
||||
return Failure;
|
||||
}
|
||||
successCallback(avatar.toString());
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -29,9 +29,5 @@ private:
|
|||
#ifdef Q_OS_WIN
|
||||
void sendWindowsNotification(const QString &channelName, Platform p);
|
||||
#endif
|
||||
|
||||
static void fetchChannelAvatar(
|
||||
const QString channelName,
|
||||
std::function<void(QString)> successCallback);
|
||||
};
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#include "common/Common.hpp"
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "providers/twitch/PartialTwitchUser.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
|
||||
#ifdef USEWINSDK
|
||||
|
@ -159,7 +158,6 @@ AdvancedLoginWidget::AdvancedLoginWidget()
|
|||
this->ui_.layout.addWidget(&this->ui_.instructionsLabel);
|
||||
this->ui_.layout.addLayout(&this->ui_.formLayout);
|
||||
this->ui_.layout.addLayout(&this->ui_.buttonUpperRow.layout);
|
||||
this->ui_.layout.addLayout(&this->ui_.buttonLowerRow.layout);
|
||||
|
||||
this->refreshButtons();
|
||||
|
||||
|
@ -207,29 +205,10 @@ AdvancedLoginWidget::AdvancedLoginWidget()
|
|||
|
||||
LogInWithCredentials(userID, username, clientID, oauthToken);
|
||||
});
|
||||
|
||||
/// Lower button row
|
||||
this->ui_.buttonLowerRow.fillInUserIDButton.setText(
|
||||
"Get user ID from username");
|
||||
|
||||
this->ui_.buttonLowerRow.layout.addWidget(
|
||||
&this->ui_.buttonLowerRow.fillInUserIDButton);
|
||||
|
||||
connect(&this->ui_.buttonLowerRow.fillInUserIDButton, &QPushButton::clicked,
|
||||
[=]() {
|
||||
const auto onIdFetched = [=](const QString &userID) {
|
||||
this->ui_.userIDInput.setText(userID); //
|
||||
};
|
||||
PartialTwitchUser::byName(this->ui_.usernameInput.text())
|
||||
.getId(onIdFetched, this);
|
||||
});
|
||||
}
|
||||
|
||||
void AdvancedLoginWidget::refreshButtons()
|
||||
{
|
||||
this->ui_.buttonLowerRow.fillInUserIDButton.setEnabled(
|
||||
!this->ui_.usernameInput.text().isEmpty());
|
||||
|
||||
if (this->ui_.userIDInput.text().isEmpty() ||
|
||||
this->ui_.usernameInput.text().isEmpty() ||
|
||||
this->ui_.clientIDInput.text().isEmpty() ||
|
||||
|
|
|
@ -57,12 +57,6 @@ public:
|
|||
QPushButton addUserButton;
|
||||
QPushButton clearFieldsButton;
|
||||
} buttonUpperRow;
|
||||
|
||||
struct {
|
||||
QHBoxLayout layout;
|
||||
|
||||
QPushButton fillInUserIDButton;
|
||||
} buttonLowerRow;
|
||||
} ui_;
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#include "common/Channel.hpp"
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "providers/twitch/PartialTwitchUser.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
#include "common/NetworkRequest.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightBlacklistUser.hpp"
|
||||
#include "providers/twitch/PartialTwitchUser.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/LayoutCreator.hpp"
|
||||
|
@ -22,9 +23,9 @@
|
|||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#define TEXT_FOLLOWERS "Followers: "
|
||||
#define TEXT_VIEWS "Views: "
|
||||
#define TEXT_CREATED "Created: "
|
||||
const QString TEXT_VIEWS("Views: %1");
|
||||
const QString TEXT_FOLLOWERS("Followers: %1");
|
||||
const QString TEXT_CREATED("Created: %1");
|
||||
#define TEXT_USER_ID "ID: "
|
||||
#define TEXT_UNAVAILABLE "(not available)"
|
||||
|
||||
|
@ -92,10 +93,11 @@ UserInfoPopup::UserInfoPopup()
|
|||
this->ui_.userIDLabel->setPalette(palette);
|
||||
}
|
||||
|
||||
vbox.emplace<Label>(TEXT_VIEWS).assign(&this->ui_.viewCountLabel);
|
||||
vbox.emplace<Label>(TEXT_FOLLOWERS)
|
||||
vbox.emplace<Label>(TEXT_VIEWS.arg(""))
|
||||
.assign(&this->ui_.viewCountLabel);
|
||||
vbox.emplace<Label>(TEXT_FOLLOWERS.arg(""))
|
||||
.assign(&this->ui_.followerCountLabel);
|
||||
vbox.emplace<Label>(TEXT_CREATED)
|
||||
vbox.emplace<Label>(TEXT_CREATED.arg(""))
|
||||
.assign(&this->ui_.createdDateLabel);
|
||||
}
|
||||
}
|
||||
|
@ -260,10 +262,6 @@ void UserInfoPopup::installEvents()
|
|||
this->ui_.follow, &QCheckBox::stateChanged, [this](int) mutable {
|
||||
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
QUrl requestUrl("https://api.twitch.tv/kraken/users/" +
|
||||
currentUser->getUserId() + "/follows/channels/" +
|
||||
this->userId_);
|
||||
|
||||
const auto reenableFollowCheckbox = [this] {
|
||||
this->ui_.follow->setEnabled(true); //
|
||||
};
|
||||
|
@ -383,14 +381,12 @@ void UserInfoPopup::updateUserData()
|
|||
{
|
||||
std::weak_ptr<bool> hack = this->hack_;
|
||||
|
||||
const auto onIdFetchFailed = [this]() {
|
||||
const auto onUserFetchFailed = [this] {
|
||||
// this can occur when the account doesn't exist.
|
||||
this->ui_.followerCountLabel->setText(TEXT_FOLLOWERS +
|
||||
QString(TEXT_UNAVAILABLE));
|
||||
this->ui_.viewCountLabel->setText(TEXT_VIEWS +
|
||||
QString(TEXT_UNAVAILABLE));
|
||||
this->ui_.createdDateLabel->setText(TEXT_CREATED +
|
||||
QString(TEXT_UNAVAILABLE));
|
||||
this->ui_.followerCountLabel->setText(
|
||||
TEXT_FOLLOWERS.arg(TEXT_UNAVAILABLE));
|
||||
this->ui_.viewCountLabel->setText(TEXT_VIEWS.arg(TEXT_UNAVAILABLE));
|
||||
this->ui_.createdDateLabel->setText(TEXT_CREATED.arg(TEXT_UNAVAILABLE));
|
||||
|
||||
this->ui_.nameLabel->setText(this->userName_);
|
||||
|
||||
|
@ -399,39 +395,38 @@ void UserInfoPopup::updateUserData()
|
|||
this->ui_.userIDLabel->setProperty("copy-text",
|
||||
QString(TEXT_UNAVAILABLE));
|
||||
};
|
||||
const auto onIdFetched = [this, hack](QString id) {
|
||||
const auto onUserFetched = [this, hack](const auto &user) {
|
||||
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
this->userId_ = id;
|
||||
this->userId_ = user.id;
|
||||
|
||||
this->ui_.userIDLabel->setText(TEXT_USER_ID + id);
|
||||
this->ui_.userIDLabel->setProperty("copy-text", id);
|
||||
// don't wait for the request to complete, just put the user id in the card
|
||||
// right away
|
||||
this->ui_.userIDLabel->setText(TEXT_USER_ID + user.id);
|
||||
this->ui_.userIDLabel->setProperty("copy-text", user.id);
|
||||
|
||||
QString url("https://api.twitch.tv/kraken/channels/" + id);
|
||||
|
||||
NetworkRequest::twitchRequest(url)
|
||||
.caller(this)
|
||||
.onSuccess([this](auto result) -> Outcome {
|
||||
auto obj = result.parseJson();
|
||||
this->ui_.followerCountLabel->setText(
|
||||
TEXT_FOLLOWERS +
|
||||
QString::number(obj.value("followers").toInt()));
|
||||
this->ui_.viewCountLabel->setText(
|
||||
TEXT_VIEWS + QString::number(obj.value("views").toInt()));
|
||||
this->ui_.viewCountLabel->setText(TEXT_VIEWS.arg(user.viewCount));
|
||||
getKraken()->getUser(
|
||||
user.id,
|
||||
[this](const auto &user) {
|
||||
this->ui_.createdDateLabel->setText(
|
||||
TEXT_CREATED +
|
||||
obj.value("created_at").toString().section("T", 0, 0));
|
||||
TEXT_CREATED.arg(user.createdAt.section("T", 0, 0)));
|
||||
},
|
||||
[] {
|
||||
// failure
|
||||
});
|
||||
this->loadAvatar(user.profileImageUrl);
|
||||
|
||||
this->loadAvatar(QUrl(obj.value("logo").toString()));
|
||||
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
getHelix()->getUserFollowers(
|
||||
user.id,
|
||||
[this](const auto &followers) {
|
||||
this->ui_.followerCountLabel->setText(
|
||||
TEXT_FOLLOWERS.arg(followers.total));
|
||||
},
|
||||
[] {
|
||||
// on failure
|
||||
});
|
||||
|
||||
// get follow state
|
||||
currentUser->checkFollow(id, [this, hack](auto result) {
|
||||
currentUser->checkFollow(user.id, [this, hack](auto result) {
|
||||
if (hack.lock())
|
||||
{
|
||||
if (result != FollowResult_Failed)
|
||||
|
@ -447,7 +442,7 @@ void UserInfoPopup::updateUserData()
|
|||
bool isIgnoring = false;
|
||||
for (const auto &ignoredUser : currentUser->getIgnores())
|
||||
{
|
||||
if (id == ignoredUser.id)
|
||||
if (user.id == ignoredUser.id)
|
||||
{
|
||||
isIgnoring = true;
|
||||
break;
|
||||
|
@ -479,8 +474,8 @@ void UserInfoPopup::updateUserData()
|
|||
this->ui_.ignoreHighlights->setChecked(isIgnoringHighlights);
|
||||
};
|
||||
|
||||
PartialTwitchUser::byName(this->userName_)
|
||||
.getId(onIdFetched, onIdFetchFailed, this);
|
||||
getHelix()->getUserByName(this->userName_, onUserFetched,
|
||||
onUserFetchFailed);
|
||||
|
||||
this->ui_.follow->setEnabled(false);
|
||||
this->ui_.ignore->setEnabled(false);
|
||||
|
|
Loading…
Reference in a new issue