From 278a00a700dd8e1009d7fd2b2dfbbad2f7cf448f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sat, 30 Jan 2021 15:39:01 +0100 Subject: [PATCH] Implement /marker command (#2360) This command works the same as it does on Twitch web chat - it creates a streamer marker at the current timestamp with an optional description --- CHANGELOG.md | 1 + .../commands/CommandController.cpp | 73 +++++++++++++++++++ src/providers/twitch/api/Helix.cpp | 60 +++++++++++++++ src/providers/twitch/api/Helix.hpp | 27 +++++++ src/providers/twitch/api/README.md | 14 +++- 5 files changed, 172 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2075990a..d5e00625c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Major: Added "Channel Filters". See https://wiki.chatterino.com/Filters/ for how they work or how to configure them. (#1748, #2083, #2090, #2200) - 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: Color mentions to match the mentioned users. You can disable this by unchecking "Color @usernames" under `Settings -> General -> Advanced (misc.)`. (#1963, #2284) +- Minor: Added `/marker` command - similar to webchat, it creates a stream marker. (#2360) - Minor: Added `/chatters` command showing chatter count. (#2344) - Minor: Added a button to the split context menu to open the moderation view for a channel when the account selected has moderator permissions. (#2321) - Minor: Made BetterTTV emote tooltips use authors' display name. (#2267) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index f3f9c8a71..14b98763c 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -16,6 +16,7 @@ #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "util/CombinePath.hpp" +#include "util/FormatTime.hpp" #include "util/Twitch.hpp" #include "widgets/Window.hpp" #include "widgets/dialogs/UserInfoPopup.hpp" @@ -465,6 +466,78 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); + + this->registerCommand("/marker", [](const QStringList &words, + auto channel) { + if (!channel->isTwitchChannel()) + { + return ""; + } + + // Avoid Helix calls without Client ID and/or OAuth Token + if (getApp()->accounts->twitch.getCurrent()->isAnon()) + { + channel->addMessage(makeSystemMessage( + "You need to be logged in to create stream markers!")); + return ""; + } + + auto *twitchChannel = dynamic_cast(channel.get()); + + // Exact same message as in webchat + if (!twitchChannel->isLive()) + { + channel->addMessage(makeSystemMessage( + "You can only add stream markers during live streams. Try " + "again when the channel is live streaming.")); + return ""; + } + + auto arguments = words; + arguments.removeFirst(); + + getHelix()->createStreamMarker( + // Limit for description is 140 characters, webchat just crops description + // if it's >140 characters, so we're doing the same thing + twitchChannel->roomId(), arguments.join(" ").left(140), + [channel, arguments](const HelixStreamMarker &streamMarker) { + channel->addMessage(makeSystemMessage( + QString("Successfully added a stream marker at %1%2") + .arg(formatTime(streamMarker.positionSeconds)) + .arg(streamMarker.description.isEmpty() + ? "" + : QString(": \"%1\"") + .arg(streamMarker.description)))); + }, + [channel](auto error) { + QString errorMessage("Failed to create stream marker - "); + + switch (error) + { + case HelixStreamMarkerError::UserNotAuthorized: { + errorMessage += + "you don't have permission to perform that action."; + } + break; + + case HelixStreamMarkerError::UserNotAuthenticated: { + errorMessage += "you need to re-authenticate."; + } + break; + + // This would most likely happen if the service is down, or if the JSON payload returned has changed format + case HelixStreamMarkerError::Unknown: + default: { + errorMessage += "an unknown error occurred."; + } + break; + } + + channel->addMessage(makeSystemMessage(errorMessage)); + }); + + return ""; + }); } void CommandController::save() diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 51c2c27d3..827de4a09 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -442,6 +442,66 @@ void Helix::getChannel(QString broadcasterId, .execute(); } +void Helix::createStreamMarker( + QString broadcasterId, QString description, + ResultCallback successCallback, + std::function failureCallback) +{ + QJsonObject payload; + + if (!description.isEmpty()) + { + payload.insert("description", QJsonValue(description)); + } + payload.insert("user_id", QJsonValue(broadcasterId)); + + this->makeRequest("streams/markers", QUrlQuery()) + .type(NetworkRequestType::Post) + .header("Content-Type", "application/json") + .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(HelixStreamMarkerError::Unknown); + return Failure; + } + + HelixStreamMarker streamMarker(data.toArray()[0].toObject()); + + successCallback(streamMarker); + return Success; + }) + .onError([failureCallback](NetworkResult result) { + switch (result.status()) + { + case 403: { + // User isn't a Channel Editor, so he can't create markers + failureCallback(HelixStreamMarkerError::UserNotAuthorized); + } + break; + + case 401: { + // User does not have the required scope to be able to create stream markers, user must reauthenticate + failureCallback( + HelixStreamMarkerError::UserNotAuthenticated); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Failed to create a stream marker: " + << result.status() << result.getData(); + failureCallback(HelixStreamMarkerError::Unknown); + } + break; + } + }) + .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 14a971ba7..4249c5a3e 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -165,12 +165,33 @@ struct HelixChannel { } }; +struct HelixStreamMarker { + QString createdAt; + QString description; + QString id; + int positionSeconds; + + explicit HelixStreamMarker(QJsonObject jsonObject) + : createdAt(jsonObject.value("created_at").toString()) + , description(jsonObject.value("description").toString()) + , id(jsonObject.value("id").toString()) + , positionSeconds(jsonObject.value("position_seconds").toInt()) + { + } +}; + enum class HelixClipError { Unknown, ClipsDisabled, UserNotAuthenticated, }; +enum class HelixStreamMarkerError { + Unknown, + UserNotAuthorized, + UserNotAuthenticated, +}; + class Helix final : boost::noncopyable { public: @@ -242,6 +263,12 @@ public: ResultCallback successCallback, HelixFailureCallback failureCallback); + // https://dev.twitch.tv/docs/api/reference/#create-stream-marker + void createStreamMarker( + QString broadcasterId, QString description, + ResultCallback successCallback, + std::function 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 4bec12259..bf3eac98e 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -84,7 +84,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams URL: https://dev.twitch.tv/docs/api/reference#create-user-follows Requires `user:edit:follows` scope - * We implement this in `providers/twitch/api/Helix.cpp followUser` + * We implement this in `providers/twitch/api/Helix.cpp followUser` Used in: * `widgets/dialogs/UserInfoPopup.cpp` to follow a user by ticking follow checkbox in usercard * `controllers/commands/CommandController.cpp` in /follow command @@ -93,7 +93,7 @@ Requires `user:edit:follows` scope URL: https://dev.twitch.tv/docs/api/reference#delete-user-follows Requires `user:edit:follows` scope - * We implement this in `providers/twitch/api/Helix.cpp unfollowUser` + * We implement this in `providers/twitch/api/Helix.cpp unfollowUser` Used in: * `widgets/dialogs/UserInfoPopup.cpp` to unfollow a user by unticking follow checkbox in usercard * `controllers/commands/CommandController.cpp` in /unfollow command @@ -102,7 +102,7 @@ Requires `user:edit:follows` scope URL: https://dev.twitch.tv/docs/api/reference#create-clip Requires `clips:edit` scope - * We implement this in `providers/twitch/api/Helix.cpp createClip` + * We implement this in `providers/twitch/api/Helix.cpp createClip` Used in: * `TwitchChannel` to create a clip of a live broadcast @@ -113,6 +113,14 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information Used in: * `TwitchChannel` to refresh stream title +### Create Stream Marker +URL: https://dev.twitch.tv/docs/api/reference/#create-stream-marker +Requires `user:edit:broadcast` scope + + * We implement this in `providers/twitch/api/Helix.cpp createStreamMarker` + Used in: + * `controllers/commands/CommandController.cpp` in /marker command + ## TMI The TMI api is undocumented.