From 9a8b85e338ba4fe3b2bb1eee81f3f6f05387959f Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 14 Mar 2020 07:13:57 -0400 Subject: [PATCH] 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. --- chatterino.pro | 8 +- .../commands/CommandController.cpp | 36 +- .../notifications/NotificationController.cpp | 106 +++-- .../notifications/NotificationController.hpp | 1 + src/main.cpp | 5 + src/providers/twitch/PartialTwitchUser.cpp | 85 ---- src/providers/twitch/PartialTwitchUser.hpp | 30 -- src/providers/twitch/TwitchAccount.cpp | 52 ++- src/providers/twitch/TwitchAccountManager.cpp | 4 + src/providers/twitch/TwitchApi.cpp | 85 ---- src/providers/twitch/TwitchApi.hpp | 19 - src/providers/twitch/TwitchChannel.cpp | 174 ++++----- src/providers/twitch/TwitchChannel.hpp | 8 +- .../twitch/TwitchParseCheerEmotes.cpp | 1 + src/providers/twitch/api/Helix.cpp | 368 ++++++++++++++++++ src/providers/twitch/api/Helix.hpp | 198 ++++++++++ src/providers/twitch/api/Kraken.cpp | 104 +++++ src/providers/twitch/api/Kraken.hpp | 61 +++ src/providers/twitch/api/README.md | 125 ++++++ src/singletons/Toasts.cpp | 59 +-- src/singletons/Toasts.hpp | 4 - src/widgets/dialogs/LoginDialog.cpp | 21 - src/widgets/dialogs/LoginDialog.hpp | 6 - src/widgets/dialogs/LogsPopup.cpp | 1 - src/widgets/dialogs/UserInfoPopup.cpp | 87 ++--- 25 files changed, 1076 insertions(+), 572 deletions(-) delete mode 100644 src/providers/twitch/PartialTwitchUser.cpp delete mode 100644 src/providers/twitch/PartialTwitchUser.hpp delete mode 100644 src/providers/twitch/TwitchApi.cpp delete mode 100644 src/providers/twitch/TwitchApi.hpp create mode 100644 src/providers/twitch/api/Helix.cpp create mode 100644 src/providers/twitch/api/Helix.hpp create mode 100644 src/providers/twitch/api/Kraken.cpp create mode 100644 src/providers/twitch/api/Kraken.hpp create mode 100644 src/providers/twitch/api/README.md diff --git a/chatterino.pro b/chatterino.pro index 41327803c..6ec03fee5 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -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 \ diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 92d89e5c2..fa8a08e95 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -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 ""; diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index 72d7b0124..2578558df 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -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) diff --git a/src/controllers/notifications/NotificationController.hpp b/src/controllers/notifications/NotificationController.hpp index a9c048efe..e8d0c4ef7 100644 --- a/src/controllers/notifications/NotificationController.hpp +++ b/src/controllers/notifications/NotificationController.hpp @@ -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 fakeTwitchChannels; QTimer *liveStatusTimer_; diff --git a/src/main.cpp b/src/main.cpp index 463465894..dfb42904b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp deleted file mode 100644 index 5407fbada..000000000 --- a/src/providers/twitch/PartialTwitchUser.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "providers/twitch/PartialTwitchUser.hpp" - -#include "common/Common.hpp" -#include "common/NetworkRequest.hpp" -#include "providers/twitch/TwitchCommon.hpp" - -#include -#include - -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 successCallback, - const QObject *caller) -{ - getId( - successCallback, [] {}, caller); -} -void PartialTwitchUser::getId(std::function successCallback, - std::function 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 diff --git a/src/providers/twitch/PartialTwitchUser.hpp b/src/providers/twitch/PartialTwitchUser.hpp deleted file mode 100644 index 6537bb279..000000000 --- a/src/providers/twitch/PartialTwitchUser.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -#include - -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 successCallback, - const QObject *caller = nullptr); - - void getId(std::function successCallback, - std::function failureCallback, - const QObject *caller = nullptr); -}; - -} // namespace chatterino diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 13b4d4092..1b0f2c0ff 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -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 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 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 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, diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index c3b2ee69c..3813e9822 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -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 diff --git a/src/providers/twitch/TwitchApi.cpp b/src/providers/twitch/TwitchApi.cpp deleted file mode 100644 index 6c3b6b6ee..000000000 --- a/src/providers/twitch/TwitchApi.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "providers/twitch/TwitchApi.hpp" - -#include "common/Common.hpp" -#include "common/NetworkRequest.hpp" -#include "providers/twitch/TwitchCommon.hpp" - -#include -#include - -namespace chatterino { - -void TwitchApi::findUserId(const QString user, - std::function 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 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 diff --git a/src/providers/twitch/TwitchApi.hpp b/src/providers/twitch/TwitchApi.hpp deleted file mode 100644 index 8b1b85c13..000000000 --- a/src/providers/twitch/TwitchApi.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -namespace chatterino { - -class TwitchApi -{ -public: - static void findUserId(const QString user, - std::function callback); - static void findUserName(const QString userid, - std::function callback); - -private: -}; - -} // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index d305f0c43..05dac0123 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -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 #include #include #include @@ -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(this)](auto result) -> Outcome { - ChannelPtr shared = weak.lock(); - if (!shared) - return Failure; + const auto onSuccess = [this, + weak = weakOf(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(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(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(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() diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 026c90717..fc40ad325 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -8,14 +8,15 @@ #include "common/UniqueAccess.hpp" #include "common/UsernameSet.hpp" #include "providers/twitch/TwitchEmotes.hpp" +#include "providers/twitch/api/Helix.hpp" -#include #include #include #include #include -#include #include + +#include #include 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(); diff --git a/src/providers/twitch/TwitchParseCheerEmotes.cpp b/src/providers/twitch/TwitchParseCheerEmotes.cpp index 5258baf9a..9667ffd2a 100644 --- a/src/providers/twitch/TwitchParseCheerEmotes.cpp +++ b/src/providers/twitch/TwitchParseCheerEmotes.cpp @@ -278,6 +278,7 @@ namespace { return true; } + } // namespace // Look through the results of diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp new file mode 100644 index 000000000..974b62128 --- /dev/null +++ b/src/providers/twitch/api/Helix.cpp @@ -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> 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 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 successCallback, + HelixFailureCallback failureCallback) +{ + QStringList userIds; + QStringList userLogins{userId}; + + this->fetchUsers( + userIds, userLogins, + [successCallback, + failureCallback](const std::vector &users) { + if (users.empty()) + { + failureCallback(); + return; + } + successCallback(users[0]); + }, + failureCallback); +} + +void Helix::getUserById(QString userId, + ResultCallback 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 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 successCallback, + HelixFailureCallback failureCallback) +{ + this->fetchUsersFollows("", userId, successCallback, failureCallback); +} + +void Helix::getUserFollow( + QString userId, QString targetId, + ResultCallback 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> 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 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 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 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> 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 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 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 diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp new file mode 100644 index 000000000..bfe54b699 --- /dev/null +++ b/src/providers/twitch/api/Helix.hpp @@ -0,0 +1,198 @@ +#pragma once + +#include "common/NetworkRequest.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace chatterino { + +using HelixFailureCallback = std::function; +template +using ResultCallback = std::function; + +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 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> successCallback, + HelixFailureCallback failureCallback); + void getUserByName(QString userName, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + void getUserById(QString userId, ResultCallback successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#get-users-follows + void fetchUsersFollows( + QString fromId, QString toId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + void getUserFollowers( + QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + void getUserFollow( + QString userId, QString targetId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#get-streams + void fetchStreams(QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + + void getStreamById(QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + void getStreamByName(QString userName, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#get-games + void fetchGames(QStringList gameIds, QStringList gameNames, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + + void getGameById(QString gameId, ResultCallback 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 diff --git a/src/providers/twitch/api/Kraken.cpp b/src/providers/twitch/api/Kraken.cpp new file mode 100644 index 000000000..be7e98b36 --- /dev/null +++ b/src/providers/twitch/api/Kraken.cpp @@ -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 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 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 diff --git a/src/providers/twitch/api/Kraken.hpp b/src/providers/twitch/api/Kraken.hpp new file mode 100644 index 000000000..047173d6e --- /dev/null +++ b/src/providers/twitch/api/Kraken.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "common/NetworkRequest.hpp" + +#include +#include +#include +#include + +#include + +namespace chatterino { + +using KrakenFailureCallback = std::function; +template +using ResultCallback = std::function; + +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 resultCallback, + KrakenFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/v5/reference/users#get-user-by-id + void getUser(QString userId, ResultCallback 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 diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md new file mode 100644 index 000000000..a95ac6bcc --- /dev/null +++ b/src/providers/twitch/api/README.md @@ -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` diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp index def5df00f..9316a8791 100644 --- a/src/singletons/Toasts.cpp +++ b/src/singletons/Toasts.cpp @@ -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 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 diff --git a/src/singletons/Toasts.hpp b/src/singletons/Toasts.hpp index 43c6d1da8..5ef792c77 100644 --- a/src/singletons/Toasts.hpp +++ b/src/singletons/Toasts.hpp @@ -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 successCallback); }; } // namespace chatterino diff --git a/src/widgets/dialogs/LoginDialog.cpp b/src/widgets/dialogs/LoginDialog.cpp index f96c86342..3666ea657 100644 --- a/src/widgets/dialogs/LoginDialog.cpp +++ b/src/widgets/dialogs/LoginDialog.cpp @@ -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() || diff --git a/src/widgets/dialogs/LoginDialog.hpp b/src/widgets/dialogs/LoginDialog.hpp index a6819e3af..796b98cac 100644 --- a/src/widgets/dialogs/LoginDialog.hpp +++ b/src/widgets/dialogs/LoginDialog.hpp @@ -57,12 +57,6 @@ public: QPushButton addUserButton; QPushButton clearFieldsButton; } buttonUpperRow; - - struct { - QHBoxLayout layout; - - QPushButton fillInUserIDButton; - } buttonLowerRow; } ui_; }; diff --git a/src/widgets/dialogs/LogsPopup.cpp b/src/widgets/dialogs/LogsPopup.cpp index 18e1f1a04..0afd6b9b4 100644 --- a/src/widgets/dialogs/LogsPopup.cpp +++ b/src/widgets/dialogs/LogsPopup.cpp @@ -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" diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 3b8307024..a4d0f2220 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -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 #include -#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