diff --git a/CHANGELOG.md b/CHANGELOG.md index df020dd91..0bbd58aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Major: Added clip creation support. You can create clips with `/clip` command, `Alt+X` keybind or `Create a clip` option in split header's context menu. This requires a new authentication scope so re-authentication will be required to use it. (#2271, #2377, #2528) - Major: Added "Channel Filters". See https://wiki.chatterino.com/Filters/ for how they work or how to configure them. (#1748, #2083, #2090, #2200, #2225) - Major: Added Streamer Mode configuration (under `Settings -> General`), where you can select which features of Chatterino should behave differently when you are in Streamer Mode. (#2001, #2316, #2342, #2376) +- Major: Add `/settitle` and `/setgame` commands, originally made for Mm2PL/Dankerino. (#2534) - Major: Color mentions to match the mentioned users. You can disable this by unchecking "Color @usernames" under `Settings -> General -> Advanced (misc.)`. (#1963, #2284) - Major: Commands `/ignore` and `/unignore` have been renamed to `/block` and `/unblock` in order to keep consistency with Twitch's terms. (#2370) - Major: Added support for bit emotes - the ones you unlock after cheering to streamer. (#2550) diff --git a/src/common/NetworkCommon.hpp b/src/common/NetworkCommon.hpp index 5e4f5678a..1ecfe30ce 100644 --- a/src/common/NetworkCommon.hpp +++ b/src/common/NetworkCommon.hpp @@ -19,6 +19,7 @@ enum class NetworkRequestType { Post, Put, Delete, + Patch, }; } // namespace chatterino diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp index e61bc4fe9..b141cd853 100644 --- a/src/common/NetworkPrivate.cpp +++ b/src/common/NetworkPrivate.cpp @@ -116,6 +116,19 @@ void loadUncached(const std::shared_ptr &data) return NetworkManager::accessManager.post( data->request_, data->payload_); } + case NetworkRequestType::Patch: + if (data->multiPartPayload_) + { + assert(data->payload_.isNull()); + + return NetworkManager::accessManager.sendCustomRequest( + data->request_, "PATCH", data->multiPartPayload_); + } + else + { + return NetworkManager::accessManager.sendCustomRequest( + data->request_, "PATCH", data->payload_); + } } return nullptr; }(); diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 2d07dff1d..644bb5b39 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -643,7 +643,85 @@ void CommandController::initialize(Settings &, Paths &paths) getApp()->windows->getMainWindow().getNotebook().getSelectedPage()); currentPage->getSelectedSplit()->getChannelView().clearMessages(); + return ""; + }); + this->registerCommand("/settitle", [](const QStringList &words, + ChannelPtr channel) { + if (words.size() < 2) + { + channel->addMessage( + makeSystemMessage("Usage: /settitle .")); + return ""; + } + if (auto twitchChannel = dynamic_cast(channel.get())) + { + auto status = twitchChannel->accessStreamStatus(); + auto title = words.mid(1).join(" "); + getHelix()->updateChannel( + twitchChannel->roomId(), "", "", title, + [channel, title](NetworkResult) { + channel->addMessage(makeSystemMessage( + QString("Updated title to %1").arg(title))); + }, + [channel] { + channel->addMessage( + makeSystemMessage("Title update failed! Are you " + "missing the required scope?")); + }); + } + else + { + channel->addMessage(makeSystemMessage( + "Unable to set title of non-Twitch channel.")); + } + return ""; + }); + this->registerCommand("/setgame", [](const QStringList &words, + ChannelPtr channel) { + if (words.size() < 2) + { + channel->addMessage( + makeSystemMessage("Usage: /setgame .")); + return ""; + } + if (auto twitchChannel = dynamic_cast(channel.get())) + { + getHelix()->fetchGames( + QStringList(), {words.mid(1).join(" ")}, + [channel, twitchChannel](std::vector games) { + if (games.size() == 0) + { + channel->addMessage( + makeSystemMessage("Game not found.")); + } + else // 0 or 1 games + { + auto status = twitchChannel->accessStreamStatus(); + getHelix()->updateChannel( + twitchChannel->roomId(), games.at(0).id, "", "", + [channel, games](NetworkResult) { + channel->addMessage(makeSystemMessage( + QString("Updated game to %1") + .arg(games.at(0).name))); + }, + [channel] { + channel->addMessage(makeSystemMessage( + "Game update failed! Are you " + "missing the required scope?")); + }); + } + }, + [channel] { + channel->addMessage( + makeSystemMessage("Failed to look up game.")); + }); + } + else + { + channel->addMessage( + makeSystemMessage("Unable to set game of non-Twitch channel.")); + } return ""; }); } diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 3b6564eae..9db9e32d3 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -578,6 +578,48 @@ void Helix::unblockUser(QString targetUserId, .execute(); } +void Helix::updateChannel(QString broadcasterId, QString gameId, + QString language, QString title, + std::function successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + auto data = QJsonDocument(); + auto obj = QJsonObject(); + if (!gameId.isEmpty()) + { + obj.insert("game_id", gameId); + } + if (!language.isEmpty()) + { + obj.insert("broadcaster_language", language); + } + if (!title.isEmpty()) + { + obj.insert("title", title); + } + + if (title.isEmpty() && gameId.isEmpty() && language.isEmpty()) + { + qCDebug(chatterinoCommon) << "Tried to update channel with no changes!"; + return; + } + + data.setObject(obj); + urlQuery.addQueryItem("broadcaster_id", broadcasterId); + this->makeRequest("channels", urlQuery) + .type(NetworkRequestType::Patch) + .header("Content-Type", "application/json") + .payload(data.toJson()) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + successCallback(result); + return Success; + }) + .onError([failureCallback](NetworkResult result) { + failureCallback(); + }) + .execute(); +} NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) { assert(!url.startsWith("/")); diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index eafa04ab9..38fc153d1 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -296,6 +296,12 @@ public: std::function successCallback, HelixFailureCallback failureCallback); + // https://dev.twitch.tv/docs/api/reference#modify-channel-information + void updateChannel(QString broadcasterId, QString gameId, QString language, + QString title, + std::function successCallback, + HelixFailureCallback failureCallback); + void update(QString clientId, QString oauthToken); static void initialize(); diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index f86aa986f..ddea624ef 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -104,6 +104,16 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information Used in: - `TwitchChannel` to refresh stream title +### Update Channel + +URL: https://dev.twitch.tv/docs/api/reference#modify-channel-information +Requires `channel:manage:broadcast` scope + +- We implement this in `providers/twitch/api/Helix.cpp updateChannel` + Used in: + - `/setgame` to update the game in the current channel + - `/settitle` to update the title in the current channel + ### Create Stream Marker URL: https://dev.twitch.tv/docs/api/reference/#create-stream-marker