feat: add initial experimental Twitch Eventsub support (#5837)
Co-authored-by: nerix <nerixdev@outlook.de>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
set(SOURCE_FILES
|
||||
session.cpp
|
||||
|
||||
chrono.cpp
|
||||
|
||||
json.cpp
|
||||
string.cpp
|
||||
|
||||
payloads/subscription.cpp
|
||||
payloads/session-welcome.cpp
|
||||
|
||||
# Subscription types
|
||||
payloads/channel-ban-v1.cpp
|
||||
payloads/stream-online-v1.cpp
|
||||
payloads/stream-offline-v1.cpp
|
||||
payloads/channel-update-v1.cpp
|
||||
payloads/channel-chat-notification-v1.cpp
|
||||
payloads/channel-chat-message-v1.cpp
|
||||
payloads/channel-moderate-v2.cpp
|
||||
# Add your new subscription type source file above this line
|
||||
|
||||
messages/metadata.cpp
|
||||
)
|
||||
|
||||
add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES})
|
||||
|
||||
# Generate source groups for use in IDEs
|
||||
# source_group(TREE ${CMAKE_SOURCE_DIR} FILES ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
|
||||
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
Qt${MAJOR_QT_VERSION}::Core
|
||||
Boost::headers
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
)
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC
|
||||
$<$<BOOL:${MSVC}>:BOOST_JSON_NO_LIB>
|
||||
$<$<BOOL:${MSVC}>:BOOST_CONTAINER_NO_LIB>
|
||||
)
|
||||
|
||||
# Hack to get the include directories from Python
|
||||
get_target_property(_inc_dirs ${PROJECT_NAME} INCLUDE_DIRECTORIES)
|
||||
list(APPEND _inc_dirs ${Boost_INCLUDE_DIRS})
|
||||
list(APPEND _inc_dirs ${OPENSSL_INCLUDE_DIR})
|
||||
list(JOIN _inc_dirs ";" _inc_dir)
|
||||
add_custom_target(_ast_includes echo "@@INCLUDE_DIRS=${_inc_dir}")
|
||||
|
||||
if (MSVC)
|
||||
# Add bigobj
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /EHsc /bigobj)
|
||||
|
||||
# Change flags for RelWithDebInfo
|
||||
|
||||
# Default: "/debug /INCREMENTAL"
|
||||
# Changes:
|
||||
# - Disable incremental linking to reduce padding
|
||||
# - Enable all optimizations - by default when /DEBUG is specified,
|
||||
# these optimizations will be disabled. We need /DEBUG to generate a PDB.
|
||||
# See https://gitlab.kitware.com/cmake/cmake/-/issues/20812 for more details.
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /INCREMENTAL:NO /OPT:REF,ICF,LBR")
|
||||
|
||||
# Use the function inlining level from 'Release' mode (2).
|
||||
string(REPLACE "/Ob1" "/Ob2" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
|
||||
|
||||
# Configure warnings
|
||||
|
||||
# Someone adds /W3 before we add /W4.
|
||||
# This makes sure, only /W4 is specified.
|
||||
string(REPLACE "/W3" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC
|
||||
/W4
|
||||
# 5038 - warnings about initialization order
|
||||
/w15038
|
||||
# 4855 - implicit capture of 'this' via '[=]' is deprecated
|
||||
/w14855
|
||||
# Disable the following warnings (see reasoning above)
|
||||
/wd4505
|
||||
/wd4100
|
||||
/wd4267
|
||||
)
|
||||
# Disable min/max macros from Windows.h
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC NOMINMAX)
|
||||
else ()
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC
|
||||
-Wall
|
||||
# Disable the following warnings
|
||||
-Wno-unused-function
|
||||
-Wno-switch
|
||||
-Wno-deprecated-declarations
|
||||
-Wno-sign-compare
|
||||
-Wno-unused-variable
|
||||
|
||||
# Disabling strict-aliasing warnings for now, although we probably want to re-enable this in the future
|
||||
-Wno-strict-aliasing
|
||||
|
||||
-Werror=return-type
|
||||
-Werror=reorder
|
||||
)
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC
|
||||
-Wno-unused-local-typedef
|
||||
-Wno-unused-private-field
|
||||
-Werror=inconsistent-missing-override
|
||||
-Werror=final-dtor-non-final-class
|
||||
-Werror=ambiguous-reversed-operator
|
||||
|
||||
)
|
||||
else ()
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC
|
||||
-Wno-class-memaccess
|
||||
)
|
||||
endif()
|
||||
endif ()
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "twitch-eventsub-ws/chrono.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/date.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace chatterino::eventsub::lib {
|
||||
|
||||
boost::json::result_for<std::chrono::system_clock::time_point,
|
||||
boost::json::value>::type
|
||||
tag_invoke(
|
||||
boost::json::try_value_to_tag<std::chrono::system_clock::time_point>,
|
||||
const boost::json::value &jvRoot, const AsISO8601 &)
|
||||
{
|
||||
const auto raw = boost::json::try_value_to<std::string>(jvRoot);
|
||||
if (raw.has_error())
|
||||
{
|
||||
return raw.error();
|
||||
}
|
||||
|
||||
std::chrono::system_clock::time_point tp;
|
||||
std::istringstream in{*raw};
|
||||
in >> date::parse("%FT%TZ", tp);
|
||||
|
||||
return tp;
|
||||
}
|
||||
} // namespace chatterino::eventsub::lib
|
||||
@@ -0,0 +1 @@
|
||||
#include <boost/json/src.hpp>
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "twitch-eventsub-ws/messages/metadata.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::messages {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Metadata, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Metadata>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Metadata must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvmessageID = root.if_contains("message_id");
|
||||
if (jvmessageID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_messageID{"Missing required key message_id"};
|
||||
return boost::system::error_code{129, error_missing_field_messageID};
|
||||
}
|
||||
|
||||
auto messageID = boost::json::try_value_to<std::string>(*jvmessageID);
|
||||
|
||||
if (messageID.has_error())
|
||||
{
|
||||
return messageID.error();
|
||||
}
|
||||
|
||||
const auto *jvmessageType = root.if_contains("message_type");
|
||||
if (jvmessageType == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_messageType{
|
||||
"Missing required key message_type"};
|
||||
return boost::system::error_code{129, error_missing_field_messageType};
|
||||
}
|
||||
|
||||
auto messageType = boost::json::try_value_to<std::string>(*jvmessageType);
|
||||
|
||||
if (messageType.has_error())
|
||||
{
|
||||
return messageType.error();
|
||||
}
|
||||
|
||||
const auto *jvmessageTimestamp = root.if_contains("message_timestamp");
|
||||
if (jvmessageTimestamp == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_messageTimestamp{
|
||||
"Missing required key message_timestamp"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_messageTimestamp};
|
||||
}
|
||||
|
||||
auto messageTimestamp =
|
||||
boost::json::try_value_to<std::string>(*jvmessageTimestamp);
|
||||
|
||||
if (messageTimestamp.has_error())
|
||||
{
|
||||
return messageTimestamp.error();
|
||||
}
|
||||
|
||||
std::optional<std::string> subscriptionType = std::nullopt;
|
||||
const auto *jvsubscriptionType = root.if_contains("subscription_type");
|
||||
if (jvsubscriptionType != nullptr && !jvsubscriptionType->is_null())
|
||||
{
|
||||
auto tsubscriptionType =
|
||||
boost::json::try_value_to<std::string>(*jvsubscriptionType);
|
||||
|
||||
if (tsubscriptionType.has_error())
|
||||
{
|
||||
return tsubscriptionType.error();
|
||||
}
|
||||
subscriptionType = std::move(tsubscriptionType.value());
|
||||
}
|
||||
|
||||
std::optional<std::string> subscriptionVersion = std::nullopt;
|
||||
const auto *jvsubscriptionVersion =
|
||||
root.if_contains("subscription_version");
|
||||
if (jvsubscriptionVersion != nullptr && !jvsubscriptionVersion->is_null())
|
||||
{
|
||||
auto tsubscriptionVersion =
|
||||
boost::json::try_value_to<std::string>(*jvsubscriptionVersion);
|
||||
|
||||
if (tsubscriptionVersion.has_error())
|
||||
{
|
||||
return tsubscriptionVersion.error();
|
||||
}
|
||||
subscriptionVersion = std::move(tsubscriptionVersion.value());
|
||||
}
|
||||
|
||||
return Metadata{
|
||||
.messageID = std::move(messageID.value()),
|
||||
.messageType = std::move(messageType.value()),
|
||||
.messageTimestamp = std::move(messageTimestamp.value()),
|
||||
.subscriptionType = std::move(subscriptionType),
|
||||
.subscriptionVersion = std::move(subscriptionVersion),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::messages
|
||||
@@ -0,0 +1,127 @@
|
||||
# Adding a new subscription type
|
||||
|
||||
The example will use `channel.update` on version `1`.
|
||||
|
||||
## Create the header & source files
|
||||
|
||||
Replace `channel-update`/`channel_update` with your dashed & underscored subscription names
|
||||
|
||||
Header file `include/twitch-eventsub-ws/payloads/channel-update-v1.hpp`:
|
||||
|
||||
```c++
|
||||
#pragma once
|
||||
|
||||
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::channel_update::v1 {
|
||||
|
||||
/// json_transform=snake_case
|
||||
struct Event {
|
||||
// TODO: Fill in your subscription-specific event here
|
||||
};
|
||||
|
||||
struct Payload {
|
||||
const subscription::Subscription subscription;
|
||||
|
||||
const Event event;
|
||||
};
|
||||
|
||||
// DESERIALIZATION DEFINITION START
|
||||
// DESERIALIZATION DEFINITION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::channel_update::v1
|
||||
```
|
||||
|
||||
Source file:
|
||||
|
||||
```c++
|
||||
#include "twitch-eventsub-ws/payloads/channel-update-v1.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::channel_update::v1 {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::channel_update::v1
|
||||
```
|
||||
|
||||
## Run the generation script
|
||||
|
||||
In the root dir of the repostitory, type `make` in your terminal.
|
||||
|
||||
If you can't do that, run the `generate-and-replace-dir.py` script manually (e.g. `./ast/generate-and-replace-dir.py ./src/payloads`)
|
||||
|
||||
## Add the source file to `src/CMakeLists.txt`
|
||||
|
||||
Look for the `# Add your new subscription type source file above this line` comment and add the source file above that line.
|
||||
|
||||
In my example, I added the following line:
|
||||
`payloads/channel-update-v1.cpp`
|
||||
|
||||
## Add a virtual method to the Listener class in `include/twitch-eventsub-ws/listener.hpp`
|
||||
|
||||
Look for the `// Add your new subscription types above this line` comment and add your definition above that line.
|
||||
|
||||
In my example, I added the following code:
|
||||
|
||||
```c++
|
||||
virtual void onChannelUpdate(
|
||||
messages::Metadata metadata,
|
||||
payload::channel_update::v1::Payload payload) = 0;
|
||||
```
|
||||
|
||||
You also need to add an include to your header file
|
||||
|
||||
In my example, I added the following code at the top of the file:
|
||||
|
||||
```c++
|
||||
#include "twitch-eventsub-ws/payloads/channel-update-v1.hpp"
|
||||
```
|
||||
|
||||
## Add a handler for the subscription type in `src/session.cpp`
|
||||
|
||||
Look for the `// Add your new subscription types above this line` comment and add your code above that line.
|
||||
|
||||
In my example, I added the following code:
|
||||
|
||||
```c++
|
||||
{
|
||||
{"channel.update", "1"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload =
|
||||
parsePayload<eventsub::payload::channel_update::v1::Payload>(
|
||||
jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onChannelUpdate(metadata, *oPayload);
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
## Make the test code work in `example/main.cpp`
|
||||
|
||||
Look for the `// Add your new subscription types above this line` comment and add your code above that line.
|
||||
|
||||
In my example, I added the following code:
|
||||
|
||||
```c++
|
||||
void onChannelUpdate(messages::Metadata metadata,
|
||||
payload::channel_update::v1::Payload payload) override
|
||||
{
|
||||
std::cout << "Channel update event!\n";
|
||||
}
|
||||
```
|
||||
|
||||
Full PR that does this: https://github.com/pajlada/beast-websocket-client/pull/19/files
|
||||
|
||||
TODO: Add step for how to verify this using the Twitch CLI, realistically though this will differ from subscription to subscription. Some are simpler than others
|
||||
@@ -0,0 +1,317 @@
|
||||
#include "twitch-eventsub-ws/payloads/channel-ban-v1.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/chrono.hpp"
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::channel_ban::v1 {
|
||||
|
||||
std::chrono::system_clock::duration Event::timeoutDuration() const
|
||||
{
|
||||
if (!this->endsAt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return *this->endsAt - this->bannedAt;
|
||||
}
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Event must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvbroadcasterUserID = root.if_contains("broadcaster_user_id");
|
||||
if (jvbroadcasterUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserID{
|
||||
"Missing required key broadcaster_user_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_broadcasterUserID};
|
||||
}
|
||||
|
||||
auto broadcasterUserID =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserID);
|
||||
|
||||
if (broadcasterUserID.has_error())
|
||||
{
|
||||
return broadcasterUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserLogin =
|
||||
root.if_contains("broadcaster_user_login");
|
||||
if (jvbroadcasterUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserLogin{
|
||||
"Missing required key broadcaster_user_login"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserLogin};
|
||||
}
|
||||
|
||||
auto broadcasterUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserLogin);
|
||||
|
||||
if (broadcasterUserLogin.has_error())
|
||||
{
|
||||
return broadcasterUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserName =
|
||||
root.if_contains("broadcaster_user_name");
|
||||
if (jvbroadcasterUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserName{
|
||||
"Missing required key broadcaster_user_name"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserName};
|
||||
}
|
||||
|
||||
auto broadcasterUserName =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserName);
|
||||
|
||||
if (broadcasterUserName.has_error())
|
||||
{
|
||||
return broadcasterUserName.error();
|
||||
}
|
||||
|
||||
const auto *jvmoderatorUserID = root.if_contains("moderator_user_id");
|
||||
if (jvmoderatorUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_moderatorUserID{
|
||||
"Missing required key moderator_user_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_moderatorUserID};
|
||||
}
|
||||
|
||||
auto moderatorUserID =
|
||||
boost::json::try_value_to<std::string>(*jvmoderatorUserID);
|
||||
|
||||
if (moderatorUserID.has_error())
|
||||
{
|
||||
return moderatorUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvmoderatorUserLogin = root.if_contains("moderator_user_login");
|
||||
if (jvmoderatorUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_moderatorUserLogin{
|
||||
"Missing required key moderator_user_login"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_moderatorUserLogin};
|
||||
}
|
||||
|
||||
auto moderatorUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvmoderatorUserLogin);
|
||||
|
||||
if (moderatorUserLogin.has_error())
|
||||
{
|
||||
return moderatorUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvmoderatorUserName = root.if_contains("moderator_user_name");
|
||||
if (jvmoderatorUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_moderatorUserName{
|
||||
"Missing required key moderator_user_name"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_moderatorUserName};
|
||||
}
|
||||
|
||||
auto moderatorUserName =
|
||||
boost::json::try_value_to<std::string>(*jvmoderatorUserName);
|
||||
|
||||
if (moderatorUserName.has_error())
|
||||
{
|
||||
return moderatorUserName.error();
|
||||
}
|
||||
|
||||
const auto *jvuserID = root.if_contains("user_id");
|
||||
if (jvuserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_userID{
|
||||
"Missing required key user_id"};
|
||||
return boost::system::error_code{129, error_missing_field_userID};
|
||||
}
|
||||
|
||||
auto userID = boost::json::try_value_to<std::string>(*jvuserID);
|
||||
|
||||
if (userID.has_error())
|
||||
{
|
||||
return userID.error();
|
||||
}
|
||||
|
||||
const auto *jvuserLogin = root.if_contains("user_login");
|
||||
if (jvuserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_userLogin{"Missing required key user_login"};
|
||||
return boost::system::error_code{129, error_missing_field_userLogin};
|
||||
}
|
||||
|
||||
auto userLogin = boost::json::try_value_to<std::string>(*jvuserLogin);
|
||||
|
||||
if (userLogin.has_error())
|
||||
{
|
||||
return userLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvuserName = root.if_contains("user_name");
|
||||
if (jvuserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_userName{"Missing required key user_name"};
|
||||
return boost::system::error_code{129, error_missing_field_userName};
|
||||
}
|
||||
|
||||
auto userName = boost::json::try_value_to<std::string>(*jvuserName);
|
||||
|
||||
if (userName.has_error())
|
||||
{
|
||||
return userName.error();
|
||||
}
|
||||
|
||||
const auto *jvreason = root.if_contains("reason");
|
||||
if (jvreason == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_reason{
|
||||
"Missing required key reason"};
|
||||
return boost::system::error_code{129, error_missing_field_reason};
|
||||
}
|
||||
|
||||
auto reason = boost::json::try_value_to<std::string>(*jvreason);
|
||||
|
||||
if (reason.has_error())
|
||||
{
|
||||
return reason.error();
|
||||
}
|
||||
|
||||
const auto *jvisPermanent = root.if_contains("is_permanent");
|
||||
if (jvisPermanent == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_isPermanent{
|
||||
"Missing required key is_permanent"};
|
||||
return boost::system::error_code{129, error_missing_field_isPermanent};
|
||||
}
|
||||
|
||||
auto isPermanent = boost::json::try_value_to<bool>(*jvisPermanent);
|
||||
|
||||
if (isPermanent.has_error())
|
||||
{
|
||||
return isPermanent.error();
|
||||
}
|
||||
|
||||
const auto *jvbannedAt = root.if_contains("banned_at");
|
||||
if (jvbannedAt == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_bannedAt{"Missing required key banned_at"};
|
||||
return boost::system::error_code{129, error_missing_field_bannedAt};
|
||||
}
|
||||
|
||||
auto bannedAt =
|
||||
boost::json::try_value_to<std::chrono::system_clock::time_point>(
|
||||
*jvbannedAt, AsISO8601());
|
||||
|
||||
if (bannedAt.has_error())
|
||||
{
|
||||
return bannedAt.error();
|
||||
}
|
||||
|
||||
std::optional<std::chrono::system_clock::time_point> endsAt = std::nullopt;
|
||||
const auto *jvendsAt = root.if_contains("ends_at");
|
||||
if (jvendsAt != nullptr && !jvendsAt->is_null())
|
||||
{
|
||||
auto tendsAt =
|
||||
boost::json::try_value_to<std::chrono::system_clock::time_point>(
|
||||
*jvendsAt, AsISO8601());
|
||||
|
||||
if (tendsAt.has_error())
|
||||
{
|
||||
return tendsAt.error();
|
||||
}
|
||||
endsAt = std::move(tendsAt.value());
|
||||
}
|
||||
|
||||
return Event{
|
||||
.broadcasterUserID = std::move(broadcasterUserID.value()),
|
||||
.broadcasterUserLogin = std::move(broadcasterUserLogin.value()),
|
||||
.broadcasterUserName = std::move(broadcasterUserName.value()),
|
||||
.moderatorUserID = std::move(moderatorUserID.value()),
|
||||
.moderatorUserLogin = std::move(moderatorUserLogin.value()),
|
||||
.moderatorUserName = std::move(moderatorUserName.value()),
|
||||
.userID = std::move(userID.value()),
|
||||
.userLogin = std::move(userLogin.value()),
|
||||
.userName = std::move(userName.value()),
|
||||
.reason = std::move(reason.value()),
|
||||
.isPermanent = std::move(isPermanent.value()),
|
||||
.bannedAt = std::move(bannedAt.value()),
|
||||
.endsAt = std::move(endsAt),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Payload must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvsubscription = root.if_contains("subscription");
|
||||
if (jvsubscription == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_subscription{
|
||||
"Missing required key subscription"};
|
||||
return boost::system::error_code{129, error_missing_field_subscription};
|
||||
}
|
||||
|
||||
auto subscription =
|
||||
boost::json::try_value_to<subscription::Subscription>(*jvsubscription);
|
||||
|
||||
if (subscription.has_error())
|
||||
{
|
||||
return subscription.error();
|
||||
}
|
||||
|
||||
const auto *jvevent = root.if_contains("event");
|
||||
if (jvevent == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_event{
|
||||
"Missing required key event"};
|
||||
return boost::system::error_code{129, error_missing_field_event};
|
||||
}
|
||||
|
||||
auto event = boost::json::try_value_to<Event>(*jvevent);
|
||||
|
||||
if (event.has_error())
|
||||
{
|
||||
return event.error();
|
||||
}
|
||||
|
||||
return Payload{
|
||||
.subscription = std::move(subscription.value()),
|
||||
.event = std::move(event.value()),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::channel_ban::v1
|
||||
@@ -0,0 +1,934 @@
|
||||
#include "twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::channel_chat_message::v1 {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Badge, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Badge>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Badge must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvsetID = root.if_contains("set_id");
|
||||
if (jvsetID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_setID{
|
||||
"Missing required key set_id"};
|
||||
return boost::system::error_code{129, error_missing_field_setID};
|
||||
}
|
||||
|
||||
auto setID = boost::json::try_value_to<std::string>(*jvsetID);
|
||||
|
||||
if (setID.has_error())
|
||||
{
|
||||
return setID.error();
|
||||
}
|
||||
|
||||
const auto *jvid = root.if_contains("id");
|
||||
if (jvid == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_id{
|
||||
"Missing required key id"};
|
||||
return boost::system::error_code{129, error_missing_field_id};
|
||||
}
|
||||
|
||||
auto id = boost::json::try_value_to<std::string>(*jvid);
|
||||
|
||||
if (id.has_error())
|
||||
{
|
||||
return id.error();
|
||||
}
|
||||
|
||||
const auto *jvinfo = root.if_contains("info");
|
||||
if (jvinfo == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_info{
|
||||
"Missing required key info"};
|
||||
return boost::system::error_code{129, error_missing_field_info};
|
||||
}
|
||||
|
||||
auto info = boost::json::try_value_to<std::string>(*jvinfo);
|
||||
|
||||
if (info.has_error())
|
||||
{
|
||||
return info.error();
|
||||
}
|
||||
|
||||
return Badge{
|
||||
.setID = std::move(setID.value()),
|
||||
.id = std::move(id.value()),
|
||||
.info = std::move(info.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Cheermote, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Cheermote>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Cheermote must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvprefix = root.if_contains("prefix");
|
||||
if (jvprefix == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_prefix{
|
||||
"Missing required key prefix"};
|
||||
return boost::system::error_code{129, error_missing_field_prefix};
|
||||
}
|
||||
|
||||
auto prefix = boost::json::try_value_to<std::string>(*jvprefix);
|
||||
|
||||
if (prefix.has_error())
|
||||
{
|
||||
return prefix.error();
|
||||
}
|
||||
|
||||
const auto *jvbits = root.if_contains("bits");
|
||||
if (jvbits == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_bits{
|
||||
"Missing required key bits"};
|
||||
return boost::system::error_code{129, error_missing_field_bits};
|
||||
}
|
||||
|
||||
auto bits = boost::json::try_value_to<int>(*jvbits);
|
||||
|
||||
if (bits.has_error())
|
||||
{
|
||||
return bits.error();
|
||||
}
|
||||
|
||||
const auto *jvtier = root.if_contains("tier");
|
||||
if (jvtier == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_tier{
|
||||
"Missing required key tier"};
|
||||
return boost::system::error_code{129, error_missing_field_tier};
|
||||
}
|
||||
|
||||
auto tier = boost::json::try_value_to<int>(*jvtier);
|
||||
|
||||
if (tier.has_error())
|
||||
{
|
||||
return tier.error();
|
||||
}
|
||||
|
||||
return Cheermote{
|
||||
.prefix = std::move(prefix.value()),
|
||||
.bits = std::move(bits.value()),
|
||||
.tier = std::move(tier.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Emote, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Emote>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Emote must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvid = root.if_contains("id");
|
||||
if (jvid == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_id{
|
||||
"Missing required key id"};
|
||||
return boost::system::error_code{129, error_missing_field_id};
|
||||
}
|
||||
|
||||
auto id = boost::json::try_value_to<std::string>(*jvid);
|
||||
|
||||
if (id.has_error())
|
||||
{
|
||||
return id.error();
|
||||
}
|
||||
|
||||
const auto *jvemoteSetID = root.if_contains("emote_set_id");
|
||||
if (jvemoteSetID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_emoteSetID{"Missing required key emote_set_id"};
|
||||
return boost::system::error_code{129, error_missing_field_emoteSetID};
|
||||
}
|
||||
|
||||
auto emoteSetID = boost::json::try_value_to<std::string>(*jvemoteSetID);
|
||||
|
||||
if (emoteSetID.has_error())
|
||||
{
|
||||
return emoteSetID.error();
|
||||
}
|
||||
|
||||
const auto *jvownerID = root.if_contains("owner_id");
|
||||
if (jvownerID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_ownerID{"Missing required key owner_id"};
|
||||
return boost::system::error_code{129, error_missing_field_ownerID};
|
||||
}
|
||||
|
||||
auto ownerID = boost::json::try_value_to<std::string>(*jvownerID);
|
||||
|
||||
if (ownerID.has_error())
|
||||
{
|
||||
return ownerID.error();
|
||||
}
|
||||
|
||||
const auto *jvformat = root.if_contains("format");
|
||||
if (jvformat == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_format{
|
||||
"Missing required key format"};
|
||||
return boost::system::error_code{129, error_missing_field_format};
|
||||
}
|
||||
const auto format =
|
||||
boost::json::try_value_to<std::vector<std::string>>(*jvformat);
|
||||
if (format.has_error())
|
||||
{
|
||||
return format.error();
|
||||
}
|
||||
|
||||
return Emote{
|
||||
.id = std::move(id.value()),
|
||||
.emoteSetID = std::move(emoteSetID.value()),
|
||||
.ownerID = std::move(ownerID.value()),
|
||||
.format = format.value(),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Mention, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Mention>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Mention must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvuserID = root.if_contains("user_id");
|
||||
if (jvuserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_userID{
|
||||
"Missing required key user_id"};
|
||||
return boost::system::error_code{129, error_missing_field_userID};
|
||||
}
|
||||
|
||||
auto userID = boost::json::try_value_to<std::string>(*jvuserID);
|
||||
|
||||
if (userID.has_error())
|
||||
{
|
||||
return userID.error();
|
||||
}
|
||||
|
||||
const auto *jvuserName = root.if_contains("user_name");
|
||||
if (jvuserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_userName{"Missing required key user_name"};
|
||||
return boost::system::error_code{129, error_missing_field_userName};
|
||||
}
|
||||
|
||||
auto userName = boost::json::try_value_to<std::string>(*jvuserName);
|
||||
|
||||
if (userName.has_error())
|
||||
{
|
||||
return userName.error();
|
||||
}
|
||||
|
||||
const auto *jvuserLogin = root.if_contains("user_login");
|
||||
if (jvuserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_userLogin{"Missing required key user_login"};
|
||||
return boost::system::error_code{129, error_missing_field_userLogin};
|
||||
}
|
||||
|
||||
auto userLogin = boost::json::try_value_to<std::string>(*jvuserLogin);
|
||||
|
||||
if (userLogin.has_error())
|
||||
{
|
||||
return userLogin.error();
|
||||
}
|
||||
|
||||
return Mention{
|
||||
.userID = std::move(userID.value()),
|
||||
.userName = std::move(userName.value()),
|
||||
.userLogin = std::move(userLogin.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<MessageFragment, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<MessageFragment>,
|
||||
const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"MessageFragment must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvtype = root.if_contains("type");
|
||||
if (jvtype == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_type{
|
||||
"Missing required key type"};
|
||||
return boost::system::error_code{129, error_missing_field_type};
|
||||
}
|
||||
|
||||
auto type = boost::json::try_value_to<std::string>(*jvtype);
|
||||
|
||||
if (type.has_error())
|
||||
{
|
||||
return type.error();
|
||||
}
|
||||
|
||||
const auto *jvtext = root.if_contains("text");
|
||||
if (jvtext == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_text{
|
||||
"Missing required key text"};
|
||||
return boost::system::error_code{129, error_missing_field_text};
|
||||
}
|
||||
|
||||
auto text = boost::json::try_value_to<std::string>(*jvtext);
|
||||
|
||||
if (text.has_error())
|
||||
{
|
||||
return text.error();
|
||||
}
|
||||
|
||||
std::optional<Cheermote> cheermote = std::nullopt;
|
||||
const auto *jvcheermote = root.if_contains("cheermote");
|
||||
if (jvcheermote != nullptr && !jvcheermote->is_null())
|
||||
{
|
||||
auto tcheermote = boost::json::try_value_to<Cheermote>(*jvcheermote);
|
||||
|
||||
if (tcheermote.has_error())
|
||||
{
|
||||
return tcheermote.error();
|
||||
}
|
||||
cheermote = std::move(tcheermote.value());
|
||||
}
|
||||
|
||||
std::optional<Emote> emote = std::nullopt;
|
||||
const auto *jvemote = root.if_contains("emote");
|
||||
if (jvemote != nullptr && !jvemote->is_null())
|
||||
{
|
||||
auto temote = boost::json::try_value_to<Emote>(*jvemote);
|
||||
|
||||
if (temote.has_error())
|
||||
{
|
||||
return temote.error();
|
||||
}
|
||||
emote = std::move(temote.value());
|
||||
}
|
||||
|
||||
std::optional<Mention> mention = std::nullopt;
|
||||
const auto *jvmention = root.if_contains("mention");
|
||||
if (jvmention != nullptr && !jvmention->is_null())
|
||||
{
|
||||
auto tmention = boost::json::try_value_to<Mention>(*jvmention);
|
||||
|
||||
if (tmention.has_error())
|
||||
{
|
||||
return tmention.error();
|
||||
}
|
||||
mention = std::move(tmention.value());
|
||||
}
|
||||
|
||||
return MessageFragment{
|
||||
.type = std::move(type.value()),
|
||||
.text = std::move(text.value()),
|
||||
.cheermote = std::move(cheermote),
|
||||
.emote = std::move(emote),
|
||||
.mention = std::move(mention),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Message, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Message>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Message must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvtext = root.if_contains("text");
|
||||
if (jvtext == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_text{
|
||||
"Missing required key text"};
|
||||
return boost::system::error_code{129, error_missing_field_text};
|
||||
}
|
||||
|
||||
auto text = boost::json::try_value_to<std::string>(*jvtext);
|
||||
|
||||
if (text.has_error())
|
||||
{
|
||||
return text.error();
|
||||
}
|
||||
|
||||
const auto *jvfragments = root.if_contains("fragments");
|
||||
if (jvfragments == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_fragments{"Missing required key fragments"};
|
||||
return boost::system::error_code{129, error_missing_field_fragments};
|
||||
}
|
||||
const auto fragments =
|
||||
boost::json::try_value_to<std::vector<MessageFragment>>(*jvfragments);
|
||||
if (fragments.has_error())
|
||||
{
|
||||
return fragments.error();
|
||||
}
|
||||
|
||||
return Message{
|
||||
.text = std::move(text.value()),
|
||||
.fragments = fragments.value(),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Cheer, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Cheer>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Cheer must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvbits = root.if_contains("bits");
|
||||
if (jvbits == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_bits{
|
||||
"Missing required key bits"};
|
||||
return boost::system::error_code{129, error_missing_field_bits};
|
||||
}
|
||||
|
||||
auto bits = boost::json::try_value_to<int>(*jvbits);
|
||||
|
||||
if (bits.has_error())
|
||||
{
|
||||
return bits.error();
|
||||
}
|
||||
|
||||
return Cheer{
|
||||
.bits = std::move(bits.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Reply, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Reply>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Reply must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvparentMessageID = root.if_contains("parent_message_id");
|
||||
if (jvparentMessageID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_parentMessageID{
|
||||
"Missing required key parent_message_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_parentMessageID};
|
||||
}
|
||||
|
||||
auto parentMessageID =
|
||||
boost::json::try_value_to<std::string>(*jvparentMessageID);
|
||||
|
||||
if (parentMessageID.has_error())
|
||||
{
|
||||
return parentMessageID.error();
|
||||
}
|
||||
|
||||
const auto *jvparentUserID = root.if_contains("parent_user_id");
|
||||
if (jvparentUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_parentUserID{
|
||||
"Missing required key parent_user_id"};
|
||||
return boost::system::error_code{129, error_missing_field_parentUserID};
|
||||
}
|
||||
|
||||
auto parentUserID = boost::json::try_value_to<std::string>(*jvparentUserID);
|
||||
|
||||
if (parentUserID.has_error())
|
||||
{
|
||||
return parentUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvparentUserLogin = root.if_contains("parent_user_login");
|
||||
if (jvparentUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_parentUserLogin{
|
||||
"Missing required key parent_user_login"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_parentUserLogin};
|
||||
}
|
||||
|
||||
auto parentUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvparentUserLogin);
|
||||
|
||||
if (parentUserLogin.has_error())
|
||||
{
|
||||
return parentUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvparentUserName = root.if_contains("parent_user_name");
|
||||
if (jvparentUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_parentUserName{
|
||||
"Missing required key parent_user_name"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_parentUserName};
|
||||
}
|
||||
|
||||
auto parentUserName =
|
||||
boost::json::try_value_to<std::string>(*jvparentUserName);
|
||||
|
||||
if (parentUserName.has_error())
|
||||
{
|
||||
return parentUserName.error();
|
||||
}
|
||||
|
||||
const auto *jvparentMessageBody = root.if_contains("parent_message_body");
|
||||
if (jvparentMessageBody == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_parentMessageBody{
|
||||
"Missing required key parent_message_body"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_parentMessageBody};
|
||||
}
|
||||
|
||||
auto parentMessageBody =
|
||||
boost::json::try_value_to<std::string>(*jvparentMessageBody);
|
||||
|
||||
if (parentMessageBody.has_error())
|
||||
{
|
||||
return parentMessageBody.error();
|
||||
}
|
||||
|
||||
const auto *jvthreadMessageID = root.if_contains("thread_message_id");
|
||||
if (jvthreadMessageID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_threadMessageID{
|
||||
"Missing required key thread_message_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_threadMessageID};
|
||||
}
|
||||
|
||||
auto threadMessageID =
|
||||
boost::json::try_value_to<std::string>(*jvthreadMessageID);
|
||||
|
||||
if (threadMessageID.has_error())
|
||||
{
|
||||
return threadMessageID.error();
|
||||
}
|
||||
|
||||
const auto *jvthreadUserID = root.if_contains("thread_user_id");
|
||||
if (jvthreadUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_threadUserID{
|
||||
"Missing required key thread_user_id"};
|
||||
return boost::system::error_code{129, error_missing_field_threadUserID};
|
||||
}
|
||||
|
||||
auto threadUserID = boost::json::try_value_to<std::string>(*jvthreadUserID);
|
||||
|
||||
if (threadUserID.has_error())
|
||||
{
|
||||
return threadUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvthreadUserLogin = root.if_contains("thread_user_login");
|
||||
if (jvthreadUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_threadUserLogin{
|
||||
"Missing required key thread_user_login"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_threadUserLogin};
|
||||
}
|
||||
|
||||
auto threadUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvthreadUserLogin);
|
||||
|
||||
if (threadUserLogin.has_error())
|
||||
{
|
||||
return threadUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvthreadUserName = root.if_contains("thread_user_name");
|
||||
if (jvthreadUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_threadUserName{
|
||||
"Missing required key thread_user_name"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_threadUserName};
|
||||
}
|
||||
|
||||
auto threadUserName =
|
||||
boost::json::try_value_to<std::string>(*jvthreadUserName);
|
||||
|
||||
if (threadUserName.has_error())
|
||||
{
|
||||
return threadUserName.error();
|
||||
}
|
||||
|
||||
return Reply{
|
||||
.parentMessageID = std::move(parentMessageID.value()),
|
||||
.parentUserID = std::move(parentUserID.value()),
|
||||
.parentUserLogin = std::move(parentUserLogin.value()),
|
||||
.parentUserName = std::move(parentUserName.value()),
|
||||
.parentMessageBody = std::move(parentMessageBody.value()),
|
||||
.threadMessageID = std::move(threadMessageID.value()),
|
||||
.threadUserID = std::move(threadUserID.value()),
|
||||
.threadUserLogin = std::move(threadUserLogin.value()),
|
||||
.threadUserName = std::move(threadUserName.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Event must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvbroadcasterUserID = root.if_contains("broadcaster_user_id");
|
||||
if (jvbroadcasterUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserID{
|
||||
"Missing required key broadcaster_user_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_broadcasterUserID};
|
||||
}
|
||||
|
||||
auto broadcasterUserID =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserID);
|
||||
|
||||
if (broadcasterUserID.has_error())
|
||||
{
|
||||
return broadcasterUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserLogin =
|
||||
root.if_contains("broadcaster_user_login");
|
||||
if (jvbroadcasterUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserLogin{
|
||||
"Missing required key broadcaster_user_login"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserLogin};
|
||||
}
|
||||
|
||||
auto broadcasterUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserLogin);
|
||||
|
||||
if (broadcasterUserLogin.has_error())
|
||||
{
|
||||
return broadcasterUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserName =
|
||||
root.if_contains("broadcaster_user_name");
|
||||
if (jvbroadcasterUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserName{
|
||||
"Missing required key broadcaster_user_name"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserName};
|
||||
}
|
||||
|
||||
auto broadcasterUserName =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserName);
|
||||
|
||||
if (broadcasterUserName.has_error())
|
||||
{
|
||||
return broadcasterUserName.error();
|
||||
}
|
||||
|
||||
const auto *jvchatterUserID = root.if_contains("chatter_user_id");
|
||||
if (jvchatterUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_chatterUserID{
|
||||
"Missing required key chatter_user_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_chatterUserID};
|
||||
}
|
||||
|
||||
auto chatterUserID =
|
||||
boost::json::try_value_to<std::string>(*jvchatterUserID);
|
||||
|
||||
if (chatterUserID.has_error())
|
||||
{
|
||||
return chatterUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvchatterUserLogin = root.if_contains("chatter_user_login");
|
||||
if (jvchatterUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_chatterUserLogin{
|
||||
"Missing required key chatter_user_login"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_chatterUserLogin};
|
||||
}
|
||||
|
||||
auto chatterUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvchatterUserLogin);
|
||||
|
||||
if (chatterUserLogin.has_error())
|
||||
{
|
||||
return chatterUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvchatterUserName = root.if_contains("chatter_user_name");
|
||||
if (jvchatterUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_chatterUserName{
|
||||
"Missing required key chatter_user_name"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_chatterUserName};
|
||||
}
|
||||
|
||||
auto chatterUserName =
|
||||
boost::json::try_value_to<std::string>(*jvchatterUserName);
|
||||
|
||||
if (chatterUserName.has_error())
|
||||
{
|
||||
return chatterUserName.error();
|
||||
}
|
||||
|
||||
const auto *jvcolor = root.if_contains("color");
|
||||
if (jvcolor == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_color{
|
||||
"Missing required key color"};
|
||||
return boost::system::error_code{129, error_missing_field_color};
|
||||
}
|
||||
|
||||
auto color = boost::json::try_value_to<std::string>(*jvcolor);
|
||||
|
||||
if (color.has_error())
|
||||
{
|
||||
return color.error();
|
||||
}
|
||||
|
||||
const auto *jvbadges = root.if_contains("badges");
|
||||
if (jvbadges == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_badges{
|
||||
"Missing required key badges"};
|
||||
return boost::system::error_code{129, error_missing_field_badges};
|
||||
}
|
||||
const auto badges =
|
||||
boost::json::try_value_to<std::vector<Badge>>(*jvbadges);
|
||||
if (badges.has_error())
|
||||
{
|
||||
return badges.error();
|
||||
}
|
||||
|
||||
const auto *jvmessageID = root.if_contains("message_id");
|
||||
if (jvmessageID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_messageID{"Missing required key message_id"};
|
||||
return boost::system::error_code{129, error_missing_field_messageID};
|
||||
}
|
||||
|
||||
auto messageID = boost::json::try_value_to<std::string>(*jvmessageID);
|
||||
|
||||
if (messageID.has_error())
|
||||
{
|
||||
return messageID.error();
|
||||
}
|
||||
|
||||
const auto *jvmessageType = root.if_contains("message_type");
|
||||
if (jvmessageType == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_messageType{
|
||||
"Missing required key message_type"};
|
||||
return boost::system::error_code{129, error_missing_field_messageType};
|
||||
}
|
||||
|
||||
auto messageType = boost::json::try_value_to<std::string>(*jvmessageType);
|
||||
|
||||
if (messageType.has_error())
|
||||
{
|
||||
return messageType.error();
|
||||
}
|
||||
|
||||
const auto *jvmessage = root.if_contains("message");
|
||||
if (jvmessage == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_message{"Missing required key message"};
|
||||
return boost::system::error_code{129, error_missing_field_message};
|
||||
}
|
||||
|
||||
auto message = boost::json::try_value_to<Message>(*jvmessage);
|
||||
|
||||
if (message.has_error())
|
||||
{
|
||||
return message.error();
|
||||
}
|
||||
|
||||
std::optional<Cheer> cheer = std::nullopt;
|
||||
const auto *jvcheer = root.if_contains("cheer");
|
||||
if (jvcheer != nullptr && !jvcheer->is_null())
|
||||
{
|
||||
auto tcheer = boost::json::try_value_to<Cheer>(*jvcheer);
|
||||
|
||||
if (tcheer.has_error())
|
||||
{
|
||||
return tcheer.error();
|
||||
}
|
||||
cheer = std::move(tcheer.value());
|
||||
}
|
||||
|
||||
std::optional<Reply> reply = std::nullopt;
|
||||
const auto *jvreply = root.if_contains("reply");
|
||||
if (jvreply != nullptr && !jvreply->is_null())
|
||||
{
|
||||
auto treply = boost::json::try_value_to<Reply>(*jvreply);
|
||||
|
||||
if (treply.has_error())
|
||||
{
|
||||
return treply.error();
|
||||
}
|
||||
reply = std::move(treply.value());
|
||||
}
|
||||
|
||||
std::optional<std::string> channelPointsCustomRewardID = std::nullopt;
|
||||
const auto *jvchannelPointsCustomRewardID =
|
||||
root.if_contains("channel_points_custom_reward_id");
|
||||
if (jvchannelPointsCustomRewardID != nullptr &&
|
||||
!jvchannelPointsCustomRewardID->is_null())
|
||||
{
|
||||
auto tchannelPointsCustomRewardID =
|
||||
boost::json::try_value_to<std::string>(
|
||||
*jvchannelPointsCustomRewardID);
|
||||
|
||||
if (tchannelPointsCustomRewardID.has_error())
|
||||
{
|
||||
return tchannelPointsCustomRewardID.error();
|
||||
}
|
||||
channelPointsCustomRewardID =
|
||||
std::move(tchannelPointsCustomRewardID.value());
|
||||
}
|
||||
|
||||
return Event{
|
||||
.broadcasterUserID = std::move(broadcasterUserID.value()),
|
||||
.broadcasterUserLogin = std::move(broadcasterUserLogin.value()),
|
||||
.broadcasterUserName = std::move(broadcasterUserName.value()),
|
||||
.chatterUserID = std::move(chatterUserID.value()),
|
||||
.chatterUserLogin = std::move(chatterUserLogin.value()),
|
||||
.chatterUserName = std::move(chatterUserName.value()),
|
||||
.color = std::move(color.value()),
|
||||
.badges = badges.value(),
|
||||
.messageID = std::move(messageID.value()),
|
||||
.messageType = std::move(messageType.value()),
|
||||
.message = std::move(message.value()),
|
||||
.cheer = std::move(cheer),
|
||||
.reply = std::move(reply),
|
||||
.channelPointsCustomRewardID = std::move(channelPointsCustomRewardID),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Payload must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvsubscription = root.if_contains("subscription");
|
||||
if (jvsubscription == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_subscription{
|
||||
"Missing required key subscription"};
|
||||
return boost::system::error_code{129, error_missing_field_subscription};
|
||||
}
|
||||
|
||||
auto subscription =
|
||||
boost::json::try_value_to<subscription::Subscription>(*jvsubscription);
|
||||
|
||||
if (subscription.has_error())
|
||||
{
|
||||
return subscription.error();
|
||||
}
|
||||
|
||||
const auto *jvevent = root.if_contains("event");
|
||||
if (jvevent == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_event{
|
||||
"Missing required key event"};
|
||||
return boost::system::error_code{129, error_missing_field_event};
|
||||
}
|
||||
|
||||
auto event = boost::json::try_value_to<Event>(*jvevent);
|
||||
|
||||
if (event.has_error())
|
||||
{
|
||||
return event.error();
|
||||
}
|
||||
|
||||
return Payload{
|
||||
.subscription = std::move(subscription.value()),
|
||||
.event = std::move(event.value()),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::channel_chat_message::v1
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
||||
#include "twitch-eventsub-ws/payloads/channel-update-v1.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::channel_update::v1 {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Event must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvbroadcasterUserID = root.if_contains("broadcaster_user_id");
|
||||
if (jvbroadcasterUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserID{
|
||||
"Missing required key broadcaster_user_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_broadcasterUserID};
|
||||
}
|
||||
|
||||
auto broadcasterUserID =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserID);
|
||||
|
||||
if (broadcasterUserID.has_error())
|
||||
{
|
||||
return broadcasterUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserLogin =
|
||||
root.if_contains("broadcaster_user_login");
|
||||
if (jvbroadcasterUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserLogin{
|
||||
"Missing required key broadcaster_user_login"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserLogin};
|
||||
}
|
||||
|
||||
auto broadcasterUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserLogin);
|
||||
|
||||
if (broadcasterUserLogin.has_error())
|
||||
{
|
||||
return broadcasterUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserName =
|
||||
root.if_contains("broadcaster_user_name");
|
||||
if (jvbroadcasterUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserName{
|
||||
"Missing required key broadcaster_user_name"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserName};
|
||||
}
|
||||
|
||||
auto broadcasterUserName =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserName);
|
||||
|
||||
if (broadcasterUserName.has_error())
|
||||
{
|
||||
return broadcasterUserName.error();
|
||||
}
|
||||
|
||||
const auto *jvtitle = root.if_contains("title");
|
||||
if (jvtitle == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_title{
|
||||
"Missing required key title"};
|
||||
return boost::system::error_code{129, error_missing_field_title};
|
||||
}
|
||||
|
||||
auto title = boost::json::try_value_to<std::string>(*jvtitle);
|
||||
|
||||
if (title.has_error())
|
||||
{
|
||||
return title.error();
|
||||
}
|
||||
|
||||
const auto *jvlanguage = root.if_contains("language");
|
||||
if (jvlanguage == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_language{"Missing required key language"};
|
||||
return boost::system::error_code{129, error_missing_field_language};
|
||||
}
|
||||
|
||||
auto language = boost::json::try_value_to<std::string>(*jvlanguage);
|
||||
|
||||
if (language.has_error())
|
||||
{
|
||||
return language.error();
|
||||
}
|
||||
|
||||
const auto *jvcategoryID = root.if_contains("category_id");
|
||||
if (jvcategoryID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_categoryID{"Missing required key category_id"};
|
||||
return boost::system::error_code{129, error_missing_field_categoryID};
|
||||
}
|
||||
|
||||
auto categoryID = boost::json::try_value_to<std::string>(*jvcategoryID);
|
||||
|
||||
if (categoryID.has_error())
|
||||
{
|
||||
return categoryID.error();
|
||||
}
|
||||
|
||||
const auto *jvcategoryName = root.if_contains("category_name");
|
||||
if (jvcategoryName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_categoryName{
|
||||
"Missing required key category_name"};
|
||||
return boost::system::error_code{129, error_missing_field_categoryName};
|
||||
}
|
||||
|
||||
auto categoryName = boost::json::try_value_to<std::string>(*jvcategoryName);
|
||||
|
||||
if (categoryName.has_error())
|
||||
{
|
||||
return categoryName.error();
|
||||
}
|
||||
|
||||
const auto *jvisMature = root.if_contains("is_mature");
|
||||
if (jvisMature == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_isMature{"Missing required key is_mature"};
|
||||
return boost::system::error_code{129, error_missing_field_isMature};
|
||||
}
|
||||
|
||||
auto isMature = boost::json::try_value_to<bool>(*jvisMature);
|
||||
|
||||
if (isMature.has_error())
|
||||
{
|
||||
return isMature.error();
|
||||
}
|
||||
|
||||
return Event{
|
||||
.broadcasterUserID = std::move(broadcasterUserID.value()),
|
||||
.broadcasterUserLogin = std::move(broadcasterUserLogin.value()),
|
||||
.broadcasterUserName = std::move(broadcasterUserName.value()),
|
||||
.title = std::move(title.value()),
|
||||
.language = std::move(language.value()),
|
||||
.categoryID = std::move(categoryID.value()),
|
||||
.categoryName = std::move(categoryName.value()),
|
||||
.isMature = std::move(isMature.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Payload must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvsubscription = root.if_contains("subscription");
|
||||
if (jvsubscription == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_subscription{
|
||||
"Missing required key subscription"};
|
||||
return boost::system::error_code{129, error_missing_field_subscription};
|
||||
}
|
||||
|
||||
auto subscription =
|
||||
boost::json::try_value_to<subscription::Subscription>(*jvsubscription);
|
||||
|
||||
if (subscription.has_error())
|
||||
{
|
||||
return subscription.error();
|
||||
}
|
||||
|
||||
const auto *jvevent = root.if_contains("event");
|
||||
if (jvevent == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_event{
|
||||
"Missing required key event"};
|
||||
return boost::system::error_code{129, error_missing_field_event};
|
||||
}
|
||||
|
||||
auto event = boost::json::try_value_to<Event>(*jvevent);
|
||||
|
||||
if (event.has_error())
|
||||
{
|
||||
return event.error();
|
||||
}
|
||||
|
||||
return Payload{
|
||||
.subscription = std::move(subscription.value()),
|
||||
.event = std::move(event.value()),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::channel_update::v1
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
subscription_name="$1"
|
||||
subscription_version="$2"
|
||||
|
||||
usage() {
|
||||
>&2 echo "Usage: $0 <name> <version> (e.g. $0 channel.ban v1)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "$subscription_name" ]; then
|
||||
>&2 echo "Missing subscription name"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -z "$subscription_version" ]; then
|
||||
>&2 echo "Missing subscription version"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Clean up subscription name
|
||||
|
||||
dashed_subscription_name="$(echo "$subscription_name" | sed 's/\./-/g')"
|
||||
underscored_subscription_name="$(echo "$subscription_name" | sed 's/\./_/g')"
|
||||
echo "Subscription name: $subscription_name ($dashed_subscription_name)"
|
||||
echo "Subscription version: $subscription_version"
|
||||
|
||||
header_file_name="${dashed_subscription_name}-${subscription_version}.hpp"
|
||||
source_file_name="${dashed_subscription_name}-${subscription_version}.cpp"
|
||||
|
||||
# Write the header file
|
||||
cat > "$SCRIPT_DIR/../../include/twitch-eventsub-ws/payloads/$header_file_name" << EOF
|
||||
#pragma once
|
||||
|
||||
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::$underscored_subscription_name::$subscription_version {
|
||||
|
||||
/// json_transform=snake_case
|
||||
struct Event {
|
||||
// TODO: Fill in your subscription-specific event here
|
||||
};
|
||||
|
||||
struct Payload {
|
||||
const subscription::Subscription subscription;
|
||||
|
||||
const Event event;
|
||||
};
|
||||
|
||||
// DESERIALIZATION DEFINITION START
|
||||
// DESERIALIZATION DEFINITION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::$underscored_subscription_name::$subscription_version
|
||||
EOF
|
||||
|
||||
# Write the source file
|
||||
cat > "$SCRIPT_DIR/$source_file_name" << EOF
|
||||
#include "twitch-eventsub-ws/payloads/$header_file_name"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::$underscored_subscription_name::$subscription_version {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::$underscored_subscription_name::$subscription_version
|
||||
EOF
|
||||
|
||||
echo "Steps that are left for you:"
|
||||
echo "1. Add a virtual method to the Listener class in include/twitch-eventsub-ws/listener.hpp"
|
||||
echo "2. Add a handler for this subscription in src/session.cpp's NOTIFICATION_HANDLERS"
|
||||
echo "3. Add payloads/${source_file_name} to src/CMakeLists.txt"
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "twitch-eventsub-ws/payloads/session-welcome.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::session_welcome {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Payload must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &outerRoot = jvRoot.get_object();
|
||||
|
||||
const auto *jvInnerRoot = outerRoot.if_contains("session");
|
||||
if (jvInnerRoot == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMissing{
|
||||
"Payload's key session is missing"};
|
||||
return boost::system::error_code{129, errorMissing};
|
||||
}
|
||||
if (!jvInnerRoot->is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Payload's session must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvInnerRoot->get_object();
|
||||
|
||||
const auto *jvid = root.if_contains("id");
|
||||
if (jvid == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_id{
|
||||
"Missing required key id"};
|
||||
return boost::system::error_code{129, error_missing_field_id};
|
||||
}
|
||||
|
||||
auto id = boost::json::try_value_to<std::string>(*jvid);
|
||||
|
||||
if (id.has_error())
|
||||
{
|
||||
return id.error();
|
||||
}
|
||||
|
||||
return Payload{
|
||||
.id = std::move(id.value()),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::session_welcome
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "twitch-eventsub-ws/payloads/stream-offline-v1.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::stream_offline::v1 {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Event must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvbroadcasterUserID = root.if_contains("broadcaster_user_id");
|
||||
if (jvbroadcasterUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserID{
|
||||
"Missing required key broadcaster_user_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_broadcasterUserID};
|
||||
}
|
||||
|
||||
auto broadcasterUserID =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserID);
|
||||
|
||||
if (broadcasterUserID.has_error())
|
||||
{
|
||||
return broadcasterUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserLogin =
|
||||
root.if_contains("broadcaster_user_login");
|
||||
if (jvbroadcasterUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserLogin{
|
||||
"Missing required key broadcaster_user_login"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserLogin};
|
||||
}
|
||||
|
||||
auto broadcasterUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserLogin);
|
||||
|
||||
if (broadcasterUserLogin.has_error())
|
||||
{
|
||||
return broadcasterUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserName =
|
||||
root.if_contains("broadcaster_user_name");
|
||||
if (jvbroadcasterUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserName{
|
||||
"Missing required key broadcaster_user_name"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserName};
|
||||
}
|
||||
|
||||
auto broadcasterUserName =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserName);
|
||||
|
||||
if (broadcasterUserName.has_error())
|
||||
{
|
||||
return broadcasterUserName.error();
|
||||
}
|
||||
|
||||
return Event{
|
||||
.broadcasterUserID = std::move(broadcasterUserID.value()),
|
||||
.broadcasterUserLogin = std::move(broadcasterUserLogin.value()),
|
||||
.broadcasterUserName = std::move(broadcasterUserName.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Payload must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvsubscription = root.if_contains("subscription");
|
||||
if (jvsubscription == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_subscription{
|
||||
"Missing required key subscription"};
|
||||
return boost::system::error_code{129, error_missing_field_subscription};
|
||||
}
|
||||
|
||||
auto subscription =
|
||||
boost::json::try_value_to<subscription::Subscription>(*jvsubscription);
|
||||
|
||||
if (subscription.has_error())
|
||||
{
|
||||
return subscription.error();
|
||||
}
|
||||
|
||||
const auto *jvevent = root.if_contains("event");
|
||||
if (jvevent == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_event{
|
||||
"Missing required key event"};
|
||||
return boost::system::error_code{129, error_missing_field_event};
|
||||
}
|
||||
|
||||
auto event = boost::json::try_value_to<Event>(*jvevent);
|
||||
|
||||
if (event.has_error())
|
||||
{
|
||||
return event.error();
|
||||
}
|
||||
|
||||
return Payload{
|
||||
.subscription = std::move(subscription.value()),
|
||||
.event = std::move(event.value()),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::stream_offline::v1
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "twitch-eventsub-ws/payloads/stream-online-v1.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::stream_online::v1 {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Event must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvid = root.if_contains("id");
|
||||
if (jvid == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_id{
|
||||
"Missing required key id"};
|
||||
return boost::system::error_code{129, error_missing_field_id};
|
||||
}
|
||||
|
||||
auto id = boost::json::try_value_to<std::string>(*jvid);
|
||||
|
||||
if (id.has_error())
|
||||
{
|
||||
return id.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserID = root.if_contains("broadcaster_user_id");
|
||||
if (jvbroadcasterUserID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserID{
|
||||
"Missing required key broadcaster_user_id"};
|
||||
return boost::system::error_code{129,
|
||||
error_missing_field_broadcasterUserID};
|
||||
}
|
||||
|
||||
auto broadcasterUserID =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserID);
|
||||
|
||||
if (broadcasterUserID.has_error())
|
||||
{
|
||||
return broadcasterUserID.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserLogin =
|
||||
root.if_contains("broadcaster_user_login");
|
||||
if (jvbroadcasterUserLogin == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserLogin{
|
||||
"Missing required key broadcaster_user_login"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserLogin};
|
||||
}
|
||||
|
||||
auto broadcasterUserLogin =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserLogin);
|
||||
|
||||
if (broadcasterUserLogin.has_error())
|
||||
{
|
||||
return broadcasterUserLogin.error();
|
||||
}
|
||||
|
||||
const auto *jvbroadcasterUserName =
|
||||
root.if_contains("broadcaster_user_name");
|
||||
if (jvbroadcasterUserName == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_broadcasterUserName{
|
||||
"Missing required key broadcaster_user_name"};
|
||||
return boost::system::error_code{
|
||||
129, error_missing_field_broadcasterUserName};
|
||||
}
|
||||
|
||||
auto broadcasterUserName =
|
||||
boost::json::try_value_to<std::string>(*jvbroadcasterUserName);
|
||||
|
||||
if (broadcasterUserName.has_error())
|
||||
{
|
||||
return broadcasterUserName.error();
|
||||
}
|
||||
|
||||
const auto *jvtype = root.if_contains("type");
|
||||
if (jvtype == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_type{
|
||||
"Missing required key type"};
|
||||
return boost::system::error_code{129, error_missing_field_type};
|
||||
}
|
||||
|
||||
auto type = boost::json::try_value_to<std::string>(*jvtype);
|
||||
|
||||
if (type.has_error())
|
||||
{
|
||||
return type.error();
|
||||
}
|
||||
|
||||
const auto *jvstartedAt = root.if_contains("started_at");
|
||||
if (jvstartedAt == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_startedAt{"Missing required key started_at"};
|
||||
return boost::system::error_code{129, error_missing_field_startedAt};
|
||||
}
|
||||
|
||||
auto startedAt = boost::json::try_value_to<std::string>(*jvstartedAt);
|
||||
|
||||
if (startedAt.has_error())
|
||||
{
|
||||
return startedAt.error();
|
||||
}
|
||||
|
||||
return Event{
|
||||
.id = std::move(id.value()),
|
||||
.broadcasterUserID = std::move(broadcasterUserID.value()),
|
||||
.broadcasterUserLogin = std::move(broadcasterUserLogin.value()),
|
||||
.broadcasterUserName = std::move(broadcasterUserName.value()),
|
||||
.type = std::move(type.value()),
|
||||
.startedAt = std::move(startedAt.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Payload must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvsubscription = root.if_contains("subscription");
|
||||
if (jvsubscription == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_subscription{
|
||||
"Missing required key subscription"};
|
||||
return boost::system::error_code{129, error_missing_field_subscription};
|
||||
}
|
||||
|
||||
auto subscription =
|
||||
boost::json::try_value_to<subscription::Subscription>(*jvsubscription);
|
||||
|
||||
if (subscription.has_error())
|
||||
{
|
||||
return subscription.error();
|
||||
}
|
||||
|
||||
const auto *jvevent = root.if_contains("event");
|
||||
if (jvevent == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_event{
|
||||
"Missing required key event"};
|
||||
return boost::system::error_code{129, error_missing_field_event};
|
||||
}
|
||||
|
||||
auto event = boost::json::try_value_to<Event>(*jvevent);
|
||||
|
||||
if (event.has_error())
|
||||
{
|
||||
return event.error();
|
||||
}
|
||||
|
||||
return Payload{
|
||||
.subscription = std::move(subscription.value()),
|
||||
.event = std::move(event.value()),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::stream_online::v1
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/errors.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
namespace chatterino::eventsub::lib::payload::subscription {
|
||||
|
||||
// DESERIALIZATION IMPLEMENTATION START
|
||||
boost::json::result_for<Transport, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Transport>, const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Transport must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvmethod = root.if_contains("method");
|
||||
if (jvmethod == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_method{
|
||||
"Missing required key method"};
|
||||
return boost::system::error_code{129, error_missing_field_method};
|
||||
}
|
||||
|
||||
auto method = boost::json::try_value_to<std::string>(*jvmethod);
|
||||
|
||||
if (method.has_error())
|
||||
{
|
||||
return method.error();
|
||||
}
|
||||
|
||||
const auto *jvsessionID = root.if_contains("session_id");
|
||||
if (jvsessionID == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_sessionID{"Missing required key session_id"};
|
||||
return boost::system::error_code{129, error_missing_field_sessionID};
|
||||
}
|
||||
|
||||
auto sessionID = boost::json::try_value_to<std::string>(*jvsessionID);
|
||||
|
||||
if (sessionID.has_error())
|
||||
{
|
||||
return sessionID.error();
|
||||
}
|
||||
|
||||
return Transport{
|
||||
.method = std::move(method.value()),
|
||||
.sessionID = std::move(sessionID.value()),
|
||||
};
|
||||
}
|
||||
|
||||
boost::json::result_for<Subscription, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<Subscription>,
|
||||
const boost::json::value &jvRoot)
|
||||
{
|
||||
if (!jvRoot.is_object())
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorMustBeObject{
|
||||
"Subscription must be an object"};
|
||||
return boost::system::error_code{129, errorMustBeObject};
|
||||
}
|
||||
const auto &root = jvRoot.get_object();
|
||||
|
||||
const auto *jvid = root.if_contains("id");
|
||||
if (jvid == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_id{
|
||||
"Missing required key id"};
|
||||
return boost::system::error_code{129, error_missing_field_id};
|
||||
}
|
||||
|
||||
auto id = boost::json::try_value_to<std::string>(*jvid);
|
||||
|
||||
if (id.has_error())
|
||||
{
|
||||
return id.error();
|
||||
}
|
||||
|
||||
const auto *jvstatus = root.if_contains("status");
|
||||
if (jvstatus == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_status{
|
||||
"Missing required key status"};
|
||||
return boost::system::error_code{129, error_missing_field_status};
|
||||
}
|
||||
|
||||
auto status = boost::json::try_value_to<std::string>(*jvstatus);
|
||||
|
||||
if (status.has_error())
|
||||
{
|
||||
return status.error();
|
||||
}
|
||||
|
||||
const auto *jvtype = root.if_contains("type");
|
||||
if (jvtype == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_type{
|
||||
"Missing required key type"};
|
||||
return boost::system::error_code{129, error_missing_field_type};
|
||||
}
|
||||
|
||||
auto type = boost::json::try_value_to<std::string>(*jvtype);
|
||||
|
||||
if (type.has_error())
|
||||
{
|
||||
return type.error();
|
||||
}
|
||||
|
||||
const auto *jvversion = root.if_contains("version");
|
||||
if (jvversion == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_version{"Missing required key version"};
|
||||
return boost::system::error_code{129, error_missing_field_version};
|
||||
}
|
||||
|
||||
auto version = boost::json::try_value_to<std::string>(*jvversion);
|
||||
|
||||
if (version.has_error())
|
||||
{
|
||||
return version.error();
|
||||
}
|
||||
|
||||
const auto *jvtransport = root.if_contains("transport");
|
||||
if (jvtransport == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_transport{"Missing required key transport"};
|
||||
return boost::system::error_code{129, error_missing_field_transport};
|
||||
}
|
||||
|
||||
auto transport = boost::json::try_value_to<Transport>(*jvtransport);
|
||||
|
||||
if (transport.has_error())
|
||||
{
|
||||
return transport.error();
|
||||
}
|
||||
|
||||
const auto *jvcreatedAt = root.if_contains("created_at");
|
||||
if (jvcreatedAt == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
error_missing_field_createdAt{"Missing required key created_at"};
|
||||
return boost::system::error_code{129, error_missing_field_createdAt};
|
||||
}
|
||||
|
||||
auto createdAt = boost::json::try_value_to<std::string>(*jvcreatedAt);
|
||||
|
||||
if (createdAt.has_error())
|
||||
{
|
||||
return createdAt.error();
|
||||
}
|
||||
|
||||
const auto *jvcost = root.if_contains("cost");
|
||||
if (jvcost == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory error_missing_field_cost{
|
||||
"Missing required key cost"};
|
||||
return boost::system::error_code{129, error_missing_field_cost};
|
||||
}
|
||||
|
||||
auto cost = boost::json::try_value_to<int>(*jvcost);
|
||||
|
||||
if (cost.has_error())
|
||||
{
|
||||
return cost.error();
|
||||
}
|
||||
|
||||
return Subscription{
|
||||
.id = std::move(id.value()),
|
||||
.status = std::move(status.value()),
|
||||
.type = std::move(type.value()),
|
||||
.version = std::move(version.value()),
|
||||
.transport = std::move(transport.value()),
|
||||
.createdAt = std::move(createdAt.value()),
|
||||
.cost = std::move(cost.value()),
|
||||
};
|
||||
}
|
||||
// DESERIALIZATION IMPLEMENTATION END
|
||||
|
||||
} // namespace chatterino::eventsub::lib::payload::subscription
|
||||
@@ -0,0 +1,438 @@
|
||||
#include "twitch-eventsub-ws/session.hpp"
|
||||
|
||||
#include "twitch-eventsub-ws/listener.hpp"
|
||||
#include "twitch-eventsub-ws/messages/metadata.hpp"
|
||||
#include "twitch-eventsub-ws/payloads/channel-ban-v1.hpp"
|
||||
#include "twitch-eventsub-ws/payloads/session-welcome.hpp"
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/beast/websocket/ssl.hpp>
|
||||
#include <boost/container_hash/hash.hpp>
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace websocket = beast::websocket;
|
||||
|
||||
namespace chatterino::eventsub::lib {
|
||||
|
||||
// Subscription Type + Subscription Version
|
||||
using EventSubSubscription = std::pair<std::string, std::string>;
|
||||
|
||||
using NotificationHandlers = std::unordered_map<
|
||||
EventSubSubscription,
|
||||
std::function<void(messages::Metadata, boost::json::value,
|
||||
std::unique_ptr<Listener> &)>,
|
||||
boost::hash<EventSubSubscription>>;
|
||||
|
||||
using MessageHandlers = std::unordered_map<
|
||||
std::string, std::function<void(messages::Metadata, boost::json::value,
|
||||
std::unique_ptr<Listener> &,
|
||||
const NotificationHandlers &)>>;
|
||||
|
||||
namespace {
|
||||
|
||||
// Report a failure
|
||||
void fail(beast::error_code ec, char const *what)
|
||||
{
|
||||
std::cerr << what << ": " << ec.message() << "\n";
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::optional<T> parsePayload(const boost::json::value &jv)
|
||||
{
|
||||
auto result = boost::json::try_value_to<T>(jv);
|
||||
if (!result.has_value())
|
||||
{
|
||||
fail(result.error(), "parsing payload");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::move(result.value());
|
||||
}
|
||||
|
||||
// Subscription types
|
||||
const NotificationHandlers NOTIFICATION_HANDLERS{
|
||||
{
|
||||
{"channel.ban", "1"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload =
|
||||
parsePayload<payload::channel_ban::v1::Payload>(jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onChannelBan(metadata, *oPayload);
|
||||
},
|
||||
},
|
||||
{
|
||||
{"stream.online", "1"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload =
|
||||
parsePayload<payload::stream_online::v1::Payload>(jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onStreamOnline(metadata, *oPayload);
|
||||
},
|
||||
},
|
||||
{
|
||||
{"stream.offline", "1"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload =
|
||||
parsePayload<payload::stream_offline::v1::Payload>(jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onStreamOffline(metadata, *oPayload);
|
||||
},
|
||||
},
|
||||
{
|
||||
{"channel.chat.notification", "1"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload = parsePayload<
|
||||
payload::channel_chat_notification::v1::Payload>(jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onChannelChatNotification(metadata, *oPayload);
|
||||
},
|
||||
},
|
||||
{
|
||||
{"channel.update", "1"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload =
|
||||
parsePayload<payload::channel_update::v1::Payload>(jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onChannelUpdate(metadata, *oPayload);
|
||||
},
|
||||
},
|
||||
{
|
||||
{"channel.chat.message", "1"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload =
|
||||
parsePayload<payload::channel_chat_message::v1::Payload>(
|
||||
jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onChannelChatMessage(metadata, *oPayload);
|
||||
},
|
||||
},
|
||||
{
|
||||
{"channel.moderate", "2"},
|
||||
[](const auto &metadata, const auto &jv, auto &listener) {
|
||||
auto oPayload =
|
||||
parsePayload<payload::channel_moderate::v2::Payload>(jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
listener->onChannelModerate(metadata, std::move(*oPayload));
|
||||
},
|
||||
},
|
||||
// Add your new subscription types above this line
|
||||
};
|
||||
|
||||
const MessageHandlers MESSAGE_HANDLERS{
|
||||
{
|
||||
"session_welcome",
|
||||
[](const auto &metadata, const auto &jv, auto &listener,
|
||||
const auto & /*notificationHandlers*/) {
|
||||
auto oPayload =
|
||||
parsePayload<payload::session_welcome::Payload>(jv);
|
||||
if (!oPayload)
|
||||
{
|
||||
// TODO: error handling
|
||||
return;
|
||||
}
|
||||
const auto &payload = *oPayload;
|
||||
|
||||
listener->onSessionWelcome(metadata, payload);
|
||||
},
|
||||
},
|
||||
{
|
||||
"session_keepalive",
|
||||
[](const auto &metadata, const auto &jv, auto &listener,
|
||||
const auto ¬ificationHandlers) {
|
||||
// TODO: should we do something here?
|
||||
},
|
||||
},
|
||||
{
|
||||
"notification",
|
||||
[](const auto &metadata, const auto &jv, auto &listener,
|
||||
const auto ¬ificationHandlers) {
|
||||
listener->onNotification(metadata, jv);
|
||||
|
||||
if (!metadata.subscriptionType || !metadata.subscriptionVersion)
|
||||
{
|
||||
// TODO: error handling
|
||||
return;
|
||||
}
|
||||
|
||||
auto it =
|
||||
notificationHandlers.find({*metadata.subscriptionType,
|
||||
*metadata.subscriptionVersion});
|
||||
if (it == notificationHandlers.end())
|
||||
{
|
||||
// TODO: error handling
|
||||
return;
|
||||
}
|
||||
|
||||
it->second(metadata, jv, listener);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
boost::system::error_code handleMessage(std::unique_ptr<Listener> &listener,
|
||||
const beast::flat_buffer &buffer)
|
||||
{
|
||||
boost::system::error_code parseError;
|
||||
auto jv =
|
||||
boost::json::parse(beast::buffers_to_string(buffer.data()), parseError);
|
||||
if (parseError)
|
||||
{
|
||||
// TODO: wrap error?
|
||||
return parseError;
|
||||
}
|
||||
|
||||
const auto *jvObject = jv.if_object();
|
||||
if (jvObject == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory errorRootMustBeObject{
|
||||
"Payload root must be an object"};
|
||||
return boost::system::error_code{129, errorRootMustBeObject};
|
||||
}
|
||||
|
||||
const auto *metadataV = jvObject->if_contains("metadata");
|
||||
if (metadataV == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
errorRootMustContainMetadata{
|
||||
"Payload root must contain a metadata field"};
|
||||
return boost::system::error_code{129, errorRootMustContainMetadata};
|
||||
}
|
||||
auto metadataResult =
|
||||
boost::json::try_value_to<messages::Metadata>(*metadataV);
|
||||
if (metadataResult.has_error())
|
||||
{
|
||||
// TODO: wrap error?
|
||||
return metadataResult.error();
|
||||
}
|
||||
|
||||
const auto &metadata = metadataResult.value();
|
||||
|
||||
auto handler = MESSAGE_HANDLERS.find(metadata.messageType);
|
||||
|
||||
if (handler == MESSAGE_HANDLERS.end())
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "No message handler found for message type: ";
|
||||
ss << metadata.messageType;
|
||||
error::ApplicationErrorCategory errorNoMessageHandlerForMessageType{
|
||||
ss.str()};
|
||||
return boost::system::error_code{129,
|
||||
errorNoMessageHandlerForMessageType};
|
||||
}
|
||||
|
||||
const auto *payloadV = jvObject->if_contains("payload");
|
||||
if (payloadV == nullptr)
|
||||
{
|
||||
static const error::ApplicationErrorCategory
|
||||
errorRootMustContainPayload{
|
||||
"Payload root must contain a payload field"};
|
||||
return boost::system::error_code{129, errorRootMustContainPayload};
|
||||
}
|
||||
|
||||
handler->second(metadata, *payloadV, listener, NOTIFICATION_HANDLERS);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolver and socket require an io_context
|
||||
Session::Session(boost::asio::io_context &ioc, boost::asio::ssl::context &ctx,
|
||||
std::unique_ptr<Listener> listener)
|
||||
: resolver(boost::asio::make_strand(ioc))
|
||||
, ws(boost::asio::make_strand(ioc), ctx)
|
||||
, listener(std::move(listener))
|
||||
{
|
||||
}
|
||||
|
||||
// Start the asynchronous operation
|
||||
void Session::run(std::string _host, std::string _port, std::string _path,
|
||||
std::string _userAgent)
|
||||
{
|
||||
// Save these for later
|
||||
this->host = std::move(_host);
|
||||
this->port = std::move(_port);
|
||||
this->path = std::move(_path);
|
||||
this->userAgent = std::move(_userAgent);
|
||||
|
||||
// Look up the domain name
|
||||
this->resolver.async_resolve(
|
||||
this->host, this->port,
|
||||
beast::bind_front_handler(&Session::onResolve, shared_from_this()));
|
||||
}
|
||||
|
||||
void Session::close()
|
||||
{
|
||||
boost::beast::websocket::close_reason closeReason("Shutting down");
|
||||
|
||||
// TODO: Test this with a misbehaving eventsub server that doesn't respond to our close
|
||||
this->ws.async_close(
|
||||
closeReason,
|
||||
beast::bind_front_handler(&Session::onClose, shared_from_this()));
|
||||
}
|
||||
|
||||
Listener *Session::getListener()
|
||||
{
|
||||
return this->listener.get();
|
||||
}
|
||||
|
||||
void Session::onResolve(beast::error_code ec,
|
||||
boost::asio::ip::tcp::resolver::results_type results)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
return fail(ec, "resolve");
|
||||
}
|
||||
|
||||
// Set a timeout on the operation
|
||||
beast::get_lowest_layer(this->ws).expires_after(std::chrono::seconds(30));
|
||||
|
||||
// Make the connection on the IP address we get from a lookup
|
||||
beast::get_lowest_layer(this->ws).async_connect(
|
||||
results,
|
||||
beast::bind_front_handler(&Session::onConnect, shared_from_this()));
|
||||
}
|
||||
|
||||
void Session::onConnect(
|
||||
beast::error_code ec,
|
||||
boost::asio::ip::tcp::resolver::results_type::endpoint_type ep)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
return fail(ec, "connect");
|
||||
}
|
||||
|
||||
// Set a timeout on the operation
|
||||
beast::get_lowest_layer(this->ws).expires_after(std::chrono::seconds(30));
|
||||
|
||||
// Set SNI Hostname (many hosts need this to handshake successfully)
|
||||
if (!SSL_set_tlsext_host_name(this->ws.next_layer().native_handle(),
|
||||
this->host.c_str()))
|
||||
{
|
||||
ec = beast::error_code(static_cast<int>(::ERR_get_error()),
|
||||
boost::asio::error::get_ssl_category());
|
||||
return fail(ec, "connect");
|
||||
}
|
||||
|
||||
// Update the host_ string. This will provide the value of the
|
||||
// Host HTTP header during the WebSocket handshake.
|
||||
// See https://tools.ietf.org/html/rfc7230#section-5.4
|
||||
host += ':' + std::to_string(ep.port());
|
||||
|
||||
// Perform the SSL handshake
|
||||
this->ws.next_layer().async_handshake(
|
||||
boost::asio::ssl::stream_base::client,
|
||||
beast::bind_front_handler(&Session::onSSLHandshake,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void Session::onSSLHandshake(beast::error_code ec)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
return fail(ec, "ssl_handshake");
|
||||
}
|
||||
|
||||
// Turn off the timeout on the tcp_stream, because
|
||||
// the websocket stream has its own timeout system.
|
||||
beast::get_lowest_layer(this->ws).expires_never();
|
||||
|
||||
// Set suggested timeout settings for the websocket
|
||||
this->ws.set_option(
|
||||
websocket::stream_base::timeout::suggested(beast::role_type::client));
|
||||
|
||||
// Set a decorator to change the User-Agent of the handshake
|
||||
this->ws.set_option(websocket::stream_base::decorator(
|
||||
[userAgent{this->userAgent}](websocket::request_type &req) {
|
||||
req.set(http::field::user_agent, userAgent);
|
||||
}));
|
||||
|
||||
// Perform the websocket handshake
|
||||
this->ws.async_handshake(
|
||||
this->host, this->path,
|
||||
beast::bind_front_handler(&Session::onHandshake, shared_from_this()));
|
||||
}
|
||||
|
||||
void Session::onHandshake(beast::error_code ec)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
return fail(ec, "handshake");
|
||||
}
|
||||
|
||||
this->ws.async_read(buffer, beast::bind_front_handler(&Session::onRead,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void Session::onRead(beast::error_code ec, std::size_t bytes_transferred)
|
||||
{
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
|
||||
if (ec)
|
||||
{
|
||||
return fail(ec, "read");
|
||||
}
|
||||
|
||||
auto messageError = handleMessage(this->listener, this->buffer);
|
||||
if (messageError)
|
||||
{
|
||||
return fail(messageError, "handleMessage");
|
||||
}
|
||||
|
||||
this->buffer.clear();
|
||||
|
||||
this->ws.async_read(buffer, beast::bind_front_handler(&Session::onRead,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
/**
|
||||
this->ws_.async_close(
|
||||
websocket::close_code::normal,
|
||||
beast::bind_front_handler(&Session::onClose, shared_from_this()));
|
||||
*/
|
||||
void Session::onClose(beast::error_code ec)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
return fail(ec, "close");
|
||||
}
|
||||
|
||||
// If we get here then the connection is closed gracefully
|
||||
|
||||
// The make_printable() function helps print a ConstBufferSequence
|
||||
std::cout << beast::make_printable(buffer.data()) << std::endl;
|
||||
}
|
||||
|
||||
} // namespace chatterino::eventsub::lib
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "twitch-eventsub-ws/string.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <QString>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace chatterino::eventsub::lib {
|
||||
|
||||
boost::json::result_for<String, boost::json::value>::type tag_invoke(
|
||||
boost::json::try_value_to_tag<String> /*tag*/,
|
||||
const boost::json::value &jvRoot)
|
||||
{
|
||||
auto v = boost::json::try_value_to<std::string>(jvRoot);
|
||||
if (v.has_error())
|
||||
{
|
||||
return v.error();
|
||||
}
|
||||
|
||||
return String(std::move(v.value()));
|
||||
}
|
||||
|
||||
} // namespace chatterino::eventsub::lib
|
||||
Reference in New Issue
Block a user