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:
pajlada 2020-03-14 07:13:57 -04:00 committed by GitHub
parent 2e39dd4d9b
commit 9a8b85e338
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1076 additions and 572 deletions

View file

@ -173,14 +173,14 @@ SOURCES += \
src/providers/irc/IrcConnection2.cpp \ src/providers/irc/IrcConnection2.cpp \
src/providers/irc/IrcServer.cpp \ src/providers/irc/IrcServer.cpp \
src/providers/LinkResolver.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/IrcMessageHandler.cpp \
src/providers/twitch/PartialTwitchUser.cpp \
src/providers/twitch/PubsubActions.cpp \ src/providers/twitch/PubsubActions.cpp \
src/providers/twitch/PubsubClient.cpp \ src/providers/twitch/PubsubClient.cpp \
src/providers/twitch/PubsubHelpers.cpp \ src/providers/twitch/PubsubHelpers.cpp \
src/providers/twitch/TwitchAccount.cpp \ src/providers/twitch/TwitchAccount.cpp \
src/providers/twitch/TwitchAccountManager.cpp \ src/providers/twitch/TwitchAccountManager.cpp \
src/providers/twitch/TwitchApi.cpp \
src/providers/twitch/TwitchBadge.cpp \ src/providers/twitch/TwitchBadge.cpp \
src/providers/twitch/TwitchBadges.cpp \ src/providers/twitch/TwitchBadges.cpp \
src/providers/twitch/TwitchChannel.cpp \ src/providers/twitch/TwitchChannel.cpp \
@ -372,15 +372,15 @@ HEADERS += \
src/providers/irc/IrcConnection2.hpp \ src/providers/irc/IrcConnection2.hpp \
src/providers/irc/IrcServer.hpp \ src/providers/irc/IrcServer.hpp \
src/providers/LinkResolver.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/EmoteValue.hpp \
src/providers/twitch/IrcMessageHandler.hpp \ src/providers/twitch/IrcMessageHandler.hpp \
src/providers/twitch/PartialTwitchUser.hpp \
src/providers/twitch/PubsubActions.hpp \ src/providers/twitch/PubsubActions.hpp \
src/providers/twitch/PubsubClient.hpp \ src/providers/twitch/PubsubClient.hpp \
src/providers/twitch/PubsubHelpers.hpp \ src/providers/twitch/PubsubHelpers.hpp \
src/providers/twitch/TwitchAccount.hpp \ src/providers/twitch/TwitchAccount.hpp \
src/providers/twitch/TwitchAccountManager.hpp \ src/providers/twitch/TwitchAccountManager.hpp \
src/providers/twitch/TwitchApi.hpp \
src/providers/twitch/TwitchBadge.hpp \ src/providers/twitch/TwitchBadge.hpp \
src/providers/twitch/TwitchBadges.hpp \ src/providers/twitch/TwitchBadges.hpp \
src/providers/twitch/TwitchChannel.hpp \ src/providers/twitch/TwitchChannel.hpp \

View file

@ -8,9 +8,9 @@
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp" #include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
#include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -364,18 +364,17 @@ QString CommandController::execCommand(const QString &textNoEmoji,
return ""; return "";
} }
TwitchApi::findUserId( getHelix()->getUserByName(
target, [user, channel, target](QString userId) { target,
if (userId.isEmpty()) [user, channel, target](const auto &targetUser) {
{ user->followUser(targetUser.id, [channel, target]() {
channel->addMessage(makeSystemMessage(
"User " + target + " could not be followed!"));
return;
}
user->followUser(userId, [channel, target]() {
channel->addMessage(makeSystemMessage( channel->addMessage(makeSystemMessage(
"You successfully followed " + target)); "You successfully followed " + target));
}); });
},
[channel, target] {
channel->addMessage(makeSystemMessage(
"User " + target + " could not be followed!"));
}); });
return ""; return "";
@ -400,18 +399,17 @@ QString CommandController::execCommand(const QString &textNoEmoji,
return ""; return "";
} }
TwitchApi::findUserId( getHelix()->getUserByName(
target, [user, channel, target](QString userId) { target,
if (userId.isEmpty()) [user, channel, target](const auto &targetUser) {
{ user->unfollowUser(targetUser.id, [channel, target]() {
channel->addMessage(makeSystemMessage(
"User " + target + " could not be followed!"));
return;
}
user->unfollowUser(userId, [channel, target]() {
channel->addMessage(makeSystemMessage( channel->addMessage(makeSystemMessage(
"You successfully unfollowed " + target)); "You successfully unfollowed " + target));
}); });
},
[channel, target] {
channel->addMessage(makeSystemMessage(
"User " + target + " could not be followed!"));
}); });
return ""; return "";

View file

@ -4,8 +4,8 @@
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "controllers/notifications/NotificationModel.hpp" #include "controllers/notifications/NotificationModel.hpp"
#include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "singletons/Toasts.hpp" #include "singletons/Toasts.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
@ -142,68 +142,52 @@ void NotificationController::fetchFakeChannels()
void NotificationController::getFakeTwitchChannelLiveStatus( void NotificationController::getFakeTwitchChannelLiveStatus(
const QString &channelName) const QString &channelName)
{ {
TwitchApi::findUserId(channelName, [channelName, this](QString roomID) { getHelix()->getStreamByName(
if (roomID.isEmpty()) 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 qDebug() << "[TwitchChannel" << channelName
<< "] Refreshing live status (Missing ID)"; << "] Refreshing live status (Missing ID)";
removeFakeChannel(channelName); this->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();
});
} }
void NotificationController::removeFakeChannel(const QString channelName) void NotificationController::removeFakeChannel(const QString channelName)

View file

@ -43,6 +43,7 @@ private:
void removeFakeChannel(const QString channelName); void removeFakeChannel(const QString channelName);
void getFakeTwitchChannelLiveStatus(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; std::vector<QString> fakeTwitchChannels;
QTimer *liveStatusTimer_; QTimer *liveStatusTimer_;

View file

@ -9,6 +9,8 @@
#include "common/Args.hpp" #include "common/Args.hpp"
#include "common/Modes.hpp" #include "common/Modes.hpp"
#include "common/Version.hpp" #include "common/Version.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/api/Kraken.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "util/IncognitoBrowser.hpp" #include "util/IncognitoBrowser.hpp"
@ -36,6 +38,9 @@ int main(int argc, char **argv)
} }
else else
{ {
Helix::initialize();
Kraken::initialize();
Paths *paths{}; Paths *paths{};
try try

View file

@ -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

View file

@ -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

View file

@ -6,8 +6,8 @@
#include "common/Env.hpp" #include "common/Env.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "util/RapidjsonHelpers.hpp" #include "util/RapidjsonHelpers.hpp"
@ -151,12 +151,14 @@ void TwitchAccount::ignore(
const QString &targetName, const QString &targetName,
std::function<void(IgnoreResult, const QString &)> onFinished) std::function<void(IgnoreResult, const QString &)> onFinished)
{ {
const auto onIdFetched = [this, targetName, const auto onUserFetched = [this, targetName,
onFinished](QString targetUserId) { onFinished](const auto &user) {
this->ignoreByID(targetUserId, targetName, onFinished); // this->ignoreByID(user.id, targetName, onFinished); //
}; };
PartialTwitchUser::byName(targetName).getId(onIdFetched); const auto onUserFetchFailed = [] {};
getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed);
} }
void TwitchAccount::ignoreByID( void TwitchAccount::ignoreByID(
@ -226,12 +228,14 @@ void TwitchAccount::unignore(
const QString &targetName, const QString &targetName,
std::function<void(UnignoreResult, const QString &message)> onFinished) std::function<void(UnignoreResult, const QString &message)> onFinished)
{ {
const auto onIdFetched = [this, targetName, const auto onUserFetched = [this, targetName,
onFinished](QString targetUserId) { onFinished](const auto &user) {
this->unignoreByID(targetUserId, targetName, onFinished); // this->unignoreByID(user.id, targetName, onFinished); //
}; };
PartialTwitchUser::byName(targetName).getId(onIdFetched); const auto onUserFetchFailed = [] {};
getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed);
} }
void TwitchAccount::unignoreByID( void TwitchAccount::unignoreByID(
@ -270,28 +274,18 @@ void TwitchAccount::unignoreByID(
void TwitchAccount::checkFollow(const QString targetUserID, void TwitchAccount::checkFollow(const QString targetUserID,
std::function<void(FollowResult)> onFinished) std::function<void(FollowResult)> onFinished)
{ {
QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + const auto onResponse = [onFinished](bool following, const auto &record) {
"/follows/channels/" + targetUserID); if (!following)
{
onFinished(FollowResult_NotFollowing);
return;
}
NetworkRequest(url) onFinished(FollowResult_Following);
};
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken()) getHelix()->getUserFollow(this->getUserId(), targetUserID, onResponse,
.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();
} }
void TwitchAccount::followUser(const QString userID, void TwitchAccount::followUser(const QString userID,

View file

@ -3,6 +3,8 @@
#include "common/Common.hpp" #include "common/Common.hpp"
#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/api/Kraken.hpp"
namespace chatterino { namespace chatterino {
@ -141,6 +143,8 @@ void TwitchAccountManager::load()
if (user) if (user)
{ {
qDebug() << "Twitch user updated to" << newUsername; qDebug() << "Twitch user updated to" << newUsername;
getHelix()->update(user->getOAuthClient(), user->getOAuthToken());
getKraken()->update(user->getOAuthClient(), user->getOAuthToken());
this->currentUser_ = user; this->currentUser_ = user;
} }
else else

View file

@ -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

View file

@ -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

View file

@ -14,6 +14,8 @@
#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
#include "providers/twitch/TwitchParseCheerEmotes.hpp" #include "providers/twitch/TwitchParseCheerEmotes.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/api/Kraken.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Toasts.hpp" #include "singletons/Toasts.hpp"
@ -21,6 +23,7 @@
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include <rapidjson/document.h>
#include <IrcConnection> #include <IrcConnection>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
@ -454,35 +457,25 @@ void TwitchChannel::refreshTitle()
} }
this->titleRefreshedTime_ = QTime::currentTime(); this->titleRefreshedTime_ = QTime::currentTime();
QString url("https://api.twitch.tv/kraken/channels/" + roomID); const auto onSuccess = [this,
NetworkRequest::twitchRequest(url) weak = weakOf<Channel>(this)](const auto &channel) {
.onSuccess( ChannelPtr shared = weak.lock();
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome { if (!shared)
ChannelPtr shared = weak.lock(); {
if (!shared) return;
return Failure; }
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()) const auto onFailure = [] {};
{
return Failure;
}
{ getKraken()->getChannel(roomID, onSuccess, onFailure);
auto status = this->streamStatus_.access();
if (!rj::getSafe(statusIt->value, status->title))
{
return Failure;
}
}
this->liveStatusChanged.invoke();
return Success;
})
.execute();
} }
void TwitchChannel::refreshLiveStatus() void TwitchChannel::refreshLiveStatus()
@ -497,106 +490,73 @@ void TwitchChannel::refreshLiveStatus()
return; 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()); this->parseLiveStatus(live, stream);
NetworkRequest::twitchRequest(url) },
[] {
.onSuccess( // failure
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome { });
ChannelPtr shared = weak.lock();
if (!shared)
return Failure;
return this->parseLiveStatus(result.parseRapidJson());
})
.execute();
} }
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); 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(); auto status = this->streamStatus_.access();
status->viewerCount = stream["viewers"].GetUint(); status->viewerCount = stream.viewerCount;
status->game = stream["game"].GetString(); if (status->gameId != stream.gameId)
status->title = streamChannel["status"].GetString(); {
QDateTime since = QDateTime::fromString( status->gameId = stream.gameId;
stream["created_at"].GetString(), Qt::ISODate);
// 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()); auto diff = since.secsTo(QDateTime::currentDateTime());
status->uptime = QString::number(diff / 3600) + "h " + status->uptime = QString::number(diff / 3600) + "h " +
QString::number(diff % 3600 / 60) + "m"; QString::number(diff % 3600 / 60) + "m";
status->rerun = false; status->rerun = false;
if (stream.HasMember("stream_type")) status->streamType = 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;
}
}
}
} }
setLive(true);
this->setLive(true);
// Signal all listeners that the stream status has been updated // Signal all listeners that the stream status has been updated
this->liveStatusChanged.invoke(); this->liveStatusChanged.invoke();
return Success;
} }
void TwitchChannel::loadRecentMessages() void TwitchChannel::loadRecentMessages()

View file

@ -8,14 +8,15 @@
#include "common/UniqueAccess.hpp" #include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp" #include "common/UsernameSet.hpp"
#include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp"
#include "providers/twitch/api/Helix.hpp"
#include <rapidjson/document.h>
#include <IrcConnection> #include <IrcConnection>
#include <QColor> #include <QColor>
#include <QRegularExpression> #include <QRegularExpression>
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <mutex>
#include <pajlada/signals/signalholder.hpp> #include <pajlada/signals/signalholder.hpp>
#include <mutex>
#include <unordered_map> #include <unordered_map>
namespace chatterino { namespace chatterino {
@ -43,6 +44,7 @@ public:
unsigned viewerCount = 0; unsigned viewerCount = 0;
QString title; QString title;
QString game; QString game;
QString gameId;
QString uptime; QString uptime;
QString streamType; QString streamType;
}; };
@ -120,7 +122,7 @@ protected:
private: private:
// Methods // Methods
void refreshLiveStatus(); void refreshLiveStatus();
Outcome parseLiveStatus(const rapidjson::Document &document); void parseLiveStatus(bool live, const HelixStream &stream);
void refreshPubsub(); void refreshPubsub();
void refreshChatters(); void refreshChatters();
void refreshBadges(); void refreshBadges();

View file

@ -278,6 +278,7 @@ namespace {
return true; return true;
} }
} // namespace } // namespace
// Look through the results of // Look through the results of

View 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

View 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

View 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

View 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

View 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`

View file

@ -7,6 +7,7 @@
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "util/StreamLink.hpp" #include "util/StreamLink.hpp"
#include "widgets/helper/CommonTexts.hpp" #include "widgets/helper/CommonTexts.hpp"
@ -85,14 +86,17 @@ void Toasts::sendChannelNotification(const QString &channelName, Platform p)
} }
else else
{ {
this->fetchChannelAvatar( getHelix()->getUserByName(
channelName, channelName,
[channelName, sendChannelNotification](QString avatarLink) { [channelName, sendChannelNotification](const auto &user) {
DownloadManager *manager = new DownloadManager(); DownloadManager *manager = new DownloadManager();
manager->setFile(avatarLink, channelName); manager->setFile(user.profileImageUrl, channelName);
manager->connect(manager, manager->connect(manager,
&DownloadManager::downloadComplete, &DownloadManager::downloadComplete,
sendChannelNotification); sendChannelNotification);
},
[] {
// on failure
}); });
} }
} }
@ -206,53 +210,4 @@ void Toasts::sendWindowsNotification(const QString &channelName, Platform p)
#endif #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 } // namespace chatterino

View file

@ -29,9 +29,5 @@ private:
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
void sendWindowsNotification(const QString &channelName, Platform p); void sendWindowsNotification(const QString &channelName, Platform p);
#endif #endif
static void fetchChannelAvatar(
const QString channelName,
std::function<void(QString)> successCallback);
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -4,7 +4,6 @@
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#ifdef USEWINSDK #ifdef USEWINSDK
@ -159,7 +158,6 @@ AdvancedLoginWidget::AdvancedLoginWidget()
this->ui_.layout.addWidget(&this->ui_.instructionsLabel); this->ui_.layout.addWidget(&this->ui_.instructionsLabel);
this->ui_.layout.addLayout(&this->ui_.formLayout); this->ui_.layout.addLayout(&this->ui_.formLayout);
this->ui_.layout.addLayout(&this->ui_.buttonUpperRow.layout); this->ui_.layout.addLayout(&this->ui_.buttonUpperRow.layout);
this->ui_.layout.addLayout(&this->ui_.buttonLowerRow.layout);
this->refreshButtons(); this->refreshButtons();
@ -207,29 +205,10 @@ AdvancedLoginWidget::AdvancedLoginWidget()
LogInWithCredentials(userID, username, clientID, oauthToken); 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() void AdvancedLoginWidget::refreshButtons()
{ {
this->ui_.buttonLowerRow.fillInUserIDButton.setEnabled(
!this->ui_.usernameInput.text().isEmpty());
if (this->ui_.userIDInput.text().isEmpty() || if (this->ui_.userIDInput.text().isEmpty() ||
this->ui_.usernameInput.text().isEmpty() || this->ui_.usernameInput.text().isEmpty() ||
this->ui_.clientIDInput.text().isEmpty() || this->ui_.clientIDInput.text().isEmpty() ||

View file

@ -57,12 +57,6 @@ public:
QPushButton addUserButton; QPushButton addUserButton;
QPushButton clearFieldsButton; QPushButton clearFieldsButton;
} buttonUpperRow; } buttonUpperRow;
struct {
QHBoxLayout layout;
QPushButton fillInUserIDButton;
} buttonLowerRow;
} ui_; } ui_;
}; };

View file

@ -4,7 +4,6 @@
#include "common/Channel.hpp" #include "common/Channel.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"

View file

@ -5,8 +5,9 @@
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightBlacklistUser.hpp" #include "controllers/highlights/HighlightBlacklistUser.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/api/Kraken.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "util/LayoutCreator.hpp" #include "util/LayoutCreator.hpp"
@ -22,9 +23,9 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#define TEXT_FOLLOWERS "Followers: " const QString TEXT_VIEWS("Views: %1");
#define TEXT_VIEWS "Views: " const QString TEXT_FOLLOWERS("Followers: %1");
#define TEXT_CREATED "Created: " const QString TEXT_CREATED("Created: %1");
#define TEXT_USER_ID "ID: " #define TEXT_USER_ID "ID: "
#define TEXT_UNAVAILABLE "(not available)" #define TEXT_UNAVAILABLE "(not available)"
@ -92,10 +93,11 @@ UserInfoPopup::UserInfoPopup()
this->ui_.userIDLabel->setPalette(palette); this->ui_.userIDLabel->setPalette(palette);
} }
vbox.emplace<Label>(TEXT_VIEWS).assign(&this->ui_.viewCountLabel); vbox.emplace<Label>(TEXT_VIEWS.arg(""))
vbox.emplace<Label>(TEXT_FOLLOWERS) .assign(&this->ui_.viewCountLabel);
vbox.emplace<Label>(TEXT_FOLLOWERS.arg(""))
.assign(&this->ui_.followerCountLabel); .assign(&this->ui_.followerCountLabel);
vbox.emplace<Label>(TEXT_CREATED) vbox.emplace<Label>(TEXT_CREATED.arg(""))
.assign(&this->ui_.createdDateLabel); .assign(&this->ui_.createdDateLabel);
} }
} }
@ -260,10 +262,6 @@ void UserInfoPopup::installEvents()
this->ui_.follow, &QCheckBox::stateChanged, [this](int) mutable { this->ui_.follow, &QCheckBox::stateChanged, [this](int) mutable {
auto currentUser = getApp()->accounts->twitch.getCurrent(); auto currentUser = getApp()->accounts->twitch.getCurrent();
QUrl requestUrl("https://api.twitch.tv/kraken/users/" +
currentUser->getUserId() + "/follows/channels/" +
this->userId_);
const auto reenableFollowCheckbox = [this] { const auto reenableFollowCheckbox = [this] {
this->ui_.follow->setEnabled(true); // this->ui_.follow->setEnabled(true); //
}; };
@ -383,14 +381,12 @@ void UserInfoPopup::updateUserData()
{ {
std::weak_ptr<bool> hack = this->hack_; 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 can occur when the account doesn't exist.
this->ui_.followerCountLabel->setText(TEXT_FOLLOWERS + this->ui_.followerCountLabel->setText(
QString(TEXT_UNAVAILABLE)); TEXT_FOLLOWERS.arg(TEXT_UNAVAILABLE));
this->ui_.viewCountLabel->setText(TEXT_VIEWS + this->ui_.viewCountLabel->setText(TEXT_VIEWS.arg(TEXT_UNAVAILABLE));
QString(TEXT_UNAVAILABLE)); this->ui_.createdDateLabel->setText(TEXT_CREATED.arg(TEXT_UNAVAILABLE));
this->ui_.createdDateLabel->setText(TEXT_CREATED +
QString(TEXT_UNAVAILABLE));
this->ui_.nameLabel->setText(this->userName_); this->ui_.nameLabel->setText(this->userName_);
@ -399,39 +395,38 @@ void UserInfoPopup::updateUserData()
this->ui_.userIDLabel->setProperty("copy-text", this->ui_.userIDLabel->setProperty("copy-text",
QString(TEXT_UNAVAILABLE)); QString(TEXT_UNAVAILABLE));
}; };
const auto onIdFetched = [this, hack](QString id) { const auto onUserFetched = [this, hack](const auto &user) {
auto currentUser = getApp()->accounts->twitch.getCurrent(); auto currentUser = getApp()->accounts->twitch.getCurrent();
this->userId_ = id; this->userId_ = user.id;
this->ui_.userIDLabel->setText(TEXT_USER_ID + id); this->ui_.userIDLabel->setText(TEXT_USER_ID + user.id);
this->ui_.userIDLabel->setProperty("copy-text", id); this->ui_.userIDLabel->setProperty("copy-text", user.id);
// don't wait for the request to complete, just put the user id in the card
// right away
QString url("https://api.twitch.tv/kraken/channels/" + id); this->ui_.viewCountLabel->setText(TEXT_VIEWS.arg(user.viewCount));
getKraken()->getUser(
NetworkRequest::twitchRequest(url) user.id,
.caller(this) [this](const auto &user) {
.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_.createdDateLabel->setText( this->ui_.createdDateLabel->setText(
TEXT_CREATED + TEXT_CREATED.arg(user.createdAt.section("T", 0, 0)));
obj.value("created_at").toString().section("T", 0, 0)); },
[] {
// failure
});
this->loadAvatar(user.profileImageUrl);
this->loadAvatar(QUrl(obj.value("logo").toString())); getHelix()->getUserFollowers(
user.id,
return Success; [this](const auto &followers) {
}) this->ui_.followerCountLabel->setText(
.execute(); TEXT_FOLLOWERS.arg(followers.total));
},
[] {
// on failure
});
// get follow state // get follow state
currentUser->checkFollow(id, [this, hack](auto result) { currentUser->checkFollow(user.id, [this, hack](auto result) {
if (hack.lock()) if (hack.lock())
{ {
if (result != FollowResult_Failed) if (result != FollowResult_Failed)
@ -447,7 +442,7 @@ void UserInfoPopup::updateUserData()
bool isIgnoring = false; bool isIgnoring = false;
for (const auto &ignoredUser : currentUser->getIgnores()) for (const auto &ignoredUser : currentUser->getIgnores())
{ {
if (id == ignoredUser.id) if (user.id == ignoredUser.id)
{ {
isIgnoring = true; isIgnoring = true;
break; break;
@ -479,8 +474,8 @@ void UserInfoPopup::updateUserData()
this->ui_.ignoreHighlights->setChecked(isIgnoringHighlights); this->ui_.ignoreHighlights->setChecked(isIgnoringHighlights);
}; };
PartialTwitchUser::byName(this->userName_) getHelix()->getUserByName(this->userName_, onUserFetched,
.getId(onIdFetched, onIdFetchFailed, this); onUserFetchFailed);
this->ui_.follow->setEnabled(false); this->ui_.follow->setEnabled(false);
this->ui_.ignore->setEnabled(false); this->ui_.ignore->setEnabled(false);