mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-07-01 00:41:07 -05:00
Add Twitch action
Allows you to set the stream title and stream category
This commit is contained in:
parent
9df6963f08
commit
a5050d4810
1
.github/scripts/.Wingetfile
vendored
1
.github/scripts/.Wingetfile
vendored
|
|
@ -1,3 +1,4 @@
|
|||
package '7zip.7zip', path: '7-zip', bin: '7z'
|
||||
package 'cmake', path: 'Cmake\bin', bin: 'cmake'
|
||||
package 'innosetup', path: 'Inno Setup 6', bin: 'iscc'
|
||||
package 'OpenSSL', path: 'OpenSSL', bin: 'openssl'
|
||||
|
|
|
|||
|
|
@ -767,6 +767,11 @@ AdvSceneSwitcher.action.sceneLock.type.lock="lock"
|
|||
AdvSceneSwitcher.action.sceneLock.type.unlock="unlock"
|
||||
AdvSceneSwitcher.action.sceneLock.type.toggle="toggle lock of"
|
||||
AdvSceneSwitcher.action.sceneLock.entry="On{{scenes}}{{actions}}{{sources}}"
|
||||
AdvSceneSwitcher.action.twitch="Twitch"
|
||||
AdvSceneSwitcher.action.twitch.type.title="Set stream title"
|
||||
AdvSceneSwitcher.action.twitch.type.category="Set stream category"
|
||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
|
||||
AdvSceneSwitcher.action.twitch.entry="On{{account}}{{actions}}{{text}}{{category}}{{manualCategorySearch}}"
|
||||
|
||||
; Transition Tab
|
||||
AdvSceneSwitcher.transitionTab.title="Transition"
|
||||
|
|
@ -1131,6 +1136,39 @@ AdvSceneSwitcher.osc.message.type.false="False"
|
|||
AdvSceneSwitcher.osc.message.type.infinity="Infinitum"
|
||||
AdvSceneSwitcher.osc.message.type.null="Nil"
|
||||
|
||||
AdvSceneSwitcher.twitchToken.name="Account name:"
|
||||
AdvSceneSwitcher.twitchToken.nameNotAvailable="Account already in use"
|
||||
AdvSceneSwitcher.twitchToken.select="--select Twitch connection--"
|
||||
AdvSceneSwitcher.twitchToken.add="Add new connection"
|
||||
AdvSceneSwitcher.twitchToken.configure="Configure Twitch connection settings"
|
||||
AdvSceneSwitcher.twitchToken.value="Token:"
|
||||
AdvSceneSwitcher.twitchToken.invalid="Invalid twitch token"
|
||||
AdvSceneSwitcher.twitchToken.request="Request token"
|
||||
AdvSceneSwitcher.twitchToken.request.waiting="Waiting for token approval ..."
|
||||
AdvSceneSwitcher.twitchToken.request.fail="Failed to get token!"
|
||||
AdvSceneSwitcher.twitchToken.request.fail.browser="Authentication failed! (%1)\nYou can close this window now."
|
||||
AdvSceneSwitcher.twitchToken.request.fail.stateMismatch="State mismatch"
|
||||
AdvSceneSwitcher.twitchToken.request.success="Successfully received token!"
|
||||
AdvSceneSwitcher.twitchToken.request.success.browser="Authentication successful! You can close this window now."
|
||||
AdvSceneSwitcher.twitchToken.request.notSet="No token set - Please request new token!"
|
||||
AdvSceneSwitcher.twitchToken.permissions="Token permissions:"
|
||||
AdvSceneSwitcher.twitchToken.analytics.readExtensions="View analytics data for the Twitch Extensions owned by the authenticated account."
|
||||
AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the games owned by the authenticated account."
|
||||
AdvSceneSwitcher.twitchToken.bits.read="View Bits information for a channel."
|
||||
AdvSceneSwitcher.twitchToken.channel.manageBroadcast="Manage a channel’s broadcast configuration, including updating channel configuration and managing stream markers and stream tags."
|
||||
|
||||
AdvSceneSwitcher.twitchCategories.fetchStart="Fetching stream categories ..."
|
||||
AdvSceneSwitcher.twitchCategories.fetchStatus="Got %1 stream categories."
|
||||
AdvSceneSwitcher.twitchCategories.fetchSkip="Skip fetching more stream categories"
|
||||
AdvSceneSwitcher.twitchCategories.fetchStop="Stop"
|
||||
AdvSceneSwitcher.twitchCategories.search="Search for stream category ..."
|
||||
AdvSceneSwitcher.twitchCategories.name="Category name:"
|
||||
AdvSceneSwitcher.twitchCategories.manualSearch="Search for additional category and it add to the selection list"
|
||||
AdvSceneSwitcher.twitchCategories.noViewersCategoriesMissing="Categories without any viewers will have to be searched for manually"
|
||||
AdvSceneSwitcher.twitchCategories.searchFailed="No new categories were found for \"%1\"."
|
||||
AdvSceneSwitcher.twitchCategories.searchSuccess="%1 new categories were found for \"%2\" and were added to the list!"
|
||||
AdvSceneSwitcher.twitchCategories.select="--select category--"
|
||||
|
||||
AdvSceneSwitcher.selectScene="--select scene--"
|
||||
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
|
||||
AdvSceneSwitcher.selectCurrentScene="Current Scene"
|
||||
|
|
|
|||
8
data/res/images/DarkSearch.svg
Normal file
8
data/res/images/DarkSearch.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
|
||||
<defs>
|
||||
</defs>
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||
<path d="M 87.803 77.194 L 68.212 57.602 c 9.5 -14.422 7.912 -34.054 -4.766 -46.732 c 0 0 -0.001 0 -0.001 0 c -14.495 -14.493 -38.08 -14.494 -52.574 0 c -14.494 14.495 -14.494 38.079 0 52.575 c 7.248 7.247 16.767 10.87 26.287 10.87 c 7.134 0 14.267 -2.035 20.445 -6.104 l 19.591 19.591 C 78.659 89.267 80.579 90 82.498 90 s 3.84 -0.733 5.305 -2.197 C 90.732 84.873 90.732 80.124 87.803 77.194 z M 21.48 52.837 c -8.645 -8.646 -8.645 -22.713 0 -31.358 c 4.323 -4.322 10 -6.483 15.679 -6.483 c 5.678 0 11.356 2.161 15.678 6.483 c 8.644 8.644 8.645 22.707 0.005 31.352 c -0.002 0.002 -0.004 0.003 -0.005 0.005 c -0.002 0.002 -0.003 0.003 -0.004 0.005 C 44.184 61.481 30.123 61.48 21.48 52.837 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: #fefefe; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
8
data/res/images/LightSearch.svg
Normal file
8
data/res/images/LightSearch.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
|
||||
<defs>
|
||||
</defs>
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||
<path d="M 87.803 77.194 L 68.212 57.602 c 9.5 -14.422 7.912 -34.054 -4.766 -46.732 c 0 0 -0.001 0 -0.001 0 c -14.495 -14.493 -38.08 -14.494 -52.574 0 c -14.494 14.495 -14.494 38.079 0 52.575 c 7.248 7.247 16.767 10.87 26.287 10.87 c 7.134 0 14.267 -2.035 20.445 -6.104 l 19.591 19.591 C 78.659 89.267 80.579 90 82.498 90 s 3.84 -0.733 5.305 -2.197 C 90.732 84.873 90.732 80.124 87.803 77.194 z M 21.48 52.837 c -8.645 -8.646 -8.645 -22.713 0 -31.358 c 4.323 -4.322 10 -6.483 15.679 -6.483 c 5.678 0 11.356 2.161 15.678 6.483 c 8.644 8.644 8.645 22.707 0.005 31.352 c -0.002 0.002 -0.004 0.003 -0.005 0.005 c -0.002 0.002 -0.003 0.003 -0.004 0.005 C 44.184 61.481 30.123 61.48 21.48 52.837 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: #202020; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -27,6 +27,11 @@ if(ENABLE_OPENVR_PLUGIN)
|
|||
add_subdirectory(openvr)
|
||||
endif()
|
||||
|
||||
option(ENABLE_TWITCH_PLUGIN "Enable the twitch plugin" ON)
|
||||
if(ENABLE_TWITCH_PLUGIN)
|
||||
add_subdirectory(twitch)
|
||||
endif()
|
||||
|
||||
option(ENABLE_VIDEO_PLUGIN "Enable the video plugin" ON)
|
||||
if(ENABLE_VIDEO_PLUGIN)
|
||||
add_subdirectory(video)
|
||||
|
|
|
|||
74
src/macro-external/twitch/CMakeLists.txt
Normal file
74
src/macro-external/twitch/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-twitch)
|
||||
|
||||
# --- Check requirements ---
|
||||
|
||||
if(OS_MACOS)
|
||||
message(
|
||||
WARNING "Twitch support disabled on MacOS due to OpenSSL linking issues!")
|
||||
return()
|
||||
endif()
|
||||
|
||||
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
|
||||
set(CPP_HTTPLIB_DIR "${ADVSS_SOURCE_DIR}/deps/cpp-httplib")
|
||||
if(NOT EXISTS "${CPP_HTTPLIB_DIR}/CMakeLists.txt")
|
||||
message(WARNING "cpp-httplib directory \"${CPP_HTTPLIB_DIR}\" not found!\n"
|
||||
"Twitch support will be disabled!")
|
||||
return()
|
||||
endif()
|
||||
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
find_package(OpenSSL)
|
||||
if(NOT OPENSSL_FOUND)
|
||||
message(WARNING "OpenSSL not found!\n" "Twitch support will be disabled!\n\n")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(${PROJECT_NAME} MODULE)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT=1)
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE category-selection.cpp
|
||||
category-selection.hpp
|
||||
macro-action-twitch.cpp
|
||||
macro-action-twitch.hpp
|
||||
token.cpp
|
||||
token.hpp
|
||||
twitch-helpers.cpp
|
||||
twitch-helpers.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
|
||||
"${OPENSSL_INCLUDE_DIR}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES})
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(OS_WINDOWS)
|
||||
# Couldn't really find a better way to install runtime dependencies for
|
||||
# Windows TODO: Clean this up at some point
|
||||
function(FIND_FILES_WITH_PATTERN result pattern dir)
|
||||
execute_process(
|
||||
COMMAND
|
||||
powershell -Command
|
||||
"Get-ChildItem -Path '${dir}' -Recurse -Include ${pattern} |"
|
||||
"Select-Object -First 1 |"
|
||||
"ForEach-Object { $_.FullName -replace '\\\\', '\\\\' }"
|
||||
OUTPUT_VARIABLE files
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set(${result}
|
||||
${files}
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(OPENSSL_DLL_SEARCH_DIR "${OPENSSL_INCLUDE_DIR}/..")
|
||||
find_files_with_pattern(CRYPTO_DLL_FILES "libcrypto*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
find_files_with_pattern(SSL_DLL_FILES "libssl*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
"${CRYPTO_DLL_FILES}" "${SSL_DLL_FILES}")
|
||||
endif()
|
||||
363
src/macro-external/twitch/category-selection.cpp
Normal file
363
src/macro-external/twitch/category-selection.cpp
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
#include "category-selection.hpp"
|
||||
#include "token.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
#include <utility.hpp>
|
||||
#include <name-dialog.hpp>
|
||||
#include <obs-module-helper.hpp>
|
||||
#include <obs.hpp>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
void TwitchCategory::Load(obs_data_t *obj)
|
||||
{
|
||||
OBSDataAutoRelease data = obs_data_get_obj(obj, "category");
|
||||
id = obs_data_get_int(data, "id");
|
||||
name = obs_data_get_string(data, "name");
|
||||
}
|
||||
|
||||
void TwitchCategory::Save(obs_data_t *obj) const
|
||||
{
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_int(data, "id", id);
|
||||
obs_data_set_string(data, "name", name.c_str());
|
||||
obs_data_set_obj(obj, "category", data);
|
||||
}
|
||||
|
||||
bool TwitchCategorySelection::_fetchingCategoriesDone = false;
|
||||
std::map<QString, int> TwitchCategorySelection::_streamingCategories;
|
||||
|
||||
void TwitchCategorySelection::PopulateCategorySelection()
|
||||
{
|
||||
auto token = _token.lock();
|
||||
if (!token && _streamingCategories.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_fetchingCategoriesDone && token) {
|
||||
_categoryGrabber.Start(token);
|
||||
if (_progressDialog->exec() == QDialog::Accepted) {
|
||||
_fetchingCategoriesDone = true;
|
||||
}
|
||||
_categoryGrabber.Stop();
|
||||
_categoryGrabber.wait();
|
||||
}
|
||||
UpdateCategoryList();
|
||||
}
|
||||
|
||||
void TwitchCategorySelection::UpdateCategoryList()
|
||||
{
|
||||
_streamingCategories = _categoryGrabber.GetCategories();
|
||||
QString currentSelection = currentText();
|
||||
|
||||
const QSignalBlocker b(this);
|
||||
clear();
|
||||
for (const auto &[name, id] : _streamingCategories) {
|
||||
addItem(name, id);
|
||||
}
|
||||
|
||||
setCurrentText(currentSelection);
|
||||
}
|
||||
|
||||
TwitchCategorySelection::TwitchCategorySelection(QWidget *parent)
|
||||
: FilterComboBox(
|
||||
parent,
|
||||
obs_module_text("AdvSceneSwitcher.twitchCategories.select")),
|
||||
_progressDialog(new ProgressDialog(this))
|
||||
{
|
||||
_progressDialog->setWindowModality(Qt::WindowModal);
|
||||
setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
|
||||
QWidget::connect(this, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(SelectionChanged(int)));
|
||||
QWidget::connect(&_categoryGrabber, SIGNAL(CategoryCountUpdated(int)),
|
||||
_progressDialog, SLOT(CategoryCountUpdated(int)));
|
||||
QWidget::connect(&_categoryGrabber, SIGNAL(Finished()), this,
|
||||
SLOT(PopulateFinished()));
|
||||
QWidget::connect(TwitchCategorySignalManager::Instance(),
|
||||
SIGNAL(RepopulateRequired()), this,
|
||||
SLOT(UpdateCategoryList()));
|
||||
}
|
||||
|
||||
void TwitchCategorySelection::SetToken(const std::weak_ptr<TwitchToken> &token)
|
||||
{
|
||||
_token = token;
|
||||
const bool expired = token.expired();
|
||||
setDisabled(expired);
|
||||
if (expired) {
|
||||
setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.categorySelectionDisabled"));
|
||||
} else {
|
||||
setToolTip("");
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchCategorySelection::PopulateFinished()
|
||||
{
|
||||
_progressDialog->accept();
|
||||
}
|
||||
|
||||
void TwitchCategorySelection::showPopup()
|
||||
{
|
||||
if (!IsPopulated()) {
|
||||
PopulateCategorySelection();
|
||||
}
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
FilterComboBox::showPopup();
|
||||
}
|
||||
|
||||
bool TwitchCategorySelection::IsPopulated()
|
||||
{
|
||||
return count() == _categoryGrabber.GetCategories().size() &&
|
||||
_fetchingCategoriesDone;
|
||||
}
|
||||
|
||||
void TwitchCategorySelection::SetCategory(const TwitchCategory &id)
|
||||
{
|
||||
// If the list is populated already try to find id ...
|
||||
int index = findData(id.id);
|
||||
if (index != -1) {
|
||||
setCurrentIndex(index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id.id == -1) {
|
||||
setCurrentIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
// ... otherwise just add a dummy entry with the category name
|
||||
addItem(QString::fromStdString(id.name), id.id);
|
||||
setCurrentIndex(findData(id.id));
|
||||
}
|
||||
|
||||
void TwitchCategorySelection::SelectionChanged(int index)
|
||||
{
|
||||
TwitchCategory category{itemData(index).toInt(),
|
||||
currentText().toStdString()};
|
||||
emit CategoreyChanged(category);
|
||||
}
|
||||
|
||||
std::map<QString, int> CategoryGrabber::_categoryMap = {};
|
||||
std::mutex CategoryGrabber::_mtx = {};
|
||||
|
||||
void CategoryGrabber::Start(const std::shared_ptr<TwitchToken> &token,
|
||||
const std::string search)
|
||||
{
|
||||
_searchString = search;
|
||||
_token = token;
|
||||
_stop = false;
|
||||
start();
|
||||
}
|
||||
|
||||
void CategoryGrabber::Stop()
|
||||
{
|
||||
_stop = true;
|
||||
}
|
||||
|
||||
const std::map<QString, int> &CategoryGrabber::GetCategories()
|
||||
{
|
||||
return _categoryMap;
|
||||
}
|
||||
|
||||
void CategoryGrabber::run()
|
||||
{
|
||||
if (!_token) {
|
||||
return;
|
||||
emit Failed();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mtx);
|
||||
if (_searchString.empty()) {
|
||||
GetAll();
|
||||
} else {
|
||||
Search(_searchString);
|
||||
}
|
||||
}
|
||||
|
||||
emit Finished();
|
||||
}
|
||||
|
||||
void CategoryGrabber::Search(const std::string &)
|
||||
{
|
||||
static const std::string uri = "https://api.twitch.tv";
|
||||
const std::string path = "/helix/search/categories";
|
||||
|
||||
int startCount = _categoryMap.size();
|
||||
std::string cursor;
|
||||
httplib::Params params = {
|
||||
{"first", "100"}, {"after", cursor}, {"query", _searchString}};
|
||||
auto response = SendGetRequest(uri, path, *_token, params);
|
||||
|
||||
while (response.status == 200 && !_stop) {
|
||||
cursor = ParseReply(response.data);
|
||||
if (cursor.empty()) {
|
||||
break; // End of category list
|
||||
}
|
||||
params = {{"first", "100"},
|
||||
{"after", cursor},
|
||||
{"query", _searchString}};
|
||||
response = SendGetRequest(uri, path, *_token, params);
|
||||
emit CategoryCountUpdated(_categoryMap.size() - startCount);
|
||||
}
|
||||
}
|
||||
|
||||
void CategoryGrabber::GetAll()
|
||||
{
|
||||
static const std::string uri = "https://api.twitch.tv";
|
||||
const std::string path = "/helix/games/top";
|
||||
|
||||
// Declare static to "save" progress in case of cancel
|
||||
static std::string cursor;
|
||||
|
||||
httplib::Params params = {{"first", "100"}, {"after", cursor}};
|
||||
auto response = SendGetRequest(uri, path, *_token, params);
|
||||
|
||||
while (response.status == 200 && !_stop) {
|
||||
cursor = ParseReply(response.data);
|
||||
if (cursor.empty()) {
|
||||
break; // End of category list
|
||||
}
|
||||
params = {{"first", "100"}, {"after", cursor}};
|
||||
response = SendGetRequest(uri, path, *_token, params);
|
||||
emit CategoryCountUpdated(_categoryMap.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::string CategoryGrabber::ParseReply(obs_data_t *data) const
|
||||
{
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(data, "data");
|
||||
size_t count = obs_data_array_count(array);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
|
||||
int id = std::stoi(obs_data_get_string(arrayObj, "id"));
|
||||
QString name = obs_data_get_string(arrayObj, "name");
|
||||
_categoryMap.emplace(name, id);
|
||||
}
|
||||
OBSDataAutoRelease pagination = obs_data_get_obj(data, "pagination");
|
||||
return obs_data_get_string(pagination, "cursor");
|
||||
}
|
||||
|
||||
ProgressDialog::ProgressDialog(QWidget *parent, bool showSkip)
|
||||
: QDialog(parent),
|
||||
_skipFetchCheckBox(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchCategories.fetchSkip"))),
|
||||
_status(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchCategories.fetchStart")))
|
||||
{
|
||||
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
|
||||
_skipFetchCheckBox->setVisible(showSkip);
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_status);
|
||||
auto cancelButton = new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.twitchCategories.fetchStop"),
|
||||
this);
|
||||
layout->addWidget(_skipFetchCheckBox);
|
||||
layout->addWidget(cancelButton);
|
||||
setLayout(layout);
|
||||
|
||||
QWidget::connect(_skipFetchCheckBox, &QCheckBox::stateChanged, this,
|
||||
[this](int value) { _skipFetch = value; });
|
||||
QWidget::connect(cancelButton, &QPushButton::clicked, this,
|
||||
[this]() { _skipFetch ? accept() : reject(); });
|
||||
|
||||
if (_skipFetch) {
|
||||
accept();
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressDialog::CategoryCountUpdated(int value)
|
||||
{
|
||||
_status->setText(
|
||||
QString(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchCategories.fetchStatus"))
|
||||
.arg(value));
|
||||
}
|
||||
|
||||
TwitchCategorySearchButton::TwitchCategorySearchButton()
|
||||
{
|
||||
setMaximumWidth(22);
|
||||
const std::string pathPrefix =
|
||||
GetDataFilePath("res/images/" + GetThemeTypeName());
|
||||
SetButtonIcon(this, (pathPrefix + "Search.svg").c_str());
|
||||
setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchCategories.manualSearch"));
|
||||
QWidget::connect(this, SIGNAL(clicked()), this,
|
||||
SLOT(StartManualCategorySearch()));
|
||||
QWidget::connect(this, SIGNAL(RequestRepopulate()),
|
||||
TwitchCategorySignalManager::Instance(),
|
||||
SIGNAL(RepopulateRequired()));
|
||||
}
|
||||
|
||||
void TwitchCategorySearchButton::SetToken(
|
||||
const std::weak_ptr<TwitchToken> &token)
|
||||
{
|
||||
_token = token;
|
||||
const bool expired = token.expired();
|
||||
setDisabled(expired);
|
||||
if (expired) {
|
||||
setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.categorySelectionDisabled"));
|
||||
} else {
|
||||
setToolTip("AdvSceneSwitcher.twitchCategories.manualSearch");
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchCategorySearchButton::StartManualCategorySearch()
|
||||
{
|
||||
std::string category;
|
||||
bool accepted = AdvSSNameDialog::AskForName(
|
||||
this,
|
||||
obs_module_text("AdvSceneSwitcher.twitchCategories.search"),
|
||||
obs_module_text("AdvSceneSwitcher.twitchCategories.name"),
|
||||
category);
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
CategoryGrabber categoryGrabber;
|
||||
auto *progressDialog = new ProgressDialog(this, false);
|
||||
|
||||
QWidget::connect(&categoryGrabber, SIGNAL(CategoryCountUpdated(int)),
|
||||
progressDialog, SLOT(CategoryCountUpdated(int)));
|
||||
QWidget::connect(&categoryGrabber, &CategoryGrabber::Finished, this,
|
||||
[progressDialog]() { progressDialog->accept(); });
|
||||
QWidget::connect(&categoryGrabber, &CategoryGrabber::Failed, this,
|
||||
[progressDialog]() { progressDialog->reject(); });
|
||||
|
||||
auto previousCategoryCount = categoryGrabber.GetCategories().size();
|
||||
|
||||
categoryGrabber.Start(_token.lock(), category);
|
||||
progressDialog->exec();
|
||||
categoryGrabber.Stop();
|
||||
categoryGrabber.wait();
|
||||
|
||||
emit RequestRepopulate();
|
||||
progressDialog->deleteLater();
|
||||
|
||||
auto newCategoryCount =
|
||||
categoryGrabber.GetCategories().size() - previousCategoryCount;
|
||||
if (newCategoryCount == 0) {
|
||||
DisplayMessage(
|
||||
QString(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchCategories.searchFailed"))
|
||||
.arg(QString::fromStdString(category)));
|
||||
} else {
|
||||
DisplayMessage(
|
||||
QString(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchCategories.searchSuccess"))
|
||||
.arg(QString::number(newCategoryCount),
|
||||
QString::fromStdString(category)));
|
||||
}
|
||||
}
|
||||
|
||||
TwitchCategorySignalManager *TwitchCategorySignalManager::Instance()
|
||||
{
|
||||
static TwitchCategorySignalManager manager;
|
||||
return &manager;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
129
src/macro-external/twitch/category-selection.hpp
Normal file
129
src/macro-external/twitch/category-selection.hpp
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
#pragma once
|
||||
#include <filter-combo-box.hpp>
|
||||
#include <string>
|
||||
#include <obs-data.h>
|
||||
#include <QThread>
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class TwitchToken;
|
||||
|
||||
struct TwitchCategory {
|
||||
void Load(obs_data_t *obj);
|
||||
void Save(obs_data_t *obj) const;
|
||||
|
||||
int id = -1;
|
||||
std::string name = "-";
|
||||
};
|
||||
|
||||
class CategoryGrabber : public QThread {
|
||||
Q_OBJECT
|
||||
public:
|
||||
void Start(const std::shared_ptr<TwitchToken> &token,
|
||||
const std::string searchString = "");
|
||||
void Stop();
|
||||
const std::map<QString, int> &GetCategories();
|
||||
|
||||
private:
|
||||
signals:
|
||||
void CategoryCountUpdated(int value);
|
||||
void Finished();
|
||||
void Failed();
|
||||
|
||||
private:
|
||||
void run() override;
|
||||
|
||||
void Search(const std::string &);
|
||||
void GetAll();
|
||||
std::string ParseReply(obs_data_t *) const;
|
||||
|
||||
std::shared_ptr<TwitchToken> _token;
|
||||
static std::map<QString, int> _categoryMap;
|
||||
std::string _searchString = "";
|
||||
bool _stop = false;
|
||||
|
||||
// Don't allow parallel search requests to not spam Twitch API
|
||||
static std::mutex _mtx;
|
||||
};
|
||||
|
||||
class ProgressDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ProgressDialog(QWidget *parent, bool showSkip = true);
|
||||
|
||||
private slots:
|
||||
void CategoryCountUpdated(int);
|
||||
|
||||
private:
|
||||
QCheckBox *_skipFetchCheckBox;
|
||||
QLabel *_status;
|
||||
bool _skipFetch = false;
|
||||
};
|
||||
|
||||
class TwitchCategorySelection : public FilterComboBox {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TwitchCategorySelection(QWidget *parent);
|
||||
|
||||
// Will *not* verify if ID is still valid or populate the selection
|
||||
// list as that would take too long
|
||||
void SetCategory(const TwitchCategory &);
|
||||
|
||||
// Used for populating the category list
|
||||
void SetToken(const std::weak_ptr<TwitchToken> &);
|
||||
|
||||
private slots:
|
||||
void SelectionChanged(int);
|
||||
void PopulateFinished();
|
||||
void UpdateCategoryList();
|
||||
|
||||
signals:
|
||||
void CategoreyChanged(const TwitchCategory &);
|
||||
|
||||
protected:
|
||||
void showPopup() override;
|
||||
|
||||
private:
|
||||
void PopulateCategorySelection();
|
||||
bool IsPopulated();
|
||||
|
||||
ProgressDialog *_progressDialog;
|
||||
CategoryGrabber _categoryGrabber;
|
||||
std::weak_ptr<TwitchToken> _token;
|
||||
static bool _fetchingCategoriesDone;
|
||||
static std::map<QString, int> _streamingCategories;
|
||||
};
|
||||
|
||||
class TwitchCategorySearchButton : public QPushButton {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TwitchCategorySearchButton();
|
||||
void SetToken(const std::weak_ptr<TwitchToken> &);
|
||||
|
||||
private slots:
|
||||
void StartManualCategorySearch();
|
||||
|
||||
signals:
|
||||
void RequestRepopulate();
|
||||
|
||||
private:
|
||||
std::weak_ptr<TwitchToken> _token;
|
||||
};
|
||||
|
||||
// Helper class to ease singal / slot handling
|
||||
class TwitchCategorySignalManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static TwitchCategorySignalManager *Instance();
|
||||
|
||||
signals:
|
||||
void RepopulateRequired();
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
242
src/macro-external/twitch/macro-action-twitch.cpp
Normal file
242
src/macro-external/twitch/macro-action-twitch.cpp
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
#include "macro-action-twitch.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
#include <log-helper.hpp>
|
||||
#include <utility.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
const std::string MacroActionTwitch::id = "twitch";
|
||||
|
||||
bool MacroActionTwitch::_registered = MacroActionFactory::Register(
|
||||
MacroActionTwitch::id,
|
||||
{MacroActionTwitch::Create, MacroActionTwitchEdit::Create,
|
||||
"AdvSceneSwitcher.action.twitch"});
|
||||
|
||||
const static std::map<MacroActionTwitch::Action, std::string> actionTypes = {
|
||||
{MacroActionTwitch::Action::TITLE,
|
||||
"AdvSceneSwitcher.action.twitch.type.title"},
|
||||
{MacroActionTwitch::Action::CATEGORY,
|
||||
"AdvSceneSwitcher.action.twitch.type.category"},
|
||||
};
|
||||
|
||||
void MacroActionTwitch::SetStreamTitle(const std::shared_ptr<TwitchToken> &token)
|
||||
{
|
||||
if (std::string(_text).empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_string(data, "title", _text.c_str());
|
||||
auto result = SendPatchRequest(
|
||||
"https://api.twitch.tv",
|
||||
std::string("/helix/channels?broadcaster_id=") +
|
||||
token->GetUserID(),
|
||||
*token, data.Get());
|
||||
if (result.status != 204) {
|
||||
blog(LOG_INFO, "Failed to set stream title");
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionTwitch::SetStreamCategory(
|
||||
const std::shared_ptr<TwitchToken> &token)
|
||||
{
|
||||
if (_category.id == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_string(data, "game_id",
|
||||
std::to_string(_category.id).c_str());
|
||||
auto result = SendPatchRequest(
|
||||
"https://api.twitch.tv",
|
||||
std::string("/helix/channels?broadcaster_id=") +
|
||||
token->GetUserID(),
|
||||
*token, data.Get());
|
||||
if (result.status != 204) {
|
||||
blog(LOG_INFO, "Failed to set stream category");
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionTwitch::PerformAction()
|
||||
{
|
||||
auto token = _token.lock();
|
||||
if (!token) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (_action) {
|
||||
case MacroActionTwitch::Action::TITLE:
|
||||
SetStreamTitle(token);
|
||||
break;
|
||||
case MacroActionTwitch::Action::CATEGORY:
|
||||
SetStreamCategory(token);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionTwitch::LogAction() const
|
||||
{
|
||||
auto it = actionTypes.find(_action);
|
||||
if (it != actionTypes.end()) {
|
||||
vblog(LOG_INFO, "performed action \"%s\" with token for \"%s\"",
|
||||
it->second.c_str(),
|
||||
GetWeakTwitchTokenName(_token).c_str());
|
||||
} else {
|
||||
blog(LOG_WARNING, "ignored unknown twitch action %d",
|
||||
static_cast<int>(_action));
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionTwitch::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
obs_data_set_int(obj, "action", static_cast<int>(_action));
|
||||
obs_data_set_string(obj, "token",
|
||||
GetWeakTwitchTokenName(_token).c_str());
|
||||
_text.Save(obj, "text");
|
||||
_category.Save(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionTwitch::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
|
||||
_token = GetWeakTwitchTokenByName(obs_data_get_string(obj, "token"));
|
||||
_text.Load(obj, "text");
|
||||
_category.Load(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MacroActionTwitch::GetShortDesc() const
|
||||
{
|
||||
return GetWeakTwitchTokenName(_token);
|
||||
}
|
||||
|
||||
static inline void populateActionSelection(QComboBox *list)
|
||||
{
|
||||
for (const auto &[_, name] : actionTypes) {
|
||||
list->addItem(obs_module_text(name.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
MacroActionTwitchEdit::MacroActionTwitchEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionTwitch> entryData)
|
||||
: QWidget(parent),
|
||||
_actions(new QComboBox()),
|
||||
_tokens(new TwitchConnectionSelection()),
|
||||
_text(new VariableLineEdit(this)),
|
||||
_category(new TwitchCategorySelection(this)),
|
||||
_manualCategorySearch(new TwitchCategorySearchButton()),
|
||||
_layout(new QHBoxLayout())
|
||||
{
|
||||
_text->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
QSizePolicy::Preferred);
|
||||
populateActionSelection(_actions);
|
||||
|
||||
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(ActionChanged(int)));
|
||||
QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)),
|
||||
this, SLOT(TwitchTokenChanged(const QString &)));
|
||||
QWidget::connect(_text, SIGNAL(editingFinished()), this,
|
||||
SLOT(TextChanged()));
|
||||
QWidget::connect(_category,
|
||||
SIGNAL(CategoreyChanged(const TwitchCategory &)), this,
|
||||
SLOT(CategoreyChanged(const TwitchCategory &)));
|
||||
|
||||
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.twitch.entry"),
|
||||
_layout,
|
||||
{{"{{account}}", _tokens},
|
||||
{"{{actions}}", _actions},
|
||||
{"{{text}}", _text},
|
||||
{"{{category}}", _category},
|
||||
{"{{manualCategorySearch}}", _manualCategorySearch}});
|
||||
setLayout(_layout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
_entryData->_token = GetWeakTwitchTokenByQString(token);
|
||||
_category->SetToken(_entryData->_token);
|
||||
_manualCategorySearch->SetToken(_entryData->_token);
|
||||
SetupWidgetVisibility();
|
||||
emit(HeaderInfoChanged(token));
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::TextChanged()
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
_entryData->_text = _text->text().toStdString();
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::CategoreyChanged(const TwitchCategory &category)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
_entryData->_category = category;
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::SetupWidgetVisibility()
|
||||
{
|
||||
_text->setVisible(_entryData->_action ==
|
||||
MacroActionTwitch::Action::TITLE);
|
||||
_category->setVisible(_entryData->_action ==
|
||||
MacroActionTwitch::Action::CATEGORY);
|
||||
if (_entryData->_action == MacroActionTwitch::Action::TITLE) {
|
||||
RemoveStretchIfPresent(_layout);
|
||||
} else {
|
||||
AddStretchIfNecessary(_layout);
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
||||
_tokens->SetToken(_entryData->_token);
|
||||
_text->setText(_entryData->_text);
|
||||
_category->SetToken(_entryData->_token);
|
||||
_manualCategorySearch->SetToken(_entryData->_token);
|
||||
_category->SetCategory(_entryData->_category);
|
||||
SetupWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::ActionChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
_entryData->_action = static_cast<MacroActionTwitch::Action>(value);
|
||||
SetupWidgetVisibility();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
82
src/macro-external/twitch/macro-action-twitch.hpp
Normal file
82
src/macro-external/twitch/macro-action-twitch.hpp
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "token.hpp"
|
||||
#include "category-selection.hpp"
|
||||
|
||||
#include <variable-line-edit.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroActionTwitch : public MacroAction {
|
||||
public:
|
||||
MacroActionTwitch(Macro *m) : MacroAction(m) {}
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string GetShortDesc() const;
|
||||
std::string GetId() const { return id; };
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionTwitch>(m);
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
TITLE,
|
||||
CATEGORY,
|
||||
};
|
||||
|
||||
Action _action = Action::TITLE;
|
||||
std::weak_ptr<TwitchToken> _token;
|
||||
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
|
||||
TwitchCategory _category;
|
||||
|
||||
private:
|
||||
void SetStreamTitle(const std::shared_ptr<TwitchToken> &);
|
||||
void SetStreamCategory(const std::shared_ptr<TwitchToken> &);
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroActionTwitchEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroActionTwitchEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionTwitch> entryData = nullptr);
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
return new MacroActionTwitchEdit(
|
||||
parent,
|
||||
std::dynamic_pointer_cast<MacroActionTwitch>(action));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void ActionChanged(int);
|
||||
void TwitchTokenChanged(const QString &);
|
||||
void TextChanged();
|
||||
void CategoreyChanged(const TwitchCategory &);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<MacroActionTwitch> _entryData;
|
||||
|
||||
private:
|
||||
void SetupWidgetVisibility();
|
||||
|
||||
QComboBox *_actions;
|
||||
TwitchConnectionSelection *_tokens;
|
||||
VariableLineEdit *_text;
|
||||
TwitchCategorySelection *_category;
|
||||
TwitchCategorySearchButton *_manualCategorySearch;
|
||||
QHBoxLayout *_layout;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
623
src/macro-external/twitch/token.cpp
Normal file
623
src/macro-external/twitch/token.cpp
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
#include "token.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
#include <switcher-data.hpp>
|
||||
#include <utility.hpp>
|
||||
#include <QScrollArea>
|
||||
#include <QDesktopServices>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::deque<std::shared_ptr<Item>> twitchTokens;
|
||||
|
||||
const std::unordered_map<std::string, std::string> TokenOption::apiIdToLocale{
|
||||
// Add necessary token permissions here
|
||||
/*
|
||||
{"analytics:read:extensions",
|
||||
"AdvSceneSwitcher.twitchToken.analytics.readExtensions"},
|
||||
{"analytics:read:games",
|
||||
"AdvSceneSwitcher.twitchToken.analytics.readGames"},
|
||||
{"bits:read", "AdvSceneSwitcher.twitchToken.bits.read"},
|
||||
*/
|
||||
|
||||
{"channel:manage:broadcast",
|
||||
"AdvSceneSwitcher.twitchToken.channel.manageBroadcast"},
|
||||
};
|
||||
|
||||
static void saveConnections(obs_data_t *obj);
|
||||
static void loadConnections(obs_data_t *obj);
|
||||
|
||||
bool setupTwitchTokenSupport()
|
||||
{
|
||||
GetSwitcher()->AddSaveStep(saveConnections);
|
||||
GetSwitcher()->AddLoadStep(loadConnections);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TwitchToken::_setup = setupTwitchTokenSupport();
|
||||
|
||||
static void saveConnections(obs_data_t *obj)
|
||||
{
|
||||
OBSDataArrayAutoRelease connectionArray = obs_data_array_create();
|
||||
for (const auto &c : twitchTokens) {
|
||||
OBSDataAutoRelease arrayObj = obs_data_create();
|
||||
c->Save(arrayObj);
|
||||
obs_data_array_push_back(connectionArray, arrayObj);
|
||||
}
|
||||
obs_data_set_array(obj, "twitchConnections", connectionArray);
|
||||
}
|
||||
|
||||
static void loadConnections(obs_data_t *obj)
|
||||
{
|
||||
twitchTokens.clear();
|
||||
|
||||
OBSDataArrayAutoRelease connectionArray =
|
||||
obs_data_get_array(obj, "twitchConnections");
|
||||
size_t count = obs_data_array_count(connectionArray);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
OBSDataAutoRelease arrayObj =
|
||||
obs_data_array_item(connectionArray, i);
|
||||
auto con = TwitchToken::Create();
|
||||
twitchTokens.emplace_back(con);
|
||||
twitchTokens.back()->Load(arrayObj);
|
||||
}
|
||||
}
|
||||
|
||||
void TokenOption::Load(obs_data_t *obj)
|
||||
{
|
||||
apiId = obs_data_get_string(obj, "apiID");
|
||||
}
|
||||
|
||||
void TokenOption::Save(obs_data_t *obj) const
|
||||
{
|
||||
obs_data_set_string(obj, "apiID", apiId.c_str());
|
||||
}
|
||||
|
||||
std::string TokenOption::GetLocale() const
|
||||
{
|
||||
return apiIdToLocale.at(apiId);
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, std::string> &TokenOption::GetTokenMap()
|
||||
{
|
||||
return apiIdToLocale;
|
||||
}
|
||||
|
||||
bool TokenOption::operator<(const TokenOption &other) const
|
||||
{
|
||||
return apiId < other.apiId;
|
||||
}
|
||||
|
||||
void TwitchToken::Load(obs_data_t *obj)
|
||||
{
|
||||
Item::Load(obj);
|
||||
_token = obs_data_get_string(obj, "token");
|
||||
_userID = obs_data_get_string(obj, "userID");
|
||||
_tokenOptions.clear();
|
||||
OBSDataArrayAutoRelease options = obs_data_get_array(obj, "options");
|
||||
size_t count = obs_data_array_count(options);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
OBSDataAutoRelease arrayObj = obs_data_array_item(options, i);
|
||||
TokenOption tokenOption;
|
||||
tokenOption.Load(arrayObj);
|
||||
_tokenOptions.insert(tokenOption);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchToken::Save(obs_data_t *obj) const
|
||||
{
|
||||
Item::Save(obj);
|
||||
obs_data_set_string(obj, "token", _token.c_str());
|
||||
obs_data_set_string(obj, "userID", _userID.c_str());
|
||||
OBSDataArrayAutoRelease options = obs_data_array_create();
|
||||
for (auto &option : _tokenOptions) {
|
||||
OBSDataAutoRelease arrayObj = obs_data_create();
|
||||
option.Save(arrayObj);
|
||||
obs_data_array_push_back(options, arrayObj);
|
||||
}
|
||||
obs_data_set_array(obj, "options", options);
|
||||
}
|
||||
|
||||
bool TwitchToken::OptionIsEnabled(const TokenOption &option) const
|
||||
{
|
||||
for (const auto &activeOption : _tokenOptions) {
|
||||
if (activeOption.apiId == option.apiId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TwitchToken::SetToken(const std::string &value)
|
||||
{
|
||||
_token = value;
|
||||
auto res =
|
||||
SendGetRequest("https://api.twitch.tv", "/helix/users", *this);
|
||||
if (res.status != 200) {
|
||||
blog(LOG_WARNING, "failed to get Twitch user id from token!");
|
||||
_userID = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(res.data, "data");
|
||||
size_t count = obs_data_array_count(array);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
|
||||
_userID = obs_data_get_string(arrayObj, "id");
|
||||
_name = obs_data_get_string(arrayObj, "display_name");
|
||||
}
|
||||
}
|
||||
|
||||
TwitchToken *GetTwitchTokenByName(const QString &name)
|
||||
{
|
||||
return GetTwitchTokenByName(name.toStdString());
|
||||
}
|
||||
|
||||
TwitchToken *GetTwitchTokenByName(const std::string &name)
|
||||
{
|
||||
for (auto &t : twitchTokens) {
|
||||
if (t->Name() == name) {
|
||||
return dynamic_cast<TwitchToken *>(t.get());
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByName(const std::string &name)
|
||||
{
|
||||
for (const auto &t : twitchTokens) {
|
||||
if (t->Name() == name) {
|
||||
std::weak_ptr<TwitchToken> wp =
|
||||
std::dynamic_pointer_cast<TwitchToken>(t);
|
||||
return wp;
|
||||
}
|
||||
}
|
||||
return std::weak_ptr<TwitchToken>();
|
||||
}
|
||||
|
||||
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByQString(const QString &name)
|
||||
{
|
||||
return GetWeakTwitchTokenByName(name.toStdString());
|
||||
}
|
||||
|
||||
std::string GetWeakTwitchTokenName(std::weak_ptr<TwitchToken> token)
|
||||
{
|
||||
auto con = token.lock();
|
||||
if (!con) {
|
||||
return obs_module_text("AdvSceneSwitcher.twitchToken.invalid");
|
||||
}
|
||||
return con->Name();
|
||||
}
|
||||
|
||||
static bool ConnectionNameAvailable(const QString &name)
|
||||
{
|
||||
return !GetTwitchTokenByName(name);
|
||||
}
|
||||
|
||||
static bool ConnectionNameAvailable(const std::string &name)
|
||||
{
|
||||
return ConnectionNameAvailable(QString::fromStdString(name));
|
||||
}
|
||||
|
||||
static bool AskForSettingsWrapper(QWidget *parent, Item &settings)
|
||||
{
|
||||
TwitchToken &ConnectionSettings = dynamic_cast<TwitchToken &>(settings);
|
||||
return TwitchTokenSettingsDialog::AskForSettings(parent,
|
||||
ConnectionSettings);
|
||||
}
|
||||
|
||||
TwitchConnectionSelection::TwitchConnectionSelection(QWidget *parent)
|
||||
: ItemSelection(twitchTokens, TwitchToken::Create,
|
||||
AskForSettingsWrapper,
|
||||
"AdvSceneSwitcher.twitchToken.select",
|
||||
"AdvSceneSwitcher.twitchToken.add",
|
||||
"AdvSceneSwitcher.twitchToken.nameNotAvailable",
|
||||
"AdvSceneSwitcher.twitchToken.configure", parent)
|
||||
{
|
||||
ShowRenameContextMenu(false);
|
||||
|
||||
// Connect to slots
|
||||
QWidget::connect(TwitchConnectionSignalManager::Instance(),
|
||||
SIGNAL(Add(const QString &)), this,
|
||||
SLOT(AddItem(const QString &)));
|
||||
QWidget::connect(TwitchConnectionSignalManager::Instance(),
|
||||
SIGNAL(Remove(const QString &)), this,
|
||||
SLOT(RemoveItem(const QString &)));
|
||||
|
||||
// Forward signals
|
||||
QWidget::connect(this, SIGNAL(ItemAdded(const QString &)),
|
||||
TwitchConnectionSignalManager::Instance(),
|
||||
SIGNAL(Add(const QString &)));
|
||||
QWidget::connect(this, SIGNAL(ItemRemoved(const QString &)),
|
||||
TwitchConnectionSignalManager::Instance(),
|
||||
SIGNAL(Remove(const QString &)));
|
||||
}
|
||||
|
||||
void TwitchConnectionSelection::SetToken(const std::string &token)
|
||||
{
|
||||
const QSignalBlocker blocker(_selection);
|
||||
if (!!GetTwitchTokenByName(token)) {
|
||||
_selection->setCurrentText(QString::fromStdString(token));
|
||||
} else {
|
||||
_selection->setCurrentIndex(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchConnectionSelection::SetToken(
|
||||
const std::weak_ptr<TwitchToken> &token_)
|
||||
{
|
||||
const QSignalBlocker blocker(_selection);
|
||||
auto token = token_.lock();
|
||||
if (token) {
|
||||
SetToken(token->Name());
|
||||
} else {
|
||||
_selection->setCurrentIndex(-1);
|
||||
}
|
||||
}
|
||||
|
||||
static QCheckBox *addOption(const TokenOption &option, const TwitchToken &token,
|
||||
QGridLayout *layout, int &row)
|
||||
{
|
||||
auto label = new QLabel(obs_module_text(option.GetLocale().c_str()));
|
||||
label->setWordWrap(true);
|
||||
layout->addWidget(label, row, 1);
|
||||
auto checkBox = new QCheckBox();
|
||||
checkBox->setChecked(token.OptionIsEnabled(option));
|
||||
layout->addWidget(checkBox, row, 0);
|
||||
row++;
|
||||
return checkBox;
|
||||
}
|
||||
|
||||
TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
|
||||
QWidget *parent, const TwitchToken &settings)
|
||||
: ItemSettingsDialog(settings, twitchTokens,
|
||||
"AdvSceneSwitcher.twitchToken.select",
|
||||
"AdvSceneSwitcher.twitchToken.add",
|
||||
"AdvSceneSwitcher.twitchToken.nameNotAvailable",
|
||||
parent),
|
||||
_requestToken(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.twitchToken.request"))),
|
||||
_showToken(new QPushButton()),
|
||||
_currentTokenValue(new QLineEdit()),
|
||||
_tokenStatus(new QLabel())
|
||||
{
|
||||
_showToken->setMaximumWidth(22);
|
||||
_showToken->setFlat(true);
|
||||
_showToken->setStyleSheet(
|
||||
"QPushButton { background-color: transparent; border: 0px }");
|
||||
|
||||
_currentTokenValue->setReadOnly(true);
|
||||
_currentTokenValue->setText(QString::fromStdString(settings._token));
|
||||
|
||||
_name->setReadOnly(true);
|
||||
|
||||
QWidget::connect(_requestToken, SIGNAL(clicked()), this,
|
||||
SLOT(RequestToken()));
|
||||
QWidget::connect(_showToken, SIGNAL(pressed()), this,
|
||||
SLOT(ShowToken()));
|
||||
QWidget::connect(_showToken, SIGNAL(released()), this,
|
||||
SLOT(HideToken()));
|
||||
connect(&_tokenGrabber, &TokenGrabberThread::GotToken, this,
|
||||
&TwitchTokenSettingsDialog::GotToken);
|
||||
|
||||
auto generalSettingsGrid = new QGridLayout();
|
||||
int row = 0;
|
||||
generalSettingsGrid->addWidget(
|
||||
new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.twitchToken.name")),
|
||||
row, 0);
|
||||
auto nameLayout = new QHBoxLayout;
|
||||
nameLayout->addWidget(_name);
|
||||
nameLayout->addWidget(_nameHint);
|
||||
generalSettingsGrid->addLayout(nameLayout, row, 1);
|
||||
++row;
|
||||
generalSettingsGrid->addWidget(
|
||||
new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.twitchToken.value")),
|
||||
row, 0);
|
||||
auto tokenValueLayout = new QHBoxLayout;
|
||||
tokenValueLayout->addWidget(_currentTokenValue);
|
||||
tokenValueLayout->addWidget(_showToken);
|
||||
generalSettingsGrid->addLayout(tokenValueLayout, row, 1);
|
||||
++row;
|
||||
generalSettingsGrid->addWidget(_requestToken, row, 0);
|
||||
generalSettingsGrid->addWidget(_tokenStatus, row, 1);
|
||||
|
||||
auto optionsGrid = new QGridLayout();
|
||||
row = 0;
|
||||
auto optionsBox = new QGroupBox(
|
||||
obs_module_text("AdvSceneSwitcher.twitchToken.permissions"));
|
||||
for (const auto &[id, _] : TokenOption::GetTokenMap()) {
|
||||
auto checkBox = addOption({id}, settings, optionsGrid, row);
|
||||
_optionWidgets[id] = checkBox;
|
||||
}
|
||||
MinimizeSizeOfColumn(optionsGrid, 0);
|
||||
optionsBox->setLayout(optionsGrid);
|
||||
|
||||
auto scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto contentWidget = new QWidget(scrollArea);
|
||||
auto layout = new QVBoxLayout(contentWidget);
|
||||
layout->addLayout(generalSettingsGrid);
|
||||
layout->addWidget(optionsBox);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
scrollArea->setWidget(contentWidget);
|
||||
|
||||
auto dialogLayout = new QVBoxLayout();
|
||||
dialogLayout->addWidget(scrollArea);
|
||||
dialogLayout->addWidget(_buttonbox);
|
||||
setLayout(dialogLayout);
|
||||
|
||||
_currentTokenValue->setText(QString::fromStdString(settings._token));
|
||||
if (settings._token.empty()) {
|
||||
_tokenStatus->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchToken.request.notSet"));
|
||||
}
|
||||
HideToken();
|
||||
|
||||
if (_name->text() == "") {
|
||||
PulseWidget(_requestToken, Qt::green, QColor(0, 0, 0, 0), true);
|
||||
}
|
||||
|
||||
_currentToken = settings;
|
||||
}
|
||||
|
||||
void TwitchTokenSettingsDialog::ShowToken()
|
||||
{
|
||||
SetButtonIcon(_showToken, ":res/images/visible.svg");
|
||||
_currentTokenValue->setEchoMode(QLineEdit::Normal);
|
||||
}
|
||||
|
||||
void TwitchTokenSettingsDialog::HideToken()
|
||||
{
|
||||
SetButtonIcon(_showToken, ":res/images/invisible.svg");
|
||||
_currentTokenValue->setEchoMode(QLineEdit::PasswordEchoOnEdit);
|
||||
}
|
||||
|
||||
static std::string generateStateString()
|
||||
{
|
||||
const char *chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
||||
const size_t stateStringLen = 32;
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<size_t> dis(0, sizeof(chars) - 2);
|
||||
|
||||
std::string state;
|
||||
state.reserve(stateStringLen);
|
||||
|
||||
for (size_t i = 0; i < stateStringLen; ++i) {
|
||||
state += chars[dis(gen)];
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
static std::string generateScopeString(const std::set<TokenOption> &options)
|
||||
{
|
||||
if (options.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string scope;
|
||||
for (const auto &option : options) {
|
||||
scope += option.apiId + "+";
|
||||
}
|
||||
scope.pop_back(); // Remove trailing +
|
||||
return scope;
|
||||
}
|
||||
|
||||
static std::string getHtml(const QString &redirect)
|
||||
{
|
||||
const char *html = R"(
|
||||
<html>
|
||||
<head>
|
||||
<title>Advanced scene switcher</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="output">Please click this link to continue if not automatically redirected</div>
|
||||
<p><a href="%1">Login with Twitch</a></a></p>
|
||||
<script type="text/javascript">
|
||||
if (document.location.hash && document.location.hash != '') {
|
||||
var parsedHash = new URLSearchParams(window.location.hash.slice(1));
|
||||
if (parsedHash.get('access_token')) {
|
||||
window.location.replace(`http://localhost:8080/token?access_token=${parsedHash.get('access_token')}&state=${parsedHash.get('state')}`);
|
||||
output.textContent = 'It is safe to close this window';
|
||||
}
|
||||
} else {
|
||||
window.location.replace('%1');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>)";
|
||||
|
||||
return QString(html).arg(redirect).toStdString();
|
||||
}
|
||||
|
||||
void TwitchTokenSettingsDialog::RequestToken()
|
||||
{
|
||||
// Don't allow parallel RequestToken() calls
|
||||
_requestToken->setDisabled(true);
|
||||
|
||||
auto scope = QString::fromStdString(
|
||||
generateScopeString(GetEnabledOptions()));
|
||||
_tokenGrabber.SetTokenScope(scope);
|
||||
_tokenGrabber.start();
|
||||
_tokenStatus->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchToken.request.waiting"));
|
||||
}
|
||||
|
||||
void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
|
||||
{
|
||||
_currentTokenValue->setText(value.value_or(""));
|
||||
if (value.has_value()) {
|
||||
_tokenStatus->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchToken.request.success"));
|
||||
_currentToken.SetToken(value.value().toStdString());
|
||||
auto name = QString::fromStdString(_currentToken._name);
|
||||
_name->setText(name);
|
||||
_name->textEdited(name);
|
||||
} else {
|
||||
_tokenStatus->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.twitchToken.request.fail"));
|
||||
_name->setText("");
|
||||
}
|
||||
_requestToken->setEnabled(true);
|
||||
}
|
||||
|
||||
std::set<TokenOption> TwitchTokenSettingsDialog::GetEnabledOptions()
|
||||
{
|
||||
std::set<TokenOption> result;
|
||||
for (const auto &[id, checkBox] : _optionWidgets) {
|
||||
if (checkBox->isChecked()) {
|
||||
TokenOption option;
|
||||
option.apiId = id;
|
||||
result.emplace(option);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool TwitchTokenSettingsDialog::AskForSettings(QWidget *parent,
|
||||
TwitchToken &settings)
|
||||
{
|
||||
TwitchTokenSettingsDialog dialog(parent, settings);
|
||||
dialog.setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
|
||||
if (dialog.exec() != DialogCode::Accepted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
settings = dialog._currentToken;
|
||||
settings._tokenOptions = dialog.GetEnabledOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
int TokenGrabberThread::_timeout = 15;
|
||||
|
||||
TokenGrabberThread::~TokenGrabberThread()
|
||||
{
|
||||
_stopWaiting = true;
|
||||
_cv.notify_all();
|
||||
Stop();
|
||||
_server.stop();
|
||||
}
|
||||
|
||||
static std::string getAuthErrorString(const char *errDetail)
|
||||
{
|
||||
QString err = obs_module_text(
|
||||
"AdvSceneSwitcher.twitchToken.request.fail.browser");
|
||||
return err.arg(obs_module_text(errDetail)).toStdString();
|
||||
}
|
||||
|
||||
void TokenGrabberThread::run()
|
||||
{
|
||||
// Reset
|
||||
_server.stop();
|
||||
_server.~Server();
|
||||
new (&_server) httplib::Server();
|
||||
if (_serverThread.joinable()) {
|
||||
_serverThread.join();
|
||||
}
|
||||
_stopWaiting = {false};
|
||||
|
||||
// Generate URI to request token
|
||||
auto state = generateStateString();
|
||||
auto getTokenURI = "https://id.twitch.tv/oauth2/authorize"
|
||||
"?response_type=token"
|
||||
"&client_id=" +
|
||||
QString(GetClientID()) +
|
||||
"&redirect_uri=http://localhost:8080/auth"
|
||||
"&scope=" +
|
||||
_scope + "&state=" + QString::fromStdString(state);
|
||||
|
||||
// Setup server receiving token string
|
||||
auto html = getHtml(getTokenURI);
|
||||
_server.Get("/auth", [html, state](const httplib::Request &req,
|
||||
httplib::Response &res) {
|
||||
// Check for errors
|
||||
if (req.has_param("error")) {
|
||||
auto recvState = req.get_param_value("state");
|
||||
if (recvState != state) {
|
||||
blog(LOG_WARNING,
|
||||
"state string does not match in error handling?! "
|
||||
"Got \"%s\" - expected \"%s\"\n"
|
||||
"ignoring error ...",
|
||||
recvState.c_str(), state.c_str());
|
||||
return;
|
||||
}
|
||||
auto errorStr =
|
||||
req.get_param_value("error_description");
|
||||
res.set_content(getAuthErrorString(errorStr.c_str()),
|
||||
"text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse fragments and redirect to /token with
|
||||
// corresponding parameters.
|
||||
res.set_content(html, "text/html");
|
||||
});
|
||||
_server.Get("/token", [&](const httplib::Request &req,
|
||||
httplib::Response &res) {
|
||||
// Check if valid request and grab the token string
|
||||
std::lock_guard<std::mutex> lk(_mutex);
|
||||
auto recvState = req.get_param_value("state");
|
||||
if (recvState != state) {
|
||||
blog(LOG_WARNING,
|
||||
"state string does not match! "
|
||||
"Got \"%s\" - expected \"%s\"",
|
||||
recvState.c_str(), state.c_str());
|
||||
res.set_content(
|
||||
getAuthErrorString(
|
||||
"AdvSceneSwitcher.twitchToken.request.fail.stateMismatch"),
|
||||
"text/plain");
|
||||
} else {
|
||||
_tokenString = QString::fromStdString(
|
||||
req.get_param_value("access_token"));
|
||||
res.set_content(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.twitchToken.request.success.browser"),
|
||||
"text/plain");
|
||||
}
|
||||
_stopWaiting = true;
|
||||
_cv.notify_all();
|
||||
});
|
||||
|
||||
// Request user to grant token
|
||||
QDesktopServices::openUrl(getTokenURI);
|
||||
|
||||
// Start the server and wait
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
_serverThread =
|
||||
std::thread([this]() { _server.listen("localhost", 8080); });
|
||||
auto time = std::chrono::high_resolution_clock::now() +
|
||||
std::chrono::seconds(_timeout);
|
||||
while (!_stopWaiting) {
|
||||
if (_cv.wait_until(lock, time) == std::cv_status::timeout) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit GotToken(_tokenString);
|
||||
}
|
||||
|
||||
void TokenGrabberThread::Stop()
|
||||
{
|
||||
if (_server.is_running()) {
|
||||
_server.stop();
|
||||
}
|
||||
if (_serverThread.joinable()) {
|
||||
_serverThread.join();
|
||||
}
|
||||
wait();
|
||||
}
|
||||
|
||||
TwitchConnectionSignalManager *TwitchConnectionSignalManager::Instance()
|
||||
{
|
||||
static TwitchConnectionSignalManager manager;
|
||||
return &manager;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
140
src/macro-external/twitch/token.hpp
Normal file
140
src/macro-external/twitch/token.hpp
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#pragma once
|
||||
#include <item-selection-helpers.hpp>
|
||||
#include <httplib.h>
|
||||
#include <set>
|
||||
#include <QCheckBox>
|
||||
#include <QThread>
|
||||
#include <optional>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class TwitchConnectionSelection;
|
||||
class TwitchTokenSettingsDialog;
|
||||
|
||||
class TokenOption {
|
||||
public:
|
||||
void Load(obs_data_t *obj);
|
||||
void Save(obs_data_t *obj) const;
|
||||
std::string GetLocale() const;
|
||||
|
||||
static const std::unordered_map<std::string, std::string> &
|
||||
GetTokenMap();
|
||||
bool operator<(const TokenOption &other) const;
|
||||
std::string apiId = "";
|
||||
|
||||
private:
|
||||
const static std::unordered_map<std::string, std::string> apiIdToLocale;
|
||||
};
|
||||
|
||||
class TwitchToken : public Item {
|
||||
public:
|
||||
static std::shared_ptr<Item> Create()
|
||||
{
|
||||
return std::make_shared<TwitchToken>();
|
||||
}
|
||||
|
||||
void Load(obs_data_t *obj);
|
||||
void Save(obs_data_t *obj) const;
|
||||
std::string GetName() { return _name; }
|
||||
bool OptionIsEnabled(const TokenOption &) const;
|
||||
void SetToken(const std::string &);
|
||||
bool IsEmpty() const { return _token.empty(); }
|
||||
std::string GetToken() const { return _token; }
|
||||
std::string GetUserID() const { return _userID; }
|
||||
|
||||
private:
|
||||
std::string _token;
|
||||
std::string _userID;
|
||||
std::set<TokenOption> _tokenOptions = {{"channel:manage:broadcast"}};
|
||||
|
||||
static bool _setup;
|
||||
|
||||
friend TwitchConnectionSelection;
|
||||
friend TwitchTokenSettingsDialog;
|
||||
};
|
||||
|
||||
class TokenGrabberThread : public QThread {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
~TokenGrabberThread();
|
||||
void SetTokenScope(const QString &value) { _scope = value; }
|
||||
|
||||
signals:
|
||||
void GotToken(const std::optional<QString> &);
|
||||
|
||||
protected:
|
||||
void run() override;
|
||||
|
||||
private:
|
||||
void Stop();
|
||||
|
||||
QString _scope;
|
||||
std::optional<QString> _tokenString;
|
||||
|
||||
static int _timeout;
|
||||
std::mutex _mutex;
|
||||
std::atomic_bool _stopWaiting = {false};
|
||||
std::condition_variable _cv;
|
||||
std::thread _serverThread;
|
||||
httplib::Server _server;
|
||||
};
|
||||
|
||||
class TwitchTokenSettingsDialog : public ItemSettingsDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TwitchTokenSettingsDialog(QWidget *parent, const TwitchToken &);
|
||||
static bool AskForSettings(QWidget *parent, TwitchToken &settings);
|
||||
|
||||
private slots:
|
||||
void ShowToken();
|
||||
void HideToken();
|
||||
void RequestToken();
|
||||
void GotToken(const std::optional<QString> &);
|
||||
|
||||
private:
|
||||
std::set<TokenOption> GetEnabledOptions();
|
||||
|
||||
QPushButton *_requestToken;
|
||||
QPushButton *_showToken;
|
||||
QLineEdit *_currentTokenValue;
|
||||
QLabel *_tokenStatus;
|
||||
TokenGrabberThread _tokenGrabber;
|
||||
TwitchToken _currentToken;
|
||||
std::unordered_map<std::string, QCheckBox *> _optionWidgets;
|
||||
};
|
||||
|
||||
class TwitchConnectionSelection : public ItemSelection {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TwitchConnectionSelection(QWidget *parent = 0);
|
||||
void SetToken(const std::string &);
|
||||
void SetToken(const std::weak_ptr<TwitchToken> &);
|
||||
};
|
||||
|
||||
// Helper class so that it is not required to add signals to the
|
||||
// AdvSceneSwitcher class for handling adding and removing Twitch connections
|
||||
class TwitchConnectionSignalManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static TwitchConnectionSignalManager *Instance();
|
||||
|
||||
private:
|
||||
signals:
|
||||
// Rename signal not required as name is based on Twitch account name
|
||||
// and item name cannot be manually changed
|
||||
void Add(const QString &);
|
||||
void Remove(const QString &);
|
||||
};
|
||||
|
||||
TwitchToken *GetTwitchTokenByName(const QString &);
|
||||
TwitchToken *GetTwitchTokenByName(const std::string &);
|
||||
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByName(const std::string &name);
|
||||
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByQString(const QString &name);
|
||||
std::string GetWeakTwitchTokenName(std::weak_ptr<TwitchToken>);
|
||||
|
||||
} // namespace advss
|
||||
|
||||
Q_DECLARE_METATYPE(advss::TwitchToken *);
|
||||
76
src/macro-external/twitch/twitch-helpers.cpp
Normal file
76
src/macro-external/twitch/twitch-helpers.cpp
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#include "twitch-helpers.hpp"
|
||||
#include "token.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
static constexpr std::string_view clientID = "ds5tt4ogliifsqc04mz3d3etnck3e5";
|
||||
|
||||
static httplib::Headers getTokenRequestHeaders(const TwitchToken &token)
|
||||
{
|
||||
return {
|
||||
{"Authorization", "Bearer " + token.GetToken()},
|
||||
{"Client-Id", clientID.data()},
|
||||
};
|
||||
}
|
||||
|
||||
RequestResult SendGetRequest(const std::string &uri, const std::string &path,
|
||||
const TwitchToken &token,
|
||||
const httplib::Params ¶ms)
|
||||
{
|
||||
httplib::Client cli(uri);
|
||||
auto headers = getTokenRequestHeaders(token);
|
||||
auto response = cli.Get(path, params, headers);
|
||||
RequestResult result;
|
||||
result.status = response->status;
|
||||
if (response->body.empty()) {
|
||||
return result;
|
||||
}
|
||||
OBSDataAutoRelease replyData =
|
||||
obs_data_create_from_json(response->body.c_str());
|
||||
result.data = replyData;
|
||||
return result;
|
||||
}
|
||||
|
||||
RequestResult SendPostRequest(const std::string &uri, const std::string &path,
|
||||
const TwitchToken &token,
|
||||
const httplib::Params ¶ms)
|
||||
{
|
||||
httplib::Client cli(uri);
|
||||
auto headers = getTokenRequestHeaders(token);
|
||||
auto response = cli.Post(path, headers, params);
|
||||
RequestResult result;
|
||||
result.status = response->status;
|
||||
if (response->body.empty()) {
|
||||
return result;
|
||||
}
|
||||
OBSDataAutoRelease replyData =
|
||||
obs_data_create_from_json(response->body.c_str());
|
||||
result.data = replyData;
|
||||
return result;
|
||||
}
|
||||
|
||||
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
|
||||
const TwitchToken &token, const OBSData &data)
|
||||
{
|
||||
httplib::Client cli(uri);
|
||||
auto headers = getTokenRequestHeaders(token);
|
||||
auto json = obs_data_get_json(data);
|
||||
std::string body = json ? json : "";
|
||||
auto response = cli.Patch(path, headers, body, "application/json");
|
||||
RequestResult result;
|
||||
result.status = response->status;
|
||||
if (response->body.empty()) {
|
||||
return result;
|
||||
}
|
||||
OBSDataAutoRelease replyData =
|
||||
obs_data_create_from_json(response->body.c_str());
|
||||
result.data = replyData;
|
||||
return result;
|
||||
}
|
||||
|
||||
const char *GetClientID()
|
||||
{
|
||||
return clientID.data();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
25
src/macro-external/twitch/twitch-helpers.hpp
Normal file
25
src/macro-external/twitch/twitch-helpers.hpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
#include <httplib.h>
|
||||
#include <obs.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class TwitchToken;
|
||||
|
||||
struct RequestResult {
|
||||
int status = 0;
|
||||
OBSData data = nullptr;
|
||||
};
|
||||
|
||||
RequestResult SendGetRequest(const std::string &uri, const std::string &path,
|
||||
const TwitchToken &token,
|
||||
const httplib::Params & = {});
|
||||
RequestResult SendPostRequest(const std::string &uri, const std::string &path,
|
||||
const TwitchToken &token,
|
||||
const httplib::Params ¶ms = {});
|
||||
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
|
||||
const TwitchToken &token, const OBSData &data);
|
||||
const char *GetClientID();
|
||||
|
||||
} // namespace advss
|
||||
Loading…
Reference in New Issue
Block a user