feat: add initial experimental Twitch Eventsub support (#5837)
Co-authored-by: nerix <nerixdev@outlook.de>
This commit is contained in:
@@ -44,6 +44,11 @@ CheckOptions:
|
|||||||
- key: readability-identifier-naming.FunctionIgnoredRegexp
|
- key: readability-identifier-naming.FunctionIgnoredRegexp
|
||||||
value: ^(TEST|MOCK_METHOD)$
|
value: ^(TEST|MOCK_METHOD)$
|
||||||
|
|
||||||
|
- key: readability-identifier-naming.GlobalFunctionCase
|
||||||
|
value: camelBack
|
||||||
|
- key: readability-identifier-naming.GlobalFunctionIgnoredRegexp
|
||||||
|
value: ^(tag_invoke)$
|
||||||
|
|
||||||
- key: readability-identifier-naming.MemberCase
|
- key: readability-identifier-naming.MemberCase
|
||||||
value: camelBack
|
value: camelBack
|
||||||
- key: readability-identifier-naming.PrivateMemberIgnoredRegexp
|
- key: readability-identifier-naming.PrivateMemberIgnoredRegexp
|
||||||
|
|||||||
@@ -49,3 +49,6 @@
|
|||||||
[submodule "lib/sol2"]
|
[submodule "lib/sol2"]
|
||||||
path = lib/sol2
|
path = lib/sol2
|
||||||
url = https://github.com/ThePhD/sol2.git
|
url = https://github.com/ThePhD/sol2.git
|
||||||
|
[submodule "lib/certify"]
|
||||||
|
path = lib/certify
|
||||||
|
url = https://github.com/Chatterino/certify
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
- Bugfix: Fixed announcements not showing up in mentions tab. (#5857)
|
- Bugfix: Fixed announcements not showing up in mentions tab. (#5857)
|
||||||
- Bugfix: Fixed the reply button showing for inline whispers and announcements. (#5863)
|
- Bugfix: Fixed the reply button showing for inline whispers and announcements. (#5863)
|
||||||
- Bugfix: Fixed suspicious user treatment update messages not being searchable. (#5865)
|
- Bugfix: Fixed suspicious user treatment update messages not being searchable. (#5865)
|
||||||
|
- Dev: Add initial experimental EventSub support. (#5837)
|
||||||
- Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784)
|
- Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784)
|
||||||
- Dev: Updated Conan dependencies. (#5776)
|
- Dev: Updated Conan dependencies. (#5776)
|
||||||
- Dev: Disable QT keywords (i.e. `emit`, `slots`, and `signals`). (#5882)
|
- Dev: Disable QT keywords (i.e. `emit`, `slots`, and `signals`). (#5882)
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ find_package(PajladaSignals REQUIRED)
|
|||||||
find_package(LRUCache REQUIRED)
|
find_package(LRUCache REQUIRED)
|
||||||
find_package(MagicEnum REQUIRED)
|
find_package(MagicEnum REQUIRED)
|
||||||
find_package(Doxygen)
|
find_package(Doxygen)
|
||||||
|
find_package(BoostCertify REQUIRED)
|
||||||
|
|
||||||
if (USE_SYSTEM_PAJLADA_SETTINGS)
|
if (USE_SYSTEM_PAJLADA_SETTINGS)
|
||||||
find_package(PajladaSettings REQUIRED)
|
find_package(PajladaSettings REQUIRED)
|
||||||
@@ -229,6 +230,8 @@ if (BUILD_WITH_CRASHPAD)
|
|||||||
add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler")
|
add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
add_subdirectory(lib/twitch-eventsub-ws EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of
|
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of
|
||||||
# compilation in CMake is a more involved, as documented in https://stackoverflow.com/q/24292898.
|
# compilation in CMake is a more involved, as documented in https://stackoverflow.com/q/24292898.
|
||||||
# For CI runs, however, the date of build file generation should be consistent with the date of
|
# For CI runs, however, the date of build file generation should be consistent with the date of
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
|
||||||
|
find_path(BoostCertify_INCLUDE_DIR boost/certify/https_verification.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/certify/include)
|
||||||
|
|
||||||
|
find_package_handle_standard_args(BoostCertify DEFAULT_MSG BoostCertify_INCLUDE_DIR)
|
||||||
|
|
||||||
|
if (BoostCertify_FOUND)
|
||||||
|
add_library(BoostCertify INTERFACE IMPORTED)
|
||||||
|
set_target_properties(BoostCertify PROPERTIES
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${BoostCertify_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
mark_as_advanced(BoostCertify_INCLUDE_DIR)
|
||||||
Submodule
+1
Submodule lib/certify added at a448a3915d
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[settings]
|
||||||
|
multi_line_output=3
|
||||||
|
include_trailing_comma=True
|
||||||
|
force_grid_wrap=0
|
||||||
|
known_first_party=lib
|
||||||
|
known_typing=typing
|
||||||
|
use_parentheses=True
|
||||||
|
line_length=120
|
||||||
|
sections=FUTURE,TYPING,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.pytest_cache/
|
||||||
|
venv/
|
||||||
|
build/
|
||||||
|
|
||||||
|
compile_commands.json
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
cmake_policy(SET CMP0087 NEW) # evaluates generator expressions in `install(CODE/SCRIPT)`
|
||||||
|
cmake_policy(SET CMP0091 NEW) # select MSVC runtime library through `CMAKE_MSVC_RUNTIME_LIBRARY`
|
||||||
|
include(FeatureSummary)
|
||||||
|
|
||||||
|
list(APPEND CMAKE_MODULE_PATH
|
||||||
|
"${CMAKE_SOURCE_DIR}/cmake"
|
||||||
|
)
|
||||||
|
|
||||||
|
project(twitch-eventsub-ws VERSION 0.1.0)
|
||||||
|
|
||||||
|
# Find boost on the system
|
||||||
|
find_package(Boost REQUIRED OPTIONAL_COMPONENTS json)
|
||||||
|
|
||||||
|
# Find OpenSSL on the system
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
|
option(BUILD_WITH_QT6 "Build with Qt6" On)
|
||||||
|
if (BUILD_WITH_QT6)
|
||||||
|
set(MAJOR_QT_VERSION "6")
|
||||||
|
else()
|
||||||
|
set(MAJOR_QT_VERSION "5")
|
||||||
|
endif()
|
||||||
|
find_package(Qt${MAJOR_QT_VERSION} REQUIRED COMPONENTS Core)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_subdirectory(src)
|
||||||
|
|
||||||
|
include(cmake/GenerateJson.cmake)
|
||||||
|
|
||||||
|
generate_json_impls(SRC_TARGET twitch-eventsub-ws GEN_TARGET twitch-eventsub-ws-gen)
|
||||||
|
|
||||||
|
feature_summary(WHAT ALL)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# twitch-eventsub-ws
|
||||||
|
|
||||||
|
This library implements some of [Twitch's EventSub](https://dev.twitch.tv/docs/eventsub/) topics for use in [Chatterino 2](https://github.com/Chatterino/chatterino2)
|
||||||
|
|
||||||
|
Topic generation is done in the `ast` directory
|
||||||
|
|
||||||
|
The example can be built like this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
cmake ../example
|
||||||
|
cmake --build .
|
||||||
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## TODO
|
||||||
|
|
||||||
|
- Default of `json_transform` should be `snake_case` since it will be used most
|
||||||
|
- The websocket connections need to die when user is changed
|
||||||
|
- Figure out if the websocket connections need to die when the user refreshed their token
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/venv
|
||||||
|
/__pycache__
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
## Available scripts
|
||||||
|
|
||||||
|
- `generate-and-replace-dir.py`
|
||||||
|
Usage: `./generate-and-replace-dir.py <path-to-dir-containing-header-files>`
|
||||||
|
Will find all header files in that directory (it won't search recursively), and if they also have a matching source file, generate json deserialize definition & implementations for them, and replacing the markers in the given file.
|
||||||
|
|
||||||
|
- `get-builtin-include-dirs.py`
|
||||||
|
Usage: `./get-builtin-include-dirs.py`
|
||||||
|
Prints what builtin include dirs will be used for any of the other scripts.
|
||||||
|
|
||||||
|
- `generate.py`
|
||||||
|
Usage: `./generate.py <path-to-header-file>`
|
||||||
|
Generates definitions & implementations for the given header file and write them to temporary files.
|
||||||
|
|
||||||
|
- `replace-in-file.py`
|
||||||
|
Usage: `./replace-in-file.py <path-to-header-file> <path-to-source-file>`
|
||||||
|
Reads sdin for two file paths containing the definition & implementations.
|
||||||
|
Can be used in tandem with `generate.py`, but realistically it's only there because I used it at some point.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
The following environment variables can be configured to change the behaviours of this project
|
||||||
|
|
||||||
|
- `LLVM_PATH`
|
||||||
|
Will be used to change where to find the `clang++` executable file.
|
||||||
|
Example: `LLVM_PATH=/opt/llvm` ./get-builtin-include-dirs.py will use `/opt/llvm/bin/clang++`
|
||||||
|
|
||||||
|
- `LIBCLANG_LIBRARY_FILE`
|
||||||
|
Will be used to change where clang cindex can find the dynamic library.
|
||||||
|
Must be an absolute path to the file.
|
||||||
|
Example: `LIBCLANG_LIBRARY_FILE=/opt/llvm/lib/libclang.so.15.0.7`
|
||||||
|
|
||||||
|
- `LIBCLANG_LIBRARY_PATH`
|
||||||
|
Will be used to change where clang cindex can look for the dynamic library.
|
||||||
|
Must be an absolute path to the directory.
|
||||||
|
Example: `LIBCLANG_LIBRARY_PATH=/opt/llvm/lib`
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from os.path import realpath
|
||||||
|
|
||||||
|
from lib import (
|
||||||
|
definition_markers,
|
||||||
|
generate,
|
||||||
|
implementation_markers,
|
||||||
|
init_clang_cindex,
|
||||||
|
init_logging,
|
||||||
|
replace_in_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger("generate-and-replace-dir")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
init_logging()
|
||||||
|
|
||||||
|
init_clang_cindex()
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
log.error(f"usage: {sys.argv[0]} <path-to-dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
additional_includes = []
|
||||||
|
if len(sys.argv) >= 4 and sys.argv[2] == "--includes":
|
||||||
|
additional_includes = sys.argv[3].split(";")
|
||||||
|
log.debug(f"additional includes: {additional_includes}")
|
||||||
|
|
||||||
|
dir = realpath(sys.argv[1])
|
||||||
|
log.debug(f"Searching for header files in {dir}")
|
||||||
|
|
||||||
|
for path in os.scandir(dir):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not path.name.endswith(".hpp"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
header_path = path.path
|
||||||
|
source_path = re.sub(r"\.hpp$", ".cpp", header_path)
|
||||||
|
source_path = re.sub(r"include[/\\]twitch-eventsub-ws[/\\]", "src/", source_path)
|
||||||
|
|
||||||
|
if not os.path.isfile(source_path):
|
||||||
|
log.warning(f"Header file {header_path} did not have a matching source file at {source_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.debug(f"Found header & source {header_path} / {source_path}")
|
||||||
|
|
||||||
|
(definition, implementation) = generate(header_path, additional_includes)
|
||||||
|
|
||||||
|
replace_in_file(header_path, definition_markers, definition)
|
||||||
|
replace_in_file(source_path, implementation_markers, implementation)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from lib import generate, init_clang_cindex, init_logging, temporary_file
|
||||||
|
|
||||||
|
log = logging.getLogger("generate")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
init_logging()
|
||||||
|
|
||||||
|
init_clang_cindex()
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
log.error(f"Missing header file argument. Usage: {sys.argv[0]} <path-to-header-file>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
(definitions, implementations) = generate(sys.argv[1])
|
||||||
|
|
||||||
|
with temporary_file() as (f, path):
|
||||||
|
log.debug(f"Write definitions to {path}")
|
||||||
|
f.write(definitions)
|
||||||
|
f.write("\n")
|
||||||
|
print(path)
|
||||||
|
|
||||||
|
with temporary_file() as (f, path):
|
||||||
|
log.debug(f"Write implementations to {path}")
|
||||||
|
f.write(implementations)
|
||||||
|
f.write("\n")
|
||||||
|
print(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from lib import get_clang_builtin_include_dirs, init_logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
init_logging()
|
||||||
|
|
||||||
|
quote_includes, angle_includes = get_clang_builtin_include_dirs()
|
||||||
|
|
||||||
|
print("Quote includes:")
|
||||||
|
print("\n".join(quote_includes))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Angle includes:")
|
||||||
|
print("\n".join(angle_includes))
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from .generate import generate
|
||||||
|
from .helpers import get_clang_builtin_include_dirs, init_clang_cindex, temporary_file
|
||||||
|
from .logging import init_logging
|
||||||
|
from .replace import definition_markers, implementation_markers, replace_in_file
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"generate",
|
||||||
|
"get_clang_builtin_include_dirs",
|
||||||
|
"init_clang_cindex",
|
||||||
|
"temporary_file",
|
||||||
|
"init_logging",
|
||||||
|
"definition_markers",
|
||||||
|
"implementation_markers",
|
||||||
|
"replace_in_file",
|
||||||
|
]
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
from .helpers import get_clang_builtin_include_dirs
|
||||||
|
from .struct import Struct
|
||||||
|
from .enum import Enum
|
||||||
|
from .walker import Walker
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def build_structs(filename: str, additional_includes: list[str] = []) -> List[Struct | Enum]:
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
raise ValueError(f"Path {filename} is not a file. cwd: {os.getcwd()}")
|
||||||
|
|
||||||
|
parse_args = [
|
||||||
|
"-std=c++17",
|
||||||
|
# Uncomment this if you need to debug where it tries to find headers
|
||||||
|
# "-H",
|
||||||
|
]
|
||||||
|
|
||||||
|
parse_options = (
|
||||||
|
clang.cindex.TranslationUnit.PARSE_INCOMPLETE | clang.cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
|
||||||
|
)
|
||||||
|
|
||||||
|
extra_includes, system_includes = get_clang_builtin_include_dirs()
|
||||||
|
|
||||||
|
for include_dir in system_includes:
|
||||||
|
parse_args.append(f"-isystem{include_dir}")
|
||||||
|
|
||||||
|
for include_dir in extra_includes:
|
||||||
|
parse_args.append(f"-I{include_dir}")
|
||||||
|
|
||||||
|
for dir in additional_includes:
|
||||||
|
parse_args.append("-I")
|
||||||
|
parse_args.append(dir)
|
||||||
|
|
||||||
|
# Append default dirs
|
||||||
|
# - Append dir of file
|
||||||
|
file_dir = os.path.dirname(os.path.realpath(filename))
|
||||||
|
parse_args.append(f"-I{file_dir}")
|
||||||
|
# - Append project include dir
|
||||||
|
file_subdir = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(filename)), "../.."))
|
||||||
|
parse_args.append(f"-I{file_subdir}")
|
||||||
|
|
||||||
|
# TODO: Use build_commands if available
|
||||||
|
|
||||||
|
index = clang.cindex.Index.create()
|
||||||
|
|
||||||
|
log.debug("Parsing translation unit")
|
||||||
|
tu = index.parse(filename, args=parse_args, options=parse_options)
|
||||||
|
root = tu.cursor
|
||||||
|
|
||||||
|
for diag in tu.diagnostics:
|
||||||
|
log.warning(diag.location)
|
||||||
|
log.warning(diag.spelling)
|
||||||
|
log.warning(diag.option)
|
||||||
|
|
||||||
|
log.debug("Walking over AST")
|
||||||
|
walker = Walker(filename)
|
||||||
|
walker.walk(root)
|
||||||
|
|
||||||
|
return walker.enums + walker.structs
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
CommentCommands = List[Tuple[str, str]]
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_comment_commands(raw_comment: str) -> CommentCommands:
|
||||||
|
comment_commands: CommentCommands = []
|
||||||
|
|
||||||
|
def clean_comment_line(line: str) -> str:
|
||||||
|
return line.replace("/", "").replace("*", "").strip()
|
||||||
|
|
||||||
|
comment_lines = [line for line in map(clean_comment_line, raw_comment.splitlines()) if line != ""]
|
||||||
|
|
||||||
|
for comment in comment_lines:
|
||||||
|
parts = comment.split("=", 2)
|
||||||
|
if len(parts) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
command = parts[0].strip()
|
||||||
|
value = parts[1].strip()
|
||||||
|
comment_commands.append((command, value))
|
||||||
|
|
||||||
|
return comment_commands
|
||||||
|
|
||||||
|
|
||||||
|
def json_transform(input_str: str, transformation: str) -> str:
|
||||||
|
match transformation:
|
||||||
|
case "snake_case":
|
||||||
|
return re.sub(r"(?<![A-Z])\B[A-Z]", r"_\g<0>", input_str).lower()
|
||||||
|
case other:
|
||||||
|
log.warning(f"Unknown transformation '{other}', ignoring")
|
||||||
|
return input_str
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from jinja2 import Environment
|
||||||
|
|
||||||
|
from .comment_commands import CommentCommands
|
||||||
|
from .enum_constant import EnumConstant
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Enum:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.constants: List[EnumConstant] = []
|
||||||
|
self.parent: str = ""
|
||||||
|
self.comment_commands: CommentCommands = []
|
||||||
|
self.inner_root: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
if self.parent:
|
||||||
|
return f"{self.parent}::{self.name}"
|
||||||
|
else:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if isinstance(other, self.__class__):
|
||||||
|
if self.name != other.name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.constants == other.constants
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
pretty_constants = "\n ".join(map(str, self.constants))
|
||||||
|
return f"enum class {self.name} {{\n {pretty_constants}\n}}"
|
||||||
|
|
||||||
|
def try_value_to_implementation(self, env: Environment) -> str:
|
||||||
|
return env.get_template("enum-implementation.tmpl").render(enum=self)
|
||||||
|
|
||||||
|
def try_value_to_definition(self, env: Environment) -> str:
|
||||||
|
return env.get_template("enum-definition.tmpl").render(enum=self)
|
||||||
|
|
||||||
|
def apply_comment_commands(self, comment_commands: CommentCommands) -> None:
|
||||||
|
for command, value in comment_commands:
|
||||||
|
match command:
|
||||||
|
case "json_rename":
|
||||||
|
# Do nothing on enums
|
||||||
|
pass
|
||||||
|
case "json_dont_fail_on_deserialization":
|
||||||
|
# Do nothing on enums
|
||||||
|
pass
|
||||||
|
case "json_transform":
|
||||||
|
# Do nothing on enums
|
||||||
|
pass
|
||||||
|
case "json_inner":
|
||||||
|
self.inner_root = value
|
||||||
|
case other:
|
||||||
|
log.warning(f"Unknown comment command found: {other} with value {value}")
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
from .comment_commands import CommentCommands, json_transform, parse_comment_commands
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EnumConstant:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.json_name = name
|
||||||
|
self.tag: Optional[str] = None
|
||||||
|
|
||||||
|
self.dont_fail_on_deserialization: bool = False
|
||||||
|
|
||||||
|
def apply_comment_commands(self, comment_commands: CommentCommands) -> None:
|
||||||
|
for command, value in comment_commands:
|
||||||
|
match command:
|
||||||
|
case "json_rename":
|
||||||
|
# Rename the key that this field will use in json terms
|
||||||
|
log.debug(f"Rename json key from {self.json_name} to {value}")
|
||||||
|
self.json_name = value
|
||||||
|
case "json_dont_fail_on_deserialization":
|
||||||
|
# Don't fail when an optional object exists and its data is bad
|
||||||
|
log.debug(f"Don't fail on deserialization for {self.name}")
|
||||||
|
self.dont_fail_on_deserialization = bool(value.lower() == "true")
|
||||||
|
case "json_transform":
|
||||||
|
# Transform the key from whatever-case to case specified by `value`
|
||||||
|
self.json_name = json_transform(self.json_name, value)
|
||||||
|
case "json_inner":
|
||||||
|
# Do nothing on members
|
||||||
|
pass
|
||||||
|
case "json_tag":
|
||||||
|
# Rename the key that this field will use in json terms
|
||||||
|
log.debug(f"Applied json tag on {self.json_name}: {value}")
|
||||||
|
self.tag = value
|
||||||
|
case other:
|
||||||
|
log.warning(f"Unknown comment command found: {other} with value {value}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_node(node: clang.cindex.Cursor) -> EnumConstant:
|
||||||
|
assert node.type is not None
|
||||||
|
|
||||||
|
name = node.spelling
|
||||||
|
|
||||||
|
enum = EnumConstant(name)
|
||||||
|
|
||||||
|
if node.raw_comment is not None:
|
||||||
|
comment_commands = parse_comment_commands(node.raw_comment)
|
||||||
|
enum.apply_comment_commands(comment_commands)
|
||||||
|
|
||||||
|
return enum
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.name != other.name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.name}"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def format_code(code: str) -> str:
|
||||||
|
proc = subprocess.Popen(["clang-format", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
outs, errs = proc.communicate(input=code.encode(), timeout=2)
|
||||||
|
if errs is not None:
|
||||||
|
log.warning(f"Error formatting code: {errs.decode()}")
|
||||||
|
|
||||||
|
return outs.decode()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .build import build_structs
|
||||||
|
from .format import format_code
|
||||||
|
from .helpers import init_clang_cindex, temporary_file
|
||||||
|
from .jinja_env import env
|
||||||
|
from .logging import init_logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def generate(header_path: str, additional_includes: list[str] = []) -> Tuple[str, str]:
|
||||||
|
structs = build_structs(header_path, additional_includes)
|
||||||
|
|
||||||
|
log.debug("Generate & format definitions")
|
||||||
|
definitions = format_code("\n\n".join([struct.try_value_to_definition(env) for struct in structs]))
|
||||||
|
log.debug("Generate & format implementations")
|
||||||
|
implementations = format_code("\n\n".join([struct.try_value_to_implementation(env) for struct in structs]))
|
||||||
|
|
||||||
|
return (definitions, implementations)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
from typing import Generator, List, Tuple
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from io import TextIOWrapper
|
||||||
|
from tempfile import mkstemp
|
||||||
|
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def init_clang_cindex() -> None:
|
||||||
|
# 1. Has the LIBCLANG_LIBRARY_FILE environment variable been set? Use it
|
||||||
|
env_library_file = os.environ.get("LIBCLANG_LIBRARY_FILE")
|
||||||
|
if env_library_file is not None:
|
||||||
|
log.debug(f"Setting clang library file from LIBCLANG_LIBRARY_PATH environment variable: {env_library_file}")
|
||||||
|
clang.cindex.Config.set_library_file(env_library_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Has the LIBCLANG_LIBRARY_PATH environment variable been set? Use it
|
||||||
|
env_library_path = os.environ.get("LIBCLANG_LIBRARY_PATH")
|
||||||
|
if env_library_path is not None:
|
||||||
|
log.debug(
|
||||||
|
f"Setting clang library search path from LIBCLANG_LIBRARY_PATH environment variable: {env_library_path}"
|
||||||
|
)
|
||||||
|
clang.cindex.Config.set_library_path(env_library_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
import ctypes.util
|
||||||
|
|
||||||
|
autodetected_path = ctypes.util.find_library("libclang" if os.name == "nt" else "clang")
|
||||||
|
if autodetected_path is not None:
|
||||||
|
log.debug(f"Autodetected clang library file: {autodetected_path}")
|
||||||
|
clang.cindex.Config.set_library_file(autodetected_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def temporary_file() -> Generator[Tuple[TextIOWrapper, str], None, None]:
|
||||||
|
fd, path = mkstemp()
|
||||||
|
log.debug(f"Opening file {path}")
|
||||||
|
f = os.fdopen(fd, "w")
|
||||||
|
|
||||||
|
yield (f, path)
|
||||||
|
|
||||||
|
f.flush()
|
||||||
|
f.close()
|
||||||
|
log.debug(f"Closed file {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_clang_builtin_include_dirs() -> Tuple[List[str], List[str]]:
|
||||||
|
quote_includes: List[str] = []
|
||||||
|
angle_includes: List[str] = []
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"clang++",
|
||||||
|
"-E",
|
||||||
|
"-x",
|
||||||
|
"c++",
|
||||||
|
"-v",
|
||||||
|
"-",
|
||||||
|
]
|
||||||
|
|
||||||
|
path_entries = os.environ.get("PATH", "").split(os.pathsep)
|
||||||
|
|
||||||
|
llvm_path = os.environ.get("LLVM_PATH")
|
||||||
|
if llvm_path is not None:
|
||||||
|
path_entries.insert(0, os.path.join(llvm_path, "bin"))
|
||||||
|
|
||||||
|
path_str = os.pathsep.join(path_entries)
|
||||||
|
|
||||||
|
full_env = os.environ
|
||||||
|
full_env["PATH"] = path_str
|
||||||
|
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
env=full_env,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, errs = proc.communicate(input=None, timeout=10)
|
||||||
|
|
||||||
|
doing_quote_includes = False
|
||||||
|
doing_angle_includes = False
|
||||||
|
|
||||||
|
for line in errs.decode("utf8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line == r'#include "..." search starts here:':
|
||||||
|
doing_angle_includes = False
|
||||||
|
doing_quote_includes = True
|
||||||
|
continue
|
||||||
|
if line == r"#include <...> search starts here:":
|
||||||
|
doing_quote_includes = False
|
||||||
|
doing_angle_includes = True
|
||||||
|
continue
|
||||||
|
if line == r"End of search list.":
|
||||||
|
doing_quote_includes = False
|
||||||
|
doing_angle_includes = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if doing_quote_includes or doing_angle_includes:
|
||||||
|
if " " in line:
|
||||||
|
continue
|
||||||
|
line = line.split(" ", 1)[0]
|
||||||
|
p = os.path.realpath(line)
|
||||||
|
if doing_quote_includes:
|
||||||
|
quote_includes.append(p)
|
||||||
|
if doing_angle_includes:
|
||||||
|
angle_includes.append(p)
|
||||||
|
|
||||||
|
return (quote_includes, angle_includes)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from jinja2_workarounds import MultiLineInclude
|
||||||
|
|
||||||
|
from .membertype import MemberType
|
||||||
|
|
||||||
|
template_paths = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
|
||||||
|
|
||||||
|
# print(f"Loading jinja templates from {template_paths}")
|
||||||
|
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(template_paths),
|
||||||
|
extensions=[MultiLineInclude],
|
||||||
|
)
|
||||||
|
env.globals["MemberType"] = MemberType
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from colorama import Fore, Style
|
||||||
|
|
||||||
|
|
||||||
|
def init_logging() -> None:
|
||||||
|
root = logging.getLogger()
|
||||||
|
root.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
handler = logging.StreamHandler(sys.stderr)
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
colors = {
|
||||||
|
"WARNING": Fore.YELLOW,
|
||||||
|
"INFO": Fore.WHITE,
|
||||||
|
"DEBUG": Fore.BLUE,
|
||||||
|
"CRITICAL": Fore.YELLOW,
|
||||||
|
"ERROR": Fore.RED,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColoredFormatter(logging.Formatter):
|
||||||
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
levelname = record.levelname
|
||||||
|
if levelname in colors:
|
||||||
|
levelname_color = Style.BRIGHT + colors[levelname] + levelname + Style.RESET_ALL
|
||||||
|
record.levelname = levelname_color
|
||||||
|
return logging.Formatter.format(self, record)
|
||||||
|
|
||||||
|
colored_formatter = ColoredFormatter("%(asctime)s %(levelname)-16s %(name)s: %(message)s")
|
||||||
|
handler.setFormatter(colored_formatter)
|
||||||
|
root.addHandler(handler)
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import clang.cindex
|
||||||
|
from clang.cindex import CursorKind
|
||||||
|
|
||||||
|
from .comment_commands import CommentCommands, json_transform, parse_comment_commands
|
||||||
|
from .membertype import MemberType
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_type_name(type: clang.cindex.Type, namespace: List[str]) -> str:
|
||||||
|
if namespace:
|
||||||
|
namespace_str = f"{'::'.join(namespace)}::"
|
||||||
|
else:
|
||||||
|
namespace_str = ""
|
||||||
|
type_name = type.spelling
|
||||||
|
|
||||||
|
if type.is_const_qualified():
|
||||||
|
type_name = type_name.replace("const", "").strip()
|
||||||
|
|
||||||
|
type_name = type_name.removeprefix(namespace_str)
|
||||||
|
|
||||||
|
return type_name
|
||||||
|
|
||||||
|
|
||||||
|
class Member:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
member_type: MemberType = MemberType.BASIC,
|
||||||
|
type_name: str = "?",
|
||||||
|
) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.json_name = name
|
||||||
|
self.member_type = member_type
|
||||||
|
self.type_name = type_name
|
||||||
|
self.tag: Optional[str] = None
|
||||||
|
|
||||||
|
self.dont_fail_on_deserialization: bool = False
|
||||||
|
|
||||||
|
def apply_comment_commands(self, comment_commands: CommentCommands) -> None:
|
||||||
|
for command, value in comment_commands:
|
||||||
|
match command:
|
||||||
|
case "json_rename":
|
||||||
|
# Rename the key that this field will use in json terms
|
||||||
|
log.debug(f"Rename json key from {self.json_name} to {value}")
|
||||||
|
self.json_name = value
|
||||||
|
case "json_dont_fail_on_deserialization":
|
||||||
|
# Don't fail when an optional object exists and its data is bad
|
||||||
|
log.debug(f"Don't fail on deserialization for {self.name}")
|
||||||
|
self.dont_fail_on_deserialization = bool(value.lower() == "true")
|
||||||
|
case "json_transform":
|
||||||
|
# Transform the key from whatever-case to case specified by `value`
|
||||||
|
self.json_name = json_transform(self.json_name, value)
|
||||||
|
case "json_inner":
|
||||||
|
# Do nothing on members
|
||||||
|
pass
|
||||||
|
case "json_tag":
|
||||||
|
# Rename the key that this field will use in json terms
|
||||||
|
log.debug(f"Applied json tag on {self.json_name}: {value}")
|
||||||
|
self.tag = value
|
||||||
|
case other:
|
||||||
|
log.warning(f"Unknown comment command found: {other} with value {value}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_field(node: clang.cindex.Cursor, namespace: List[str]) -> Member:
|
||||||
|
assert node.type is not None
|
||||||
|
|
||||||
|
name = node.spelling
|
||||||
|
member_type = MemberType.BASIC
|
||||||
|
type_name = get_type_name(node.type, namespace)
|
||||||
|
|
||||||
|
log.debug(f"{node.spelling} - {type_name} - {node.type.is_const_qualified()}")
|
||||||
|
|
||||||
|
ntargs = node.type.get_num_template_arguments()
|
||||||
|
if ntargs > 0:
|
||||||
|
overwrite_member_type: Optional[MemberType] = None
|
||||||
|
|
||||||
|
# log.debug(node.type.get_template_argument_type(0).kind)
|
||||||
|
# log.debug(node.type.get_template_argument_type(0).spelling)
|
||||||
|
# log.debug(node.type.get_template_argument_type(0).get_named_type().spelling)
|
||||||
|
# log.debug(node.type.get_template_argument_type(0).get_class_type().spelling)
|
||||||
|
|
||||||
|
type_name = get_type_name(node.type.get_template_argument_type(0), namespace)
|
||||||
|
|
||||||
|
for xd in node.get_children():
|
||||||
|
match xd.kind:
|
||||||
|
case CursorKind.NAMESPACE_REF:
|
||||||
|
# Ignore namespaces
|
||||||
|
pass
|
||||||
|
|
||||||
|
case CursorKind.TEMPLATE_REF:
|
||||||
|
match xd.spelling:
|
||||||
|
case "optional":
|
||||||
|
match overwrite_member_type:
|
||||||
|
case None:
|
||||||
|
overwrite_member_type = MemberType.OPTIONAL
|
||||||
|
case other:
|
||||||
|
log.warning(f"Optional cannot be added on top of other member type: {other}")
|
||||||
|
|
||||||
|
case "vector":
|
||||||
|
match overwrite_member_type:
|
||||||
|
case None:
|
||||||
|
overwrite_member_type = MemberType.VECTOR
|
||||||
|
case MemberType.OPTIONAL:
|
||||||
|
overwrite_member_type = MemberType.OPTIONAL_VECTOR
|
||||||
|
case other:
|
||||||
|
log.warning(f"Vector cannot be added on top of other member type: {other}")
|
||||||
|
|
||||||
|
case other:
|
||||||
|
log.warning(f"Unhandled template type: {other}")
|
||||||
|
|
||||||
|
case CursorKind.TYPE_REF:
|
||||||
|
type_name = get_type_name(xd.type, namespace)
|
||||||
|
|
||||||
|
case other:
|
||||||
|
log.debug(f"Unhandled child kind type: {other}")
|
||||||
|
|
||||||
|
if overwrite_member_type is not None:
|
||||||
|
member_type = overwrite_member_type
|
||||||
|
|
||||||
|
member = Member(name, member_type, type_name)
|
||||||
|
|
||||||
|
if node.raw_comment is not None:
|
||||||
|
comment_commands = parse_comment_commands(node.raw_comment)
|
||||||
|
member.apply_comment_commands(comment_commands)
|
||||||
|
|
||||||
|
return member
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.name != other.name:
|
||||||
|
return False
|
||||||
|
if self.member_type != other.member_type:
|
||||||
|
return False
|
||||||
|
if self.type_name != other.type_name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
match self.member_type:
|
||||||
|
case MemberType.BASIC:
|
||||||
|
return f"{self.type_name} {self.name}"
|
||||||
|
|
||||||
|
case MemberType.VECTOR:
|
||||||
|
return f"std::vector<{self.type_name}> {self.name}"
|
||||||
|
|
||||||
|
case MemberType.OPTIONAL:
|
||||||
|
return f"std::optional<{self.type_name}> {self.name}"
|
||||||
|
|
||||||
|
case MemberType.OPTIONAL_VECTOR:
|
||||||
|
return f"std::optional<std::vector<{self.type_name}>> {self.name}"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class MemberType(Enum):
|
||||||
|
BASIC = 1
|
||||||
|
VECTOR = 2
|
||||||
|
OPTIONAL = 3
|
||||||
|
OPTIONAL_VECTOR = 4
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
definition_markers: Tuple[str, str] = (
|
||||||
|
"// DESERIALIZATION DEFINITION START",
|
||||||
|
"// DESERIALIZATION DEFINITION END",
|
||||||
|
)
|
||||||
|
implementation_markers: Tuple[str, str] = (
|
||||||
|
"// DESERIALIZATION IMPLEMENTATION START",
|
||||||
|
"// DESERIALIZATION IMPLEMENTATION END",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_in_file(
|
||||||
|
path: str,
|
||||||
|
markers: Tuple[str, str],
|
||||||
|
replacement: str,
|
||||||
|
) -> None:
|
||||||
|
with open(path, "r+") as fh:
|
||||||
|
# Read file into a list of strings
|
||||||
|
lines = [line.rstrip() for line in fh.readlines()]
|
||||||
|
start = lines.index(markers[0])
|
||||||
|
end = lines.index(markers[1], start)
|
||||||
|
log.debug(f"Markers: {start}-{end}")
|
||||||
|
|
||||||
|
lines[start + 1 : end] = [line.rstrip() for line in replacement.splitlines()]
|
||||||
|
|
||||||
|
# Clear file
|
||||||
|
fh.truncate(0)
|
||||||
|
fh.seek(0)
|
||||||
|
|
||||||
|
# Build new file data
|
||||||
|
data = "\n".join(lines)
|
||||||
|
# Add a newline at the end of the file
|
||||||
|
data += "\n"
|
||||||
|
|
||||||
|
# Write data to file
|
||||||
|
fh.write(data)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from jinja2 import Environment
|
||||||
|
|
||||||
|
from .comment_commands import CommentCommands
|
||||||
|
from .member import Member
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Struct:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.members: List[Member] = []
|
||||||
|
self.parent: str = ""
|
||||||
|
self.comment_commands: CommentCommands = []
|
||||||
|
self.inner_root: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
if self.parent:
|
||||||
|
return f"{self.parent}::{self.name}"
|
||||||
|
else:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if isinstance(other, self.__class__):
|
||||||
|
if self.name != other.name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.members == other.members
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
pretty_members = "\n ".join(map(str, self.members))
|
||||||
|
return f"struct {self.name} {{\n {pretty_members}\n}}"
|
||||||
|
|
||||||
|
def try_value_to_implementation(self, env: Environment) -> str:
|
||||||
|
return env.get_template("struct-implementation.tmpl").render(struct=self)
|
||||||
|
|
||||||
|
def try_value_to_definition(self, env: Environment) -> str:
|
||||||
|
return env.get_template("struct-definition.tmpl").render(struct=self)
|
||||||
|
|
||||||
|
def apply_comment_commands(self, comment_commands: CommentCommands) -> None:
|
||||||
|
for command, value in comment_commands:
|
||||||
|
match command:
|
||||||
|
case "json_rename":
|
||||||
|
# Do nothing on structs
|
||||||
|
pass
|
||||||
|
case "json_dont_fail_on_deserialization":
|
||||||
|
# Do nothing on structs
|
||||||
|
pass
|
||||||
|
case "json_transform":
|
||||||
|
# Do nothing on structs
|
||||||
|
pass
|
||||||
|
case "json_inner":
|
||||||
|
self.inner_root = value
|
||||||
|
case other:
|
||||||
|
log.warning(f"Unknown comment command found: {other} with value {value}")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
boost::json::result_for<{{enum.full_name}}, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<{{enum.full_name}}>, const boost::json::value &jvRoot);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
boost::json::result_for<{{enum.full_name}}, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<{{enum.full_name}}>, const boost::json::value &jvRoot)
|
||||||
|
{
|
||||||
|
if (!jvRoot.is_string())
|
||||||
|
{
|
||||||
|
static const error::ApplicationErrorCategory errorMustBeString{"{{enum.full_name}} must be a string"};
|
||||||
|
return boost::system::error_code{129, errorMustBeString};
|
||||||
|
}
|
||||||
|
std::string_view eString(jvRoot.get_string());
|
||||||
|
|
||||||
|
using namespace std::string_view_literals;
|
||||||
|
|
||||||
|
{%- for constant in enum.constants %}
|
||||||
|
if (eString == "{{constant.json_name}}"sv) {
|
||||||
|
return {{enum.full_name}}::{{constant.name}};
|
||||||
|
}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
static const error::ApplicationErrorCategory errorEnumNameDidNotExist{"{{enum.full_name}} did not have a constant that matched this string"};
|
||||||
|
return boost::system::error_code{129, errorEnumNameDidNotExist};
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
return {{field.name}}.error();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
static const error::ApplicationErrorCategory error_missing_field_{{field.name}}{"Missing required key {{field.json_name}}"};
|
||||||
|
return boost::system::error_code{129, error_missing_field_{{field.name}}};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const auto *jv{{field.name}} = root.if_contains("{{field.json_name}}");
|
||||||
|
if (jv{{field.name}} == nullptr)
|
||||||
|
{
|
||||||
|
{% include 'error-missing-field.tmpl' indent content %}
|
||||||
|
}
|
||||||
|
{% if field.tag %}
|
||||||
|
auto {{field.name}} = boost::json::try_value_to<{{field.type_name}}>(*jv{{field.name}}, {{field.tag}}());
|
||||||
|
{% else %}
|
||||||
|
auto {{field.name}} = boost::json::try_value_to<{{field.type_name}}>(*jv{{field.name}});
|
||||||
|
{% endif %}
|
||||||
|
if ({{field.name}}.has_error())
|
||||||
|
{
|
||||||
|
{% include 'error-failed-to-deserialize.tmpl' indent content %}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
assert(false && "OPTIONAL VECTOR FIELD UNIMPLEMENTED");
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
std::optional<{{field.type_name}}> {{field.name}} = std::nullopt;
|
||||||
|
const auto *jv{{field.name}} = root.if_contains("{{field.json_name}}");
|
||||||
|
if (jv{{field.name}} != nullptr && !jv{{field.name}}->is_null())
|
||||||
|
{
|
||||||
|
{% if field.tag %}
|
||||||
|
auto t{{field.name}} = boost::json::try_value_to<{{field.type_name}}>(*jv{{field.name}}, {{field.tag}}());
|
||||||
|
{% else %}
|
||||||
|
auto t{{field.name}} = boost::json::try_value_to<{{field.type_name}}>(*jv{{field.name}});
|
||||||
|
{% endif %}
|
||||||
|
{% if field.dont_fail_on_deserialization %}
|
||||||
|
if (t{{field.name}}.has_error())
|
||||||
|
{
|
||||||
|
// TODO: report error in some way?
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
{{field.name}} = t{{field.name}}.value();
|
||||||
|
}
|
||||||
|
{% else %}
|
||||||
|
if (t{{field.name}}.has_error())
|
||||||
|
{
|
||||||
|
return t{{field.name}}.error();
|
||||||
|
}
|
||||||
|
{{field.name}} = std::move(t{{field.name}}.value());
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{% if field.tag -%}
|
||||||
|
static_assert(false && "JSON tag support is not implemented for vectors");
|
||||||
|
{%- endif %}
|
||||||
|
const auto *jv{{field.name}} = root.if_contains("{{field.json_name}}");
|
||||||
|
if (jv{{field.name}} == nullptr)
|
||||||
|
{
|
||||||
|
{% include 'error-missing-field.tmpl' indent content %}
|
||||||
|
}
|
||||||
|
const auto {{field.name}} = boost::json::try_value_to<std::vector<{{field.type_name}}>>(*jv{{field.name}});
|
||||||
|
if ({{field.name}}.has_error())
|
||||||
|
{
|
||||||
|
{% include 'error-failed-to-deserialize.tmpl' indent content %}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.{{field.name}} = std::move({{field.name}}.value()),
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
assert(false && "OPTIONAL VECTOR INITIALIZER UNIMPLEMENTED");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.{{field.name}} = std::move({{field.name}}),
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.{{field.name}} = {{field.name}}.value(),
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
boost::json::result_for<{{struct.full_name}}, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<{{struct.full_name}}>, const boost::json::value &jvRoot);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
boost::json::result_for<{{struct.full_name}}, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<{{struct.full_name}}>, const boost::json::value &jvRoot)
|
||||||
|
{
|
||||||
|
{% if struct.inner_root %}
|
||||||
|
if (!jvRoot.is_object())
|
||||||
|
{
|
||||||
|
static const error::ApplicationErrorCategory errorMustBeObject{"{{struct.full_name}} must be an object"};
|
||||||
|
return boost::system::error_code{129, errorMustBeObject};
|
||||||
|
}
|
||||||
|
const auto &outerRoot = jvRoot.get_object();
|
||||||
|
|
||||||
|
const auto *jvInnerRoot = outerRoot.if_contains("{{struct.inner_root}}");
|
||||||
|
if (jvInnerRoot == nullptr)
|
||||||
|
{
|
||||||
|
static const error::ApplicationErrorCategory errorMissing{"{{struct.full_name}}'s key {{struct.inner_root}} is missing"};
|
||||||
|
return boost::system::error_code{129, errorMissing};
|
||||||
|
}
|
||||||
|
if (!jvInnerRoot->is_object())
|
||||||
|
{
|
||||||
|
static const error::ApplicationErrorCategory errorMustBeObject{"{{struct.full_name}}'s {{struct.inner_root}} must be an object"};
|
||||||
|
return boost::system::error_code{129, errorMustBeObject};
|
||||||
|
}
|
||||||
|
const auto &root = jvInnerRoot->get_object();
|
||||||
|
{% else %}
|
||||||
|
if (!jvRoot.is_object())
|
||||||
|
{
|
||||||
|
static const error::ApplicationErrorCategory errorMustBeObject{"{{struct.full_name}} must be an object"};
|
||||||
|
return boost::system::error_code{129, errorMustBeObject};
|
||||||
|
}
|
||||||
|
const auto &root = jvRoot.get_object();
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for field in struct.members %}
|
||||||
|
{% if field.member_type == MemberType.BASIC -%}
|
||||||
|
{% include 'field-basic.tmpl' indent content %}
|
||||||
|
{%- elif field.member_type == MemberType.VECTOR -%}
|
||||||
|
{% include 'field-vector.tmpl' indent content %}
|
||||||
|
{%- elif field.member_type == MemberType.OPTIONAL -%}
|
||||||
|
{% include 'field-optional.tmpl' indent content %}
|
||||||
|
{%- elif field.member_type == MemberType.OPTIONAL_VECTOR -%}
|
||||||
|
{% include 'field-optional-vector.tmpl' indent content %}
|
||||||
|
{%- endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
return {{struct.full_name}}{
|
||||||
|
{%- for field in struct.members %}
|
||||||
|
{% if field.member_type == MemberType.BASIC -%}
|
||||||
|
{% include 'initializer-basic.tmpl' indent content %}
|
||||||
|
{%- elif field.member_type == MemberType.VECTOR -%}
|
||||||
|
{% include 'initializer-vector.tmpl' indent content %}
|
||||||
|
{%- elif field.member_type == MemberType.OPTIONAL -%}
|
||||||
|
{% include 'initializer-optional.tmpl' indent content %}
|
||||||
|
{%- elif field.member_type == MemberType.OPTIONAL_VECTOR -%}
|
||||||
|
{% include 'initializer-optional-vector.tmpl' indent content %}
|
||||||
|
{%- endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Pod {
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Const {
|
||||||
|
const int a;
|
||||||
|
const bool b;
|
||||||
|
const char c;
|
||||||
|
const Pod d;
|
||||||
|
const std::vector<bool> e;
|
||||||
|
const std::optional<bool> f;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#include <optional>
|
||||||
|
|
||||||
|
struct Optional {
|
||||||
|
std::optional<bool> a;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
struct Simple {
|
||||||
|
int a;
|
||||||
|
bool b;
|
||||||
|
char c;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct String {
|
||||||
|
std::string a;
|
||||||
|
const std::string b;
|
||||||
|
std::vector<std::string> c;
|
||||||
|
std::optional<std::string> d;
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Foo {
|
||||||
|
struct InnerFoo {
|
||||||
|
int asd;
|
||||||
|
};
|
||||||
|
|
||||||
|
int test;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct S {
|
||||||
|
Foo a;
|
||||||
|
std::vector<Foo> as;
|
||||||
|
/**
|
||||||
|
* 4
|
||||||
|
* 5
|
||||||
|
* json_rename=vec_of_ints
|
||||||
|
**/
|
||||||
|
std::vector<int> vecOfInts;
|
||||||
|
std::optional<Foo> ab;
|
||||||
|
/**
|
||||||
|
* json_dont_fail_on_deserialization=True
|
||||||
|
**/
|
||||||
|
std::optional<Foo> ac;
|
||||||
|
int b;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Pod {
|
||||||
|
int a;
|
||||||
|
bool b;
|
||||||
|
char c;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VectorPod {
|
||||||
|
std::vector<Pod> a;
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Vector {
|
||||||
|
std::vector<bool> a;
|
||||||
|
std::vector<std::vector<bool>> b;
|
||||||
|
};
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
from lib.build import build_structs
|
||||||
|
from lib.helpers import init_clang_cindex
|
||||||
|
from lib.membertype import MemberType
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def init_clang():
|
||||||
|
init_clang_cindex()
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple():
|
||||||
|
structs = build_structs("lib/tests/resources/simple.hpp")
|
||||||
|
|
||||||
|
assert len(structs) == 1
|
||||||
|
|
||||||
|
s = structs[0]
|
||||||
|
|
||||||
|
assert s.name == "Simple"
|
||||||
|
assert len(s.members) == 3
|
||||||
|
|
||||||
|
assert s.members[0].name == "a"
|
||||||
|
assert s.members[0].member_type == MemberType.BASIC
|
||||||
|
assert s.members[0].type_name == "int"
|
||||||
|
|
||||||
|
assert s.members[1].name == "b"
|
||||||
|
assert s.members[1].member_type == MemberType.BASIC
|
||||||
|
assert s.members[1].type_name == "bool"
|
||||||
|
|
||||||
|
assert s.members[2].name == "c"
|
||||||
|
assert s.members[2].member_type == MemberType.BASIC
|
||||||
|
assert s.members[2].type_name == "char"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vector():
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
print(clang.cindex.conf.get_filename())
|
||||||
|
structs = build_structs("lib/tests/resources/vector.hpp")
|
||||||
|
assert len(structs) == 1
|
||||||
|
s = structs[0]
|
||||||
|
|
||||||
|
assert s.name == "Vector"
|
||||||
|
assert len(s.members) == 2
|
||||||
|
|
||||||
|
assert s.members[0].name == "a"
|
||||||
|
assert s.members[0].member_type == MemberType.VECTOR
|
||||||
|
assert s.members[0].type_name == "bool"
|
||||||
|
|
||||||
|
assert s.members[1].name == "b"
|
||||||
|
assert s.members[1].member_type == MemberType.VECTOR
|
||||||
|
assert s.members[1].type_name == "std::vector<bool>"
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional():
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
print(clang.cindex.conf.get_filename())
|
||||||
|
structs = build_structs("lib/tests/resources/optional.hpp")
|
||||||
|
assert len(structs) == 1
|
||||||
|
s = structs[0]
|
||||||
|
|
||||||
|
assert s.name == "Optional"
|
||||||
|
assert len(s.members) == 1
|
||||||
|
|
||||||
|
assert s.members[0].name == "a"
|
||||||
|
assert s.members[0].member_type == MemberType.OPTIONAL
|
||||||
|
assert s.members[0].type_name == "bool"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vector_pod():
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
print(clang.cindex.conf.get_filename())
|
||||||
|
structs = build_structs("lib/tests/resources/vector-pod.hpp")
|
||||||
|
assert len(structs) == 2
|
||||||
|
|
||||||
|
s = structs[0]
|
||||||
|
|
||||||
|
assert s.name == "Pod"
|
||||||
|
assert len(s.members) == 3
|
||||||
|
|
||||||
|
assert s.members[0].name == "a"
|
||||||
|
assert s.members[0].member_type == MemberType.BASIC
|
||||||
|
assert s.members[0].type_name == "int"
|
||||||
|
|
||||||
|
assert s.members[1].name == "b"
|
||||||
|
assert s.members[1].member_type == MemberType.BASIC
|
||||||
|
assert s.members[1].type_name == "bool"
|
||||||
|
|
||||||
|
assert s.members[2].name == "c"
|
||||||
|
assert s.members[2].member_type == MemberType.BASIC
|
||||||
|
assert s.members[2].type_name == "char"
|
||||||
|
|
||||||
|
s = structs[1]
|
||||||
|
|
||||||
|
assert s.name == "VectorPod"
|
||||||
|
assert len(s.members) == 1
|
||||||
|
|
||||||
|
assert s.members[0].name == "a"
|
||||||
|
assert s.members[0].member_type == MemberType.VECTOR
|
||||||
|
assert s.members[0].type_name == "Pod"
|
||||||
|
|
||||||
|
|
||||||
|
def test_const():
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
print(clang.cindex.conf.get_filename())
|
||||||
|
structs = build_structs("lib/tests/resources/const.hpp")
|
||||||
|
assert len(structs) == 2
|
||||||
|
|
||||||
|
s = structs[0]
|
||||||
|
assert s.name == "Pod"
|
||||||
|
assert len(s.members) == 0
|
||||||
|
|
||||||
|
s = structs[1]
|
||||||
|
|
||||||
|
assert s.name == "Const"
|
||||||
|
assert len(s.members) == 6
|
||||||
|
|
||||||
|
assert s.members[0].name == "a"
|
||||||
|
assert s.members[0].member_type == MemberType.BASIC
|
||||||
|
assert s.members[0].type_name == "int"
|
||||||
|
|
||||||
|
assert s.members[1].name == "b"
|
||||||
|
assert s.members[1].member_type == MemberType.BASIC
|
||||||
|
assert s.members[1].type_name == "bool"
|
||||||
|
|
||||||
|
assert s.members[2].name == "c"
|
||||||
|
assert s.members[2].member_type == MemberType.BASIC
|
||||||
|
assert s.members[2].type_name == "char"
|
||||||
|
|
||||||
|
assert s.members[3].name == "d"
|
||||||
|
assert s.members[3].member_type == MemberType.BASIC
|
||||||
|
assert s.members[3].type_name == "Pod"
|
||||||
|
|
||||||
|
assert s.members[4].name == "e"
|
||||||
|
assert s.members[4].member_type == MemberType.VECTOR
|
||||||
|
assert s.members[4].type_name == "bool"
|
||||||
|
|
||||||
|
assert s.members[5].name == "f"
|
||||||
|
assert s.members[5].member_type == MemberType.OPTIONAL
|
||||||
|
assert s.members[5].type_name == "bool"
|
||||||
|
|
||||||
|
|
||||||
|
def test_string():
|
||||||
|
import clang.cindex
|
||||||
|
|
||||||
|
print(clang.cindex.conf.get_filename())
|
||||||
|
structs = build_structs("lib/tests/resources/string.hpp")
|
||||||
|
assert len(structs) == 1
|
||||||
|
|
||||||
|
s = structs[0]
|
||||||
|
|
||||||
|
assert s.name == "String"
|
||||||
|
assert len(s.members) == 4
|
||||||
|
|
||||||
|
assert s.members[0].name == "a"
|
||||||
|
assert s.members[0].member_type == MemberType.BASIC
|
||||||
|
assert s.members[0].type_name == "std::string"
|
||||||
|
|
||||||
|
assert s.members[1].name == "b"
|
||||||
|
assert s.members[1].member_type == MemberType.BASIC
|
||||||
|
assert s.members[1].type_name == "std::string"
|
||||||
|
|
||||||
|
assert s.members[2].name == "c"
|
||||||
|
assert s.members[2].member_type == MemberType.VECTOR
|
||||||
|
assert s.members[2].type_name == "std::string"
|
||||||
|
|
||||||
|
assert s.members[3].name == "d"
|
||||||
|
assert s.members[3].member_type == MemberType.OPTIONAL
|
||||||
|
assert s.members[3].type_name == "std::string"
|
||||||
|
|
||||||
|
|
||||||
|
init_clang()
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import clang.cindex
|
||||||
|
from clang.cindex import CursorKind
|
||||||
|
|
||||||
|
from .comment_commands import parse_comment_commands
|
||||||
|
from .member import Member
|
||||||
|
from .enum_constant import EnumConstant
|
||||||
|
from .struct import Struct
|
||||||
|
from .enum import Enum
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Walker:
|
||||||
|
def __init__(self, filename: str) -> None:
|
||||||
|
self.filename = filename
|
||||||
|
self.real_filepath = os.path.realpath(self.filename)
|
||||||
|
self.structs: List[Struct] = []
|
||||||
|
self.enums: List[Enum] = []
|
||||||
|
self.namespace: List[str] = []
|
||||||
|
|
||||||
|
def handle_node(self, node: clang.cindex.Cursor, struct: Optional[Struct], enum: Optional[Enum]) -> bool:
|
||||||
|
match node.kind:
|
||||||
|
case CursorKind.STRUCT_DECL:
|
||||||
|
new_struct = Struct(node.spelling)
|
||||||
|
if node.raw_comment is not None:
|
||||||
|
new_struct.comment_commands = parse_comment_commands(node.raw_comment)
|
||||||
|
new_struct.apply_comment_commands(new_struct.comment_commands)
|
||||||
|
if struct is not None:
|
||||||
|
new_struct.parent = struct.full_name
|
||||||
|
|
||||||
|
for child in node.get_children():
|
||||||
|
self.handle_node(child, new_struct, None)
|
||||||
|
|
||||||
|
self.structs.append(new_struct)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
case CursorKind.ENUM_DECL:
|
||||||
|
new_enum = Enum(node.spelling)
|
||||||
|
if node.raw_comment is not None:
|
||||||
|
new_enum.comment_commands = parse_comment_commands(node.raw_comment)
|
||||||
|
new_enum.apply_comment_commands(new_enum.comment_commands)
|
||||||
|
if struct is not None:
|
||||||
|
new_enum.parent = struct.full_name
|
||||||
|
|
||||||
|
for child in node.get_children():
|
||||||
|
self.handle_node(child, None, new_enum)
|
||||||
|
|
||||||
|
self.enums.append(new_enum)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
case CursorKind.ENUM_CONSTANT_DECL:
|
||||||
|
if enum:
|
||||||
|
# log.warning(
|
||||||
|
# f"enum constant decl {node.spelling} - enum comments: {enum.comment_commands} - node comments: {node.raw_comment}"
|
||||||
|
# )
|
||||||
|
constant = EnumConstant.from_node(node)
|
||||||
|
constant.apply_comment_commands(enum.comment_commands)
|
||||||
|
enum.constants.append(constant)
|
||||||
|
|
||||||
|
case CursorKind.FIELD_DECL:
|
||||||
|
type = node.type
|
||||||
|
if type is None:
|
||||||
|
# Skip nodes without a type
|
||||||
|
return False
|
||||||
|
|
||||||
|
# log.debug(f"{struct}: {type.spelling} {node.spelling} ({type.kind})")
|
||||||
|
if struct:
|
||||||
|
member = Member.from_field(node, self.namespace)
|
||||||
|
member.apply_comment_commands(struct.comment_commands)
|
||||||
|
struct.members.append(member)
|
||||||
|
|
||||||
|
case CursorKind.NAMESPACE:
|
||||||
|
self.namespace.append(node.spelling)
|
||||||
|
# Ignore namespaces
|
||||||
|
pass
|
||||||
|
|
||||||
|
case CursorKind.FUNCTION_DECL:
|
||||||
|
# Ignore function declarations
|
||||||
|
pass
|
||||||
|
|
||||||
|
case _:
|
||||||
|
# log.warning(f"unhandled kind: {node.kind}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def walk(self, node: clang.cindex.Cursor) -> None:
|
||||||
|
if node.location.file and not clang.cindex.Config().lib.clang_Location_isFromMainFile(node.location):
|
||||||
|
return
|
||||||
|
|
||||||
|
handled = self.handle_node(node, None, None)
|
||||||
|
|
||||||
|
if not handled:
|
||||||
|
for child in node.get_children():
|
||||||
|
self.walk(child)
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from os.path import realpath
|
||||||
|
|
||||||
|
from lib import definition_markers, implementation_markers, init_logging, replace_in_file
|
||||||
|
|
||||||
|
log = logging.getLogger("replace-in-file")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
init_logging()
|
||||||
|
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
log.error(f"usage: {sys.argv[0]} <path-to-header-file> <path-to-source-file>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# The files where we will do the replacements
|
||||||
|
header_path = realpath(sys.argv[1])
|
||||||
|
source_path = realpath(sys.argv[2])
|
||||||
|
|
||||||
|
# The files where we read the generated definitions & implementations from
|
||||||
|
# TODO: pass these along in argv too?
|
||||||
|
stdin_lines = sys.stdin.readlines()
|
||||||
|
generated_definition_path = realpath(stdin_lines[0].strip())
|
||||||
|
generated_implementation_path = realpath(stdin_lines[1].strip())
|
||||||
|
|
||||||
|
log.debug(f"Generated files: {generated_definition_path} / {generated_implementation_path}")
|
||||||
|
log.debug(f"Real files: {header_path} / {source_path}")
|
||||||
|
|
||||||
|
with open(generated_definition_path, "r") as generated_fh:
|
||||||
|
replacement = generated_fh.read()
|
||||||
|
replace_in_file(source_path, definition_markers, replacement)
|
||||||
|
|
||||||
|
with open(generated_implementation_path, "r") as generated_fh:
|
||||||
|
replacement = generated_fh.read()
|
||||||
|
replace_in_file(source_path, implementation_markers, replacement)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
black==23.3.0
|
||||||
|
flake8==6.0.0
|
||||||
|
mypy==1.3.0
|
||||||
|
isort==5.12.0
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
clang==15.0.7
|
||||||
|
Jinja2==3.1.2
|
||||||
|
jinja2-workarounds==0.1.0
|
||||||
|
clang-format==16.0.4
|
||||||
|
colorama==0.4.6
|
||||||
|
pytest==7.3.1
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
function(generate_json_impls)
|
||||||
|
set(_one_value_opts SRC_TARGET GEN_TARGET)
|
||||||
|
cmake_parse_arguments(PARSE_ARGV 0 arg
|
||||||
|
"" "${_one_value_opts}" ""
|
||||||
|
)
|
||||||
|
if (DEFINED arg_KEYWORDS_MISSING_VALUES)
|
||||||
|
message(FATAL_ERROR "generate_json_impls: Missing ${arg_KEYWORDS_MISSING_VALUES}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_venv_path "${CMAKE_CURRENT_BINARY_DIR}/eventsub-venv")
|
||||||
|
if(WIN32)
|
||||||
|
set(_venv_python_path "${_venv_path}/Scripts/python.exe")
|
||||||
|
set(_venv_pip_path "${_venv_path}/Scripts/pip.exe")
|
||||||
|
else()
|
||||||
|
set(_venv_python_path "${_venv_path}/bin/python3")
|
||||||
|
set(_venv_pip_path "${_venv_path}/bin/pip3")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
cmake_path(SET _requirements NORMALIZE "${CMAKE_CURRENT_LIST_DIR}/ast/requirements.txt")
|
||||||
|
cmake_path(SET _gen_script NORMALIZE "${CMAKE_CURRENT_LIST_DIR}/ast/generate-and-replace-dir.py")
|
||||||
|
cmake_path(SET _search_dir NORMALIZE "${CMAKE_CURRENT_LIST_DIR}/include/twitch-eventsub-ws")
|
||||||
|
|
||||||
|
get_target_property(_inc_dirs ${arg_SRC_TARGET} INCLUDE_DIRECTORIES)
|
||||||
|
list(APPEND _inc_dirs ${Boost_INCLUDE_DIRS})
|
||||||
|
list(APPEND _inc_dirs ${OPENSSL_INCLUDE_DIR})
|
||||||
|
list(APPEND _inc_dirs ${Qt${MAJOR_QT_VERSION}Core_INCLUDE_DIRS})
|
||||||
|
list(JOIN _inc_dirs ";" _inc_dir)
|
||||||
|
|
||||||
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
|
if(NOT Python3_FOUND)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_custom_target(${arg_GEN_TARGET}_python_venv_create
|
||||||
|
COMMENT "Creating python virtual environment"
|
||||||
|
COMMAND "${Python3_EXECUTABLE}" -m venv "${_venv_path}"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(${arg_GEN_TARGET}_python_venv_install
|
||||||
|
COMMENT "Installing python dependencies"
|
||||||
|
COMMAND "${_venv_pip_path}" install -r "${_requirements}"
|
||||||
|
DEPENDS "${_requirements}" ${arg_GEN_TARGET}_python_venv_create
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(${arg_GEN_TARGET}
|
||||||
|
COMMENT "Generating JSON deserializers"
|
||||||
|
DEPENDS ${arg_GEN_TARGET}_python_venv_install
|
||||||
|
COMMAND "${_venv_python_path}" "${_gen_script}" "${_search_dir}/messages" --includes "${_inc_dir}"
|
||||||
|
COMMAND "${_venv_python_path}" "${_gen_script}" "${_search_dir}/payloads" --includes "${_inc_dir}"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endfunction()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
cmake_policy(SET CMP0087 NEW) # evaluates generator expressions in `install(CODE/SCRIPT)`
|
||||||
|
cmake_policy(SET CMP0091 NEW) # select MSVC runtime library through `CMAKE_MSVC_RUNTIME_LIBRARY`
|
||||||
|
|
||||||
|
project(twitch-eventsub-ws-example VERSION 0.1.0)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_executable(${PROJECT_NAME} main.cpp)
|
||||||
|
|
||||||
|
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/.." twitch-eventsub-ws)
|
||||||
|
|
||||||
|
find_library(LIBRT rt)
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
|
target_link_libraries(${PROJECT_NAME}
|
||||||
|
PUBLIC
|
||||||
|
|
||||||
|
Threads::Threads
|
||||||
|
twitch-eventsub-ws
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the output of TARGET to be
|
||||||
|
# - CMAKE_BIN_DIR/lib for libraries
|
||||||
|
# - CMAKE_BIN_DIR/bin for BINARIES
|
||||||
|
# an additional argument specifies the subdirectory.
|
||||||
|
function(set_target_directory_hierarchy TARGET)
|
||||||
|
set_target_properties(${TARGET}
|
||||||
|
PROPERTIES
|
||||||
|
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/${ARGV1}"
|
||||||
|
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/${ARGV1}"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${ARGV1}"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/${ARGV1}"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/${ARGV1}"
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin/${ARGV1}"
|
||||||
|
)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
set_target_directory_hierarchy(${PROJECT_NAME})
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(${PROJECT_NAME} PUBLIC /EHsc /bigobj)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (LIBRT)
|
||||||
|
target_link_libraries(${PROJECT_NAME}
|
||||||
|
PUBLIC
|
||||||
|
${LIBRT}
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#include "twitch-eventsub-ws/listener.hpp"
|
||||||
|
#include "twitch-eventsub-ws/session.hpp"
|
||||||
|
#include "twitch-eventsub-ws/string.hpp"
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/ssl.hpp>
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
#include <qdebug.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
using namespace chatterino::eventsub;
|
||||||
|
|
||||||
|
class MyListener final : public lib::Listener
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void onSessionWelcome(
|
||||||
|
lib::messages::Metadata metadata,
|
||||||
|
lib::payload::session_welcome::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
std::cout << "ON session welcome " << payload.id << " XD\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onNotification(lib::messages::Metadata metadata,
|
||||||
|
const boost::json::value &jv) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
std::cout << "on notification: " << jv << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChannelBan(lib::messages::Metadata metadata,
|
||||||
|
lib::payload::channel_ban::v1::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
std::cout << "Channel ban occured in "
|
||||||
|
<< payload.event.broadcasterUserLogin << "'s channel:"
|
||||||
|
<< " isPermanent=" << payload.event.isPermanent
|
||||||
|
<< " reason=" << payload.event.reason
|
||||||
|
<< " userLogin=" << payload.event.userLogin
|
||||||
|
<< " moderatorLogin=" << payload.event.moderatorUserLogin
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void onStreamOnline(
|
||||||
|
lib::messages::Metadata metadata,
|
||||||
|
lib::payload::stream_online::v1::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
(void)payload;
|
||||||
|
std::cout << "ON STREAM ONLINE XD\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onStreamOffline(
|
||||||
|
lib::messages::Metadata metadata,
|
||||||
|
lib::payload::stream_offline::v1::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
(void)payload;
|
||||||
|
std::cout << "ON STREAM OFFLINE XD\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChannelChatNotification(
|
||||||
|
lib::messages::Metadata metadata,
|
||||||
|
lib::payload::channel_chat_notification::v1::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
(void)payload;
|
||||||
|
std::cout << "Received channel.chat.notification v1\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChannelUpdate(
|
||||||
|
lib::messages::Metadata metadata,
|
||||||
|
lib::payload::channel_update::v1::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
(void)payload;
|
||||||
|
std::cout << "Channel update event!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChannelChatMessage(
|
||||||
|
lib::messages::Metadata metadata,
|
||||||
|
lib::payload::channel_chat_message::v1::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
(void)payload;
|
||||||
|
std::cout << "Channel chat message event!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChannelModerate(
|
||||||
|
lib::messages::Metadata metadata,
|
||||||
|
lib::payload::channel_moderate::v2::Payload payload) override
|
||||||
|
{
|
||||||
|
(void)metadata;
|
||||||
|
(void)payload;
|
||||||
|
std::cout << "Channel moderate event!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add your new subscription types above this line
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
|
||||||
|
lib::String a("foo");
|
||||||
|
|
||||||
|
// qDebug() << "xd1:"
|
||||||
|
// << QString::fromStdString(std::get<std::string>(a.backingString));
|
||||||
|
qDebug() << "xd2:" << a.qt();
|
||||||
|
qDebug() << "xd3:" << a.qt();
|
||||||
|
|
||||||
|
std::string userAgent{"chatterino-eventsub-testing"};
|
||||||
|
|
||||||
|
// for use with twitch CLI: twitch event websocket start-server --ssl --port 3012
|
||||||
|
// std::string host{"localhost"};
|
||||||
|
// std::string port{"3012"};
|
||||||
|
// std::string path{"/ws"};
|
||||||
|
|
||||||
|
// for use with websocat: websocat -s 8080 --pkcs12-der certificate.p12
|
||||||
|
std::string host{"localhost"};
|
||||||
|
std::string port{"8080"};
|
||||||
|
std::string path;
|
||||||
|
|
||||||
|
// for use with real Twitch eventsub
|
||||||
|
// std::string host{"eventsub.wss.twitch.tv"};
|
||||||
|
// std::string port("443");
|
||||||
|
// std::string path("/ws");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
boost::asio::io_context ctx(1);
|
||||||
|
|
||||||
|
boost::asio::ssl::context sslContext{
|
||||||
|
boost::asio::ssl::context::tlsv12_client};
|
||||||
|
|
||||||
|
// TODO: Load certificates into SSL context
|
||||||
|
|
||||||
|
std::make_shared<lib::Session>(ctx, sslContext,
|
||||||
|
std::make_unique<MyListener>())
|
||||||
|
->run(host, port, path, userAgent);
|
||||||
|
|
||||||
|
ctx.run();
|
||||||
|
}
|
||||||
|
catch (std::exception &e)
|
||||||
|
{
|
||||||
|
std::cerr << "Exception: " << e.what() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib {
|
||||||
|
|
||||||
|
struct AsISO8601 {
|
||||||
|
};
|
||||||
|
|
||||||
|
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 &);
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/system/error_category.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::error {
|
||||||
|
|
||||||
|
class ApplicationErrorCategory final : public boost::system::error_category
|
||||||
|
{
|
||||||
|
const std::string innerMessage;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ApplicationErrorCategory(const char *_innerMessage)
|
||||||
|
: innerMessage(_innerMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit ApplicationErrorCategory(std::string _innerMessage)
|
||||||
|
: innerMessage(std::move(_innerMessage))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *name() const noexcept override
|
||||||
|
{
|
||||||
|
return "Application JSON error";
|
||||||
|
}
|
||||||
|
std::string message(int /*ev*/) const override
|
||||||
|
{
|
||||||
|
return this->innerMessage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApplicationErrorCategory EXPECTED_OBJECT{"Expected object"};
|
||||||
|
const ApplicationErrorCategory MISSING_KEY{"Missing key"};
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::error
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/json/object.hpp>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
std::optional<T> readMember(const boost::json::object &obj,
|
||||||
|
std::string_view key)
|
||||||
|
{
|
||||||
|
const auto *it = obj.find(key);
|
||||||
|
|
||||||
|
if (it == obj.end())
|
||||||
|
{
|
||||||
|
// No member with the key found
|
||||||
|
std::cerr << "No member with the key " << key << " found\n";
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = boost::json::try_value_to<T>(it->value());
|
||||||
|
if (!result.has_value())
|
||||||
|
{
|
||||||
|
std::cerr << key << " could not be deserialized to the desired type\n";
|
||||||
|
// Member could not be serialized to this type
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/messages/metadata.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/channel-ban-v1.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/channel-chat-notification-v1.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/channel-moderate-v2.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/channel-update-v1.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/session-welcome.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/stream-offline-v1.hpp"
|
||||||
|
#include "twitch-eventsub-ws/payloads/stream-online-v1.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib {
|
||||||
|
|
||||||
|
class Listener
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~Listener() = default;
|
||||||
|
|
||||||
|
virtual void onSessionWelcome(
|
||||||
|
messages::Metadata metadata,
|
||||||
|
payload::session_welcome::Payload payload) = 0;
|
||||||
|
|
||||||
|
virtual void onNotification(messages::Metadata metadata,
|
||||||
|
const boost::json::value &jv) = 0;
|
||||||
|
|
||||||
|
// Subscription types
|
||||||
|
virtual void onChannelBan(messages::Metadata metadata,
|
||||||
|
payload::channel_ban::v1::Payload payload) = 0;
|
||||||
|
|
||||||
|
virtual void onStreamOnline(
|
||||||
|
messages::Metadata metadata,
|
||||||
|
payload::stream_online::v1::Payload payload) = 0;
|
||||||
|
|
||||||
|
virtual void onStreamOffline(
|
||||||
|
messages::Metadata metadata,
|
||||||
|
payload::stream_offline::v1::Payload payload) = 0;
|
||||||
|
|
||||||
|
virtual void onChannelChatNotification(
|
||||||
|
messages::Metadata metadata,
|
||||||
|
payload::channel_chat_notification::v1::Payload payload) = 0;
|
||||||
|
|
||||||
|
virtual void onChannelUpdate(
|
||||||
|
messages::Metadata metadata,
|
||||||
|
payload::channel_update::v1::Payload payload) = 0;
|
||||||
|
|
||||||
|
virtual void onChannelChatMessage(
|
||||||
|
messages::Metadata metadata,
|
||||||
|
payload::channel_chat_message::v1::Payload payload) = 0;
|
||||||
|
|
||||||
|
virtual void onChannelModerate(
|
||||||
|
messages::Metadata metadata,
|
||||||
|
payload::channel_moderate::v2::Payload payload) = 0;
|
||||||
|
|
||||||
|
// Add your new subscription types above this line
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/errors.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::messages {
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"message_id": "40cc68b8-dc5b-a46e-0388-a7c9193eec5e",
|
||||||
|
"message_type": "session_welcome",
|
||||||
|
"message_timestamp": "2023-05-14T12:31:47.995298776Z"
|
||||||
|
"subscription_type": "channel.unban", // only included on message_type=notification
|
||||||
|
"subscription_version": "1" // only included on message_type=notification
|
||||||
|
},
|
||||||
|
"payload": ...
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Metadata {
|
||||||
|
const std::string messageID;
|
||||||
|
const std::string messageType;
|
||||||
|
// TODO: should this be chronofied?
|
||||||
|
const std::string messageTimestamp;
|
||||||
|
|
||||||
|
const std::optional<std::string> subscriptionType;
|
||||||
|
const std::optional<std::string> subscriptionVersion;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Metadata, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Metadata>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::messages
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::channel_ban::v1 {
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"metadata": ...,
|
||||||
|
"payload": {
|
||||||
|
"subscription": {
|
||||||
|
"id": "4aa632e0-fca3-590b-e981-bbd12abdb3fe",
|
||||||
|
"status": "enabled",
|
||||||
|
"type": "channel.ban",
|
||||||
|
"version": "1",
|
||||||
|
"condition": {
|
||||||
|
"broadcaster_user_id": "74378979"
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"method": "websocket",
|
||||||
|
"session_id": "38de428e_b11f07be"
|
||||||
|
},
|
||||||
|
"created_at": "2023-05-20T12:30:55.518375571Z",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"banned_at": "2023-05-20T12:30:55.518375571Z",
|
||||||
|
"broadcaster_user_id": "74378979",
|
||||||
|
"broadcaster_user_login": "testBroadcaster",
|
||||||
|
"broadcaster_user_name": "testBroadcaster",
|
||||||
|
"ends_at": "2023-05-20T12:40:55.518375571Z",
|
||||||
|
"is_permanent": false,
|
||||||
|
"moderator_user_id": "29024944",
|
||||||
|
"moderator_user_login": "CLIModerator",
|
||||||
|
"moderator_user_name": "CLIModerator",
|
||||||
|
"reason": "This is a test event",
|
||||||
|
"user_id": "40389552",
|
||||||
|
"user_login": "testFromUser",
|
||||||
|
"user_name": "testFromUser"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Event {
|
||||||
|
// User ID (e.g. 117166826) of the user who's channel the event took place in
|
||||||
|
std::string broadcasterUserID;
|
||||||
|
// User Login (e.g. testaccount_420) of the user who's channel the event took place in
|
||||||
|
std::string broadcasterUserLogin;
|
||||||
|
// User Name (e.g. 테스트계정420) of the user who's channel the event took place in
|
||||||
|
std::string broadcasterUserName;
|
||||||
|
|
||||||
|
// User ID (e.g. 117166826) of the user who took the action
|
||||||
|
std::string moderatorUserID;
|
||||||
|
// User Login (e.g. testaccount_420) of the user who took the action
|
||||||
|
std::string moderatorUserLogin;
|
||||||
|
// User Name (e.g. 테스트계정420) of the user who took the action
|
||||||
|
std::string moderatorUserName;
|
||||||
|
|
||||||
|
// User ID (e.g. 117166826) of the user who was timed out or banned
|
||||||
|
std::string userID;
|
||||||
|
// User Login (e.g. testaccount_420) of the user who was timed out or banned
|
||||||
|
std::string userLogin;
|
||||||
|
// User Name (e.g. 테스트계정420) of the user who was timed out or banned
|
||||||
|
std::string userName;
|
||||||
|
|
||||||
|
// Reason given for the timeout or ban.
|
||||||
|
// If no reason was specified, this string is empty
|
||||||
|
std::string reason;
|
||||||
|
|
||||||
|
// Set to true if this was a ban.
|
||||||
|
// If this is false, this event describes a timeout
|
||||||
|
bool isPermanent;
|
||||||
|
|
||||||
|
// Time point when the timeout or ban took place
|
||||||
|
/// json_tag=AsISO8601
|
||||||
|
std::chrono::system_clock::time_point bannedAt;
|
||||||
|
|
||||||
|
// Time point when the timeout will end
|
||||||
|
/// json_tag=AsISO8601
|
||||||
|
std::optional<std::chrono::system_clock::time_point> endsAt;
|
||||||
|
|
||||||
|
// Returns the duration of the timeout
|
||||||
|
// If this event describes a ban, the value returned won't make sense
|
||||||
|
std::chrono::system_clock::duration timeoutDuration() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Payload {
|
||||||
|
const subscription::Subscription subscription;
|
||||||
|
|
||||||
|
const Event event;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::channel_ban::v1
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
...,
|
||||||
|
"event": {
|
||||||
|
"broadcaster_user_id": "117166826",
|
||||||
|
"broadcaster_user_login": "testaccount_420",
|
||||||
|
"broadcaster_user_name": "테스트계정420",
|
||||||
|
"chatter_user_id": "100135110",
|
||||||
|
"chatter_user_login": "streamelements",
|
||||||
|
"chatter_user_name": "StreamElements",
|
||||||
|
"message_id": "7732e85c-4543-464f-9d84-533e73f71459",
|
||||||
|
"message": {
|
||||||
|
"text": "https://youtu.be/v515yo0Ad_M",
|
||||||
|
"fragments": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "https://youtu.be/v515yo0Ad_M",
|
||||||
|
"cheermote": null,
|
||||||
|
"emote": null,
|
||||||
|
"mention": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"color": "#5B99FF",
|
||||||
|
"badges": [
|
||||||
|
{
|
||||||
|
"set_id": "moderator",
|
||||||
|
"id": "1",
|
||||||
|
"info": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"set_id": "partner",
|
||||||
|
"id": "1",
|
||||||
|
"info": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message_type": "text",
|
||||||
|
"cheer": null,
|
||||||
|
"reply": null,
|
||||||
|
"channel_points_custom_reward_id": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::channel_chat_message::v1 {
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Badge {
|
||||||
|
std::string setID;
|
||||||
|
std::string id;
|
||||||
|
std::string info;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Cheermote {
|
||||||
|
std::string prefix;
|
||||||
|
int bits;
|
||||||
|
int tier;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Emote {
|
||||||
|
std::string id;
|
||||||
|
std::string emoteSetID;
|
||||||
|
std::string ownerID;
|
||||||
|
std::vector<std::string> format;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Mention {
|
||||||
|
std::string userID;
|
||||||
|
std::string userName;
|
||||||
|
std::string userLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct MessageFragment {
|
||||||
|
std::string type;
|
||||||
|
std::string text;
|
||||||
|
std::optional<Cheermote> cheermote;
|
||||||
|
std::optional<Emote> emote;
|
||||||
|
std::optional<Mention> mention;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Message {
|
||||||
|
std::string text;
|
||||||
|
std::vector<MessageFragment> fragments;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Cheer {
|
||||||
|
int bits;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Reply {
|
||||||
|
std::string parentMessageID;
|
||||||
|
std::string parentUserID;
|
||||||
|
std::string parentUserLogin;
|
||||||
|
std::string parentUserName;
|
||||||
|
std::string parentMessageBody;
|
||||||
|
|
||||||
|
std::string threadMessageID;
|
||||||
|
std::string threadUserID;
|
||||||
|
std::string threadUserLogin;
|
||||||
|
std::string threadUserName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Event {
|
||||||
|
// Broadcaster of the channel the message was sent in
|
||||||
|
std::string broadcasterUserID;
|
||||||
|
std::string broadcasterUserLogin;
|
||||||
|
std::string broadcasterUserName;
|
||||||
|
|
||||||
|
// User who sent the message
|
||||||
|
std::string chatterUserID;
|
||||||
|
std::string chatterUserLogin;
|
||||||
|
std::string chatterUserName;
|
||||||
|
|
||||||
|
// Color of the user who sent the message
|
||||||
|
std::string color;
|
||||||
|
|
||||||
|
std::vector<Badge> badges;
|
||||||
|
|
||||||
|
std::string messageID;
|
||||||
|
std::string messageType;
|
||||||
|
Message message;
|
||||||
|
|
||||||
|
std::optional<Cheer> cheer;
|
||||||
|
std::optional<Reply> reply;
|
||||||
|
std::optional<std::string> channelPointsCustomRewardID;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Payload {
|
||||||
|
const subscription::Subscription subscription;
|
||||||
|
|
||||||
|
const Event event;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Badge, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Badge>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Cheermote, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Cheermote>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Emote, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Emote>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Mention, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Mention>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<MessageFragment, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<MessageFragment>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Message, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Message>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Cheer, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Cheer>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Reply, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Reply>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::channel_chat_message::v1
|
||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::channel_chat_notification::v1 {
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Badge {
|
||||||
|
std::string setID;
|
||||||
|
std::string id;
|
||||||
|
std::string info;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Cheermote {
|
||||||
|
std::string prefix;
|
||||||
|
int bits;
|
||||||
|
int tier;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Emote {
|
||||||
|
std::string id;
|
||||||
|
std::string emoteSetID;
|
||||||
|
std::string ownerID;
|
||||||
|
std::vector<std::string> format;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Mention {
|
||||||
|
std::string userID;
|
||||||
|
std::string userName;
|
||||||
|
std::string userLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct MessageFragment {
|
||||||
|
std::string type;
|
||||||
|
std::string text;
|
||||||
|
std::optional<Cheermote> cheermote;
|
||||||
|
std::optional<Emote> emote;
|
||||||
|
std::optional<Mention> mention;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Subcription {
|
||||||
|
std::string subTier;
|
||||||
|
bool isPrime;
|
||||||
|
int durationMonths;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Resubscription {
|
||||||
|
int cumulativeMonths;
|
||||||
|
int durationMonths;
|
||||||
|
std::optional<int> streakMonths;
|
||||||
|
std::string subTier;
|
||||||
|
bool isPrime;
|
||||||
|
bool isGift;
|
||||||
|
bool gifterIsAnonymous;
|
||||||
|
std::optional<std::string> gifterUserID;
|
||||||
|
std::optional<std::string> gifterUserName;
|
||||||
|
std::optional<std::string> gifterUserLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct GiftSubscription {
|
||||||
|
int durationMonths;
|
||||||
|
std::optional<int> cumulativeTotal;
|
||||||
|
std::optional<int> streakMonths;
|
||||||
|
std::string recipientUserID;
|
||||||
|
std::string recipientUserName;
|
||||||
|
std::string recipientUserLogin;
|
||||||
|
std::string subTier;
|
||||||
|
std::optional<std::string> communityGiftID;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct CommunityGiftSubscription {
|
||||||
|
std::string id;
|
||||||
|
int total;
|
||||||
|
std::string subTier;
|
||||||
|
std::optional<int> cumulativeTotal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct GiftPaidUpgrade {
|
||||||
|
bool gifterIsAnonymous;
|
||||||
|
std::optional<std::string> gifterUserID;
|
||||||
|
std::optional<std::string> gifterUserName;
|
||||||
|
std::optional<std::string> gifterUserLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct PrimePaidUpgrade {
|
||||||
|
std::string subTier;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Raid {
|
||||||
|
std::string userID;
|
||||||
|
std::string userName;
|
||||||
|
std::string userLogin;
|
||||||
|
int viewerCount;
|
||||||
|
std::string profileImageURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Unraid {
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct PayItForward {
|
||||||
|
bool gifterIsAnonymous;
|
||||||
|
std::optional<std::string> gifterUserID;
|
||||||
|
std::optional<std::string> gifterUserName;
|
||||||
|
std::optional<std::string> gifterUserLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Announcement {
|
||||||
|
std::string color;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct CharityDonationAmount {
|
||||||
|
int value;
|
||||||
|
int decimalPlaces;
|
||||||
|
std::string currency;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct CharityDonation {
|
||||||
|
std::string charityName;
|
||||||
|
CharityDonationAmount amount;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct BitsBadgeTier {
|
||||||
|
int tier;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Message {
|
||||||
|
std::string text;
|
||||||
|
std::vector<MessageFragment> fragments;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Event {
|
||||||
|
std::string broadcasterUserID;
|
||||||
|
std::string broadcasterUserLogin;
|
||||||
|
std::string broadcasterUserName;
|
||||||
|
std::string chatterUserID;
|
||||||
|
std::string chatterUserLogin;
|
||||||
|
std::string chatterUserName;
|
||||||
|
bool chatterIsAnonymous;
|
||||||
|
std::string color;
|
||||||
|
std::vector<Badge> badges;
|
||||||
|
std::string systemMessage;
|
||||||
|
std::string messageID;
|
||||||
|
Message message;
|
||||||
|
std::string noticeType;
|
||||||
|
std::optional<Subcription> sub;
|
||||||
|
std::optional<Resubscription> resub;
|
||||||
|
std::optional<GiftSubscription> subGift;
|
||||||
|
std::optional<CommunityGiftSubscription> communitySubGift;
|
||||||
|
std::optional<GiftPaidUpgrade> giftPaidUpgrade;
|
||||||
|
std::optional<PrimePaidUpgrade> primePaidUpgrade;
|
||||||
|
std::optional<Raid> raid;
|
||||||
|
std::optional<Unraid> unraid;
|
||||||
|
std::optional<PayItForward> payItForward;
|
||||||
|
std::optional<Announcement> announcement;
|
||||||
|
std::optional<CharityDonation> charityDonation;
|
||||||
|
std::optional<BitsBadgeTier> bitsBadgeTier;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Payload {
|
||||||
|
const subscription::Subscription subscription;
|
||||||
|
|
||||||
|
const Event event;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Badge, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Badge>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Cheermote, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Cheermote>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Emote, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Emote>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Mention, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Mention>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<MessageFragment, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<MessageFragment>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Subcription, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Subcription>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Resubscription, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Resubscription>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<GiftSubscription, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<GiftSubscription>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<CommunityGiftSubscription, boost::json::value>::type
|
||||||
|
tag_invoke(boost::json::try_value_to_tag<CommunityGiftSubscription>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<GiftPaidUpgrade, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<GiftPaidUpgrade>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<PrimePaidUpgrade, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<PrimePaidUpgrade>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Raid, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Raid>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Unraid, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Unraid>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<PayItForward, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<PayItForward>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Announcement, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Announcement>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<CharityDonationAmount, boost::json::value>::type
|
||||||
|
tag_invoke(boost::json::try_value_to_tag<CharityDonationAmount>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<CharityDonation, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<CharityDonation>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<BitsBadgeTier, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<BitsBadgeTier>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Message, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Message>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::channel_chat_notification::v1
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||||
|
#include "twitch-eventsub-ws/string.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::channel_moderate::v2 {
|
||||||
|
|
||||||
|
/* follower mode enabled
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"followers","followers":{"follow_duration_minutes":0},"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* follower mode disabled
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"followersoff","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* follower mode set to 7d
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"followers","followers":{"follow_duration_minutes":10080},"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Followers {
|
||||||
|
int followDurationMinutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* slow mode set to 30s
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"slow","followers":null,"slow":{"wait_time_seconds":30},"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* slow mode off
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"slowoff","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Slow {
|
||||||
|
int waitTimeSeconds;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* User is VIP'ed
|
||||||
|
{
|
||||||
|
"subscription": {
|
||||||
|
"id": "ef1d3e66-0001-4cdc-8e74-8746f9243704",
|
||||||
|
"status": "enabled",
|
||||||
|
"type": "channel.moderate",
|
||||||
|
"version": "2",
|
||||||
|
"condition": {
|
||||||
|
"broadcaster_user_id": "11148817",
|
||||||
|
"moderator_user_id": "117166826"
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"method": "websocket",
|
||||||
|
"session_id": "AgoQBV1S6bGuR7e2hs04k4J1EhIGY2VsbC1j"
|
||||||
|
},
|
||||||
|
"created_at": "2025-02-01T11:55:30.114060889Z",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"broadcaster_user_id": "11148817",
|
||||||
|
"broadcaster_user_login": "pajlada",
|
||||||
|
"broadcaster_user_name": "pajlada",
|
||||||
|
"source_broadcaster_user_id": null,
|
||||||
|
"source_broadcaster_user_login": null,
|
||||||
|
"source_broadcaster_user_name": null,
|
||||||
|
"moderator_user_id": "11148817",
|
||||||
|
"moderator_user_login": "pajlada",
|
||||||
|
"moderator_user_name": "pajlada",
|
||||||
|
"action": "vip",
|
||||||
|
"followers": null,
|
||||||
|
"slow": null,
|
||||||
|
"vip": {
|
||||||
|
"user_id": "159849156",
|
||||||
|
"user_login": "bajlada",
|
||||||
|
"user_name": "BajLada"
|
||||||
|
},
|
||||||
|
"unvip": null,
|
||||||
|
"mod": null,
|
||||||
|
"unmod": null,
|
||||||
|
"ban": null,
|
||||||
|
"unban": null,
|
||||||
|
"timeout": null,
|
||||||
|
"untimeout": null,
|
||||||
|
"raid": null,
|
||||||
|
"unraid": null,
|
||||||
|
"delete": null,
|
||||||
|
"automod_terms": null,
|
||||||
|
"unban_request": null,
|
||||||
|
"warn": null,
|
||||||
|
"shared_chat_ban": null,
|
||||||
|
"shared_chat_unban": null,
|
||||||
|
"shared_chat_timeout": null,
|
||||||
|
"shared_chat_untimeout": null,
|
||||||
|
"shared_chat_delete": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Vip {
|
||||||
|
String userID;
|
||||||
|
String userLogin;
|
||||||
|
String userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* user is unvipped
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"unvip","followers":null,"slow":null,"vip":null,"unvip":{"user_id":"159849156","user_login":"bajlada","user_name":"BajLada"},"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Unvip {
|
||||||
|
String userID;
|
||||||
|
String userLogin;
|
||||||
|
String userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* user is modded
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"mod","followers":null,"slow":null,"vip":null,"unvip":null,"mod":{"user_id":"159849156","user_login":"bajlada","user_name":"BajLada"},"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Mod {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* user is unmodded
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"unmod","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":{"user_id":"159849156","user_login":"bajlada","user_name":"BajLada"},"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Unmod {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* user is banned with reason
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"ban","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":{"user_id":"70948394","user_login":"weeb123","user_name":"WEEB123","reason":"this is the reason"},"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* user is banned without reason
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"ban","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":{"user_id":"70948394","user_login":"weeb123","user_name":"WEEB123","reason":""},"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Ban {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
// TODO: Verify that we handle null here
|
||||||
|
std::string reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* user is unbanned
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"unban","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":{"user_id":"70948394","user_login":"weeb123","user_name":"WEEB123"},"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Unban {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* user is timed out without reason
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"timeout","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":{"user_id":"70948394","user_login":"weeb123","user_name":"WEEB123","reason":"","expires_at":"2025-02-01T12:11:02.684499409Z"},"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* user is timed out with reason
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"timeout","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":{"user_id":"70948394","user_login":"weeb123","user_name":"WEEB123","reason":"this is the reason","expires_at":"2025-02-01T12:11:15.859552916Z"},"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Timeout {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
// TODO: Verify that we handle null here
|
||||||
|
std::string reason;
|
||||||
|
// TODO: This should be a timestamp?
|
||||||
|
std::string expiresAt;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* user is untimeouted
|
||||||
|
{"subscription":{"id":"86d99e53-2837-40cf-bc6e-c6e00698919c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQZgfnyKxXQ32hpzCWF4aCGBIGY2VsbC1j"},"created_at":"2025-02-01T12:02:16.005321321Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"untimeout","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":{"user_id":"70948394","user_login":"weeb123","user_name":"WEEB123"},"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Untimeout {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* channel is raided (from bajlada to pajlada)
|
||||||
|
{"subscription":{"id":"e7b45c7a-9b4d-4101-8d7d-92e8945c26fa","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"159849156","moderator_user_id":"159849156"},"transport":{"method":"websocket","session_id":"AgoQEPRIfB3SQTCSkJM2zznvNxIGY2VsbC1j"},"created_at":"2025-02-01T12:12:45.685831769Z","cost":0},"event":{"broadcaster_user_id":"159849156","broadcaster_user_login":"bajlada","broadcaster_user_name":"BajLada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"159849156","moderator_user_login":"bajlada","moderator_user_name":"BajLada","action":"raid","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":{"user_id":"11148817","user_login":"pajlada","user_name":"pajlada","viewer_count":0},"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Raid {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
|
||||||
|
int viewerCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* raid from bajlada to pajlada was cancelled
|
||||||
|
{"subscription":{"id":"e7b45c7a-9b4d-4101-8d7d-92e8945c26fa","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"159849156","moderator_user_id":"159849156"},"transport":{"method":"websocket","session_id":"AgoQEPRIfB3SQTCSkJM2zznvNxIGY2VsbC1j"},"created_at":"2025-02-01T12:12:45.685831769Z","cost":0},"event":{"broadcaster_user_id":"159849156","broadcaster_user_login":"bajlada","broadcaster_user_name":"BajLada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"159849156","moderator_user_login":"bajlada","moderator_user_name":"BajLada","action":"unraid","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":{"user_id":"11148817","user_login":"pajlada","user_name":"pajlada"},"delete":null,"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Unraid {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* message deleted
|
||||||
|
{"subscription":{"id":"4284c08c-402a-43a8-8537-1e75f38f562c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.083098352Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"delete","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":{"user_id":"232490245","user_login":"namtheweebs","user_name":"NaMTheWeebs","message_id":"6c9bd28b-779b-4607-b66f-5c336e1ae29e","message_body":"bajlada raid NomNom"},"automod_terms":null,"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Delete {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
std::string messageID;
|
||||||
|
std::string messageBody;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* automodded message approved
|
||||||
|
{"subscription":{"id":"0ccc8f11-7e77-40cf-84a9-25ab934c30fb","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"117166826","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.092673205Z","cost":0},"event":{"broadcaster_user_id":"117166826","broadcaster_user_login":"testaccount_420","broadcaster_user_name":"테스트계정420","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"117166826","moderator_user_login":"testaccount_420","moderator_user_name":"테스트계정420","action":"add_permitted_term","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":{"action":"add","list":"permitted","terms":["cock cock cock penis sex cock"],"from_automod":true},"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* automodded message denied
|
||||||
|
{"subscription":{"id":"0ccc8f11-7e77-40cf-84a9-25ab934c30fb","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"117166826","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.092673205Z","cost":0},"event":{"broadcaster_user_id":"117166826","broadcaster_user_login":"testaccount_420","broadcaster_user_name":"테스트계정420","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"117166826","moderator_user_login":"testaccount_420","moderator_user_name":"테스트계정420","action":"add_blocked_term","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":{"action":"add","list":"blocked","terms":["boobies"],"from_automod":true},"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
|
||||||
|
{"subscription":{"id":"0ccc8f11-7e77-40cf-84a9-25ab934c30fb","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"117166826","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.092673205Z","cost":0},"event":{"broadcaster_user_id":"117166826","broadcaster_user_login":"testaccount_420","broadcaster_user_name":"테스트계정420","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"117166826","moderator_user_login":"testaccount_420","moderator_user_name":"테스트계정420","action":"add_blocked_term","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":{"action":"add","list":"blocked","terms":["cock cock cock penis sex cock penis penis cock"],"from_automod":true},"unban_request":null,"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct AutomodTerms {
|
||||||
|
// either add or remove
|
||||||
|
std::string action;
|
||||||
|
// either blocked or permitted
|
||||||
|
std::string list;
|
||||||
|
|
||||||
|
std::vector<std::string> terms;
|
||||||
|
bool fromAutomod;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* unban request approved
|
||||||
|
{"subscription":{"id":"4284c08c-402a-43a8-8537-1e75f38f562c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.083098352Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"approve_unban_request","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":{"is_approved":true,"user_id":"159849156","user_login":"bajlada","user_name":"BajLada","moderator_message":"you have been granted mercy"},"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* unban request denied
|
||||||
|
{"subscription":{"id":"4284c08c-402a-43a8-8537-1e75f38f562c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.083098352Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"deny_unban_request","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":{"is_approved":false,"user_id":"975285839","user_login":"selenatormapguy","user_name":"selenatormapguy","moderator_message":""},"warn":null,"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct UnbanRequest {
|
||||||
|
bool isApproved;
|
||||||
|
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
|
||||||
|
std::string moderatorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* freetext warn from chatterino
|
||||||
|
{"subscription":{"id":"4284c08c-402a-43a8-8537-1e75f38f562c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.083098352Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"warn","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":{"user_id":"159849156","user_login":"bajlada","user_name":"BajLada","reason":"this is a test warning from chatterino","chat_rules_cited":null},"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* warning from web ui
|
||||||
|
{"subscription":{"id":"4284c08c-402a-43a8-8537-1e75f38f562c","status":"enabled","type":"channel.moderate","version":"2","condition":{"broadcaster_user_id":"11148817","moderator_user_id":"117166826"},"transport":{"method":"websocket","session_id":"AgoQfS3RYz3MSqOophais4HEjxIGY2VsbC1j"},"created_at":"2025-02-01T12:15:13.083098352Z","cost":0},"event":{"broadcaster_user_id":"11148817","broadcaster_user_login":"pajlada","broadcaster_user_name":"pajlada","source_broadcaster_user_id":null,"source_broadcaster_user_login":null,"source_broadcaster_user_name":null,"moderator_user_id":"11148817","moderator_user_login":"pajlada","moderator_user_name":"pajlada","action":"warn","followers":null,"slow":null,"vip":null,"unvip":null,"mod":null,"unmod":null,"ban":null,"unban":null,"timeout":null,"untimeout":null,"raid":null,"unraid":null,"delete":null,"automod_terms":null,"unban_request":null,"warn":{"user_id":"159849156","user_login":"bajlada","user_name":"BajLada","reason":"and a custom reason","chat_rules_cited":["Rule 2"]},"shared_chat_ban":null,"shared_chat_unban":null,"shared_chat_timeout":null,"shared_chat_untimeout":null,"shared_chat_delete":null}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Warn {
|
||||||
|
std::string userID;
|
||||||
|
std::string userLogin;
|
||||||
|
std::string userName;
|
||||||
|
std::string reason;
|
||||||
|
|
||||||
|
// TODO: Verify we handle null for this as an empty vector
|
||||||
|
std::vector<std::string> chatRulesCited;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
enum class Action : uint8_t {
|
||||||
|
Ban,
|
||||||
|
Timeout,
|
||||||
|
Unban,
|
||||||
|
Untimeout,
|
||||||
|
Clear,
|
||||||
|
Emoteonly,
|
||||||
|
Emoteonlyoff,
|
||||||
|
Followers,
|
||||||
|
Followersoff,
|
||||||
|
Uniquechat,
|
||||||
|
Uniquechatoff,
|
||||||
|
Slow,
|
||||||
|
Slowoff,
|
||||||
|
Subscribers,
|
||||||
|
Subscribersoff,
|
||||||
|
Unraid,
|
||||||
|
/// json_rename=delete
|
||||||
|
DeleteMessage,
|
||||||
|
/// clangd currently "inherits" all future comments to all future enum constants
|
||||||
|
/// so after using something like json_rename we need to ensure it doesn't spread
|
||||||
|
Unvip,
|
||||||
|
Vip,
|
||||||
|
Raid,
|
||||||
|
AddBlockedTerm,
|
||||||
|
AddPermittedTerm,
|
||||||
|
RemoveBlockedTerm,
|
||||||
|
RemovePermittedTerm,
|
||||||
|
Mod,
|
||||||
|
Unmod,
|
||||||
|
ApproveUnbanRequest,
|
||||||
|
DenyUnbanRequest,
|
||||||
|
Warn,
|
||||||
|
SharedChatBan,
|
||||||
|
SharedChatTimeout,
|
||||||
|
SharedChatUnban,
|
||||||
|
SharedChatUntimeout,
|
||||||
|
SharedChatDelete,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Event {
|
||||||
|
/// User ID (e.g. 117166826) of the user who's channel the event took place in
|
||||||
|
String broadcasterUserID;
|
||||||
|
/// User Login (e.g. testaccount_420) of the user who's channel the event took place in
|
||||||
|
String broadcasterUserLogin;
|
||||||
|
/// User Name (e.g. 테스트계정420) of the user who's channel the event took place in
|
||||||
|
String broadcasterUserName;
|
||||||
|
|
||||||
|
/// For Shared Chat events, the user ID (e.g. 117166826) of the user who's channel the event took place in
|
||||||
|
std::optional<std::string> sourceBroadcasterUserID;
|
||||||
|
/// For Shared Chat events, the user Login (e.g. testaccount_420) of the user who's channel the event took place in
|
||||||
|
std::optional<std::string> sourceBroadcasterUserLogin;
|
||||||
|
/// For Shared Chat events, the user Name (e.g. 테스트계정420) of the user who's channel the event took place in
|
||||||
|
std::optional<std::string> sourceBroadcasterUserName;
|
||||||
|
|
||||||
|
/// User ID (e.g. 117166826) of the user who took the action
|
||||||
|
String moderatorUserID;
|
||||||
|
/// User Login (e.g. testaccount_420) of the user who took the action
|
||||||
|
String moderatorUserLogin;
|
||||||
|
/// User Name (e.g. 테스트계정420) of the user who took the action
|
||||||
|
String moderatorUserName;
|
||||||
|
|
||||||
|
// TODO: enum?
|
||||||
|
/// The action that took place (e.g. "warn" or "ban")
|
||||||
|
Action action;
|
||||||
|
|
||||||
|
std::optional<Followers> followers;
|
||||||
|
std::optional<Slow> slow;
|
||||||
|
std::optional<Vip> vip;
|
||||||
|
std::optional<Unvip> unvip;
|
||||||
|
std::optional<Unmod> unmod;
|
||||||
|
std::optional<Ban> ban;
|
||||||
|
std::optional<Unban> unban;
|
||||||
|
std::optional<Timeout> timeout;
|
||||||
|
std::optional<Untimeout> untimeout;
|
||||||
|
std::optional<Raid> raid;
|
||||||
|
std::optional<Unraid> unraid;
|
||||||
|
/// json_rename=delete
|
||||||
|
std::optional<Delete> deleteMessage;
|
||||||
|
std::optional<AutomodTerms> automodTerms;
|
||||||
|
std::optional<UnbanRequest> unbanRequest;
|
||||||
|
std::optional<Warn> warn;
|
||||||
|
std::optional<Ban> sharedChatBan;
|
||||||
|
std::optional<Unban> sharedChatUnban;
|
||||||
|
std::optional<Timeout> sharedChatTimeout;
|
||||||
|
std::optional<Untimeout> sharedChatUntimeout;
|
||||||
|
std::optional<Delete> sharedChatDelete;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Payload {
|
||||||
|
subscription::Subscription subscription;
|
||||||
|
|
||||||
|
Event event;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Action, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Action>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Followers, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Followers>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Slow, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Slow>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Vip, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Vip>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Unvip, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Unvip>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Mod, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Mod>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Unmod, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Unmod>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Ban, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Ban>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Unban, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Unban>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Timeout, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Timeout>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Untimeout, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Untimeout>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Raid, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Raid>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Unraid, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Unraid>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Delete, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Delete>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<AutomodTerms, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<AutomodTerms>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<UnbanRequest, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<UnbanRequest>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Warn, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Warn>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::channel_moderate::v2
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#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 {
|
||||||
|
// The broadcaster's user ID
|
||||||
|
const std::string broadcasterUserID;
|
||||||
|
// The broadcaster's user login
|
||||||
|
const std::string broadcasterUserLogin;
|
||||||
|
// The broadcaster's user display name
|
||||||
|
const std::string broadcasterUserName;
|
||||||
|
|
||||||
|
// The channel's stream title
|
||||||
|
const std::string title;
|
||||||
|
|
||||||
|
// The channel's broadcast language
|
||||||
|
const std::string language;
|
||||||
|
|
||||||
|
// The channels category ID
|
||||||
|
const std::string categoryID;
|
||||||
|
// The category name
|
||||||
|
const std::string categoryName;
|
||||||
|
|
||||||
|
// A boolean identifying whether the channel is flagged as mature
|
||||||
|
const bool isMature;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Payload {
|
||||||
|
const subscription::Subscription subscription;
|
||||||
|
|
||||||
|
const Event event;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::channel_update::v1
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/errors.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::session_welcome {
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"metadata": ...
|
||||||
|
"payload": {
|
||||||
|
"session": {
|
||||||
|
"id": "44f8cbce_c7ee958a",
|
||||||
|
"status": "connected",
|
||||||
|
"keepalive_timeout_seconds": 10,
|
||||||
|
"reconnect_url": null,
|
||||||
|
"connected_at": "2023-05-14T12:31:47.995262791Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_inner=session
|
||||||
|
struct Payload {
|
||||||
|
const std::string id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::session_welcome
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::stream_offline::v1 {
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Event {
|
||||||
|
// The broadcaster's user ID
|
||||||
|
const std::string broadcasterUserID;
|
||||||
|
// The broadcaster's user login
|
||||||
|
const std::string broadcasterUserLogin;
|
||||||
|
// The broadcaster's user display name
|
||||||
|
const std::string broadcasterUserName;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Payload {
|
||||||
|
const subscription::Subscription subscription;
|
||||||
|
|
||||||
|
const Event event;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::stream_offline::v1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::stream_online::v1 {
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Event {
|
||||||
|
// The ID of the stream
|
||||||
|
const std::string id;
|
||||||
|
|
||||||
|
// The broadcaster's user ID
|
||||||
|
const std::string broadcasterUserID;
|
||||||
|
// The broadcaster's user login
|
||||||
|
const std::string broadcasterUserLogin;
|
||||||
|
// The broadcaster's user display name
|
||||||
|
const std::string broadcasterUserName;
|
||||||
|
|
||||||
|
// The stream type (e.g. live, playlist, watch_party)
|
||||||
|
const std::string type;
|
||||||
|
|
||||||
|
// The timestamp at which the stream went online
|
||||||
|
// TODO: chronofy?
|
||||||
|
const std::string startedAt;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Payload {
|
||||||
|
const subscription::Subscription subscription;
|
||||||
|
|
||||||
|
const Event event;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::stream_online::v1
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "twitch-eventsub-ws/payloads/subscription.hpp"
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib::payload::subscription {
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"metadata": ...,
|
||||||
|
"payload": {
|
||||||
|
"subscription": {
|
||||||
|
"id": "4aa632e0-fca3-590b-e981-bbd12abdb3fe",
|
||||||
|
"status": "enabled",
|
||||||
|
"type": "channel.ban",
|
||||||
|
"version": "1",
|
||||||
|
"condition": {
|
||||||
|
"broadcaster_user_id": "74378979"
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"method": "websocket",
|
||||||
|
"session_id": "38de428e_b11f07be"
|
||||||
|
},
|
||||||
|
"created_at": "2023-05-20T12:30:55.518375571Z",
|
||||||
|
"cost": 0
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Transport {
|
||||||
|
const std::string method;
|
||||||
|
const std::string sessionID;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// json_transform=snake_case
|
||||||
|
struct Subscription {
|
||||||
|
const std::string id;
|
||||||
|
const std::string status;
|
||||||
|
const std::string type;
|
||||||
|
const std::string version;
|
||||||
|
|
||||||
|
// TODO: How do we map condition here? vector of key/value pairs?
|
||||||
|
|
||||||
|
const Transport transport;
|
||||||
|
|
||||||
|
// TODO: chronofy?
|
||||||
|
const std::string createdAt;
|
||||||
|
const int cost;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DESERIALIZATION DEFINITION START
|
||||||
|
boost::json::result_for<Transport, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Transport>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
boost::json::result_for<Subscription, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<Subscription>,
|
||||||
|
const boost::json::value &jvRoot);
|
||||||
|
// DESERIALIZATION DEFINITION END
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib::payload::subscription
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#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/json.hpp>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib {
|
||||||
|
|
||||||
|
class Listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handleMessage takes the incoming message in the buffer, parses it
|
||||||
|
* as JSON then forwards it to the listener, if applicable.
|
||||||
|
*
|
||||||
|
* This is called from the Session, and is only provided if you are interested
|
||||||
|
* in building your own boost asio framework thing
|
||||||
|
**/
|
||||||
|
boost::system::error_code handleMessage(
|
||||||
|
std::unique_ptr<Listener> &listener,
|
||||||
|
const boost::beast::flat_buffer &buffer);
|
||||||
|
|
||||||
|
// Sends a WebSocket message and prints the response
|
||||||
|
class Session : public std::enable_shared_from_this<Session>
|
||||||
|
{
|
||||||
|
boost::asio::ip::tcp::resolver resolver;
|
||||||
|
boost::beast::websocket::stream<
|
||||||
|
boost::beast::ssl_stream<boost::beast::tcp_stream>>
|
||||||
|
ws;
|
||||||
|
boost::beast::flat_buffer buffer;
|
||||||
|
std::string host;
|
||||||
|
std::string port;
|
||||||
|
std::string path;
|
||||||
|
std::string userAgent;
|
||||||
|
std::unique_ptr<Listener> listener;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Resolver and socket require an io_context
|
||||||
|
explicit Session(boost::asio::io_context &ioc,
|
||||||
|
boost::asio::ssl::context &ctx,
|
||||||
|
std::unique_ptr<Listener> listener);
|
||||||
|
|
||||||
|
// Start the asynchronous operation
|
||||||
|
void run(std::string _host, std::string _port, std::string _path,
|
||||||
|
std::string _userAgent);
|
||||||
|
|
||||||
|
void close();
|
||||||
|
|
||||||
|
Listener *getListener();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onResolve(boost::beast::error_code ec,
|
||||||
|
boost::asio::ip::tcp::resolver::results_type results);
|
||||||
|
|
||||||
|
void onConnect(
|
||||||
|
boost::beast::error_code ec,
|
||||||
|
boost::asio::ip::tcp::resolver::results_type::endpoint_type ep);
|
||||||
|
|
||||||
|
void onSSLHandshake(boost::beast::error_code ec);
|
||||||
|
|
||||||
|
void onHandshake(boost::beast::error_code ec);
|
||||||
|
|
||||||
|
void onRead(boost::beast::error_code ec, std::size_t bytes_transferred);
|
||||||
|
|
||||||
|
void onClose(boost::beast::error_code ec);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace chatterino::eventsub::lib {
|
||||||
|
|
||||||
|
template <class>
|
||||||
|
constexpr bool DEPENDENT_FALSE = false; // workaround before CWG2518/P2593R1
|
||||||
|
|
||||||
|
/// String is a struct that holds either an std::string or a QString
|
||||||
|
///
|
||||||
|
/// The intended use is for it to receive an std::string that has been built by
|
||||||
|
/// boost::json, and once it's been passed into a GUI appliciaton, it can use
|
||||||
|
/// the `qt` function to convert the backing string to a QString,
|
||||||
|
/// while we ensure the conversion is only done once.
|
||||||
|
struct String {
|
||||||
|
explicit String(std::string &&v)
|
||||||
|
: backingString(std::move(v))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~String() = default;
|
||||||
|
|
||||||
|
String(const String &s) = delete;
|
||||||
|
String(String &&s) = default;
|
||||||
|
|
||||||
|
String &operator=(const String &) = delete;
|
||||||
|
String &operator=(String &&) = default;
|
||||||
|
|
||||||
|
/// Returns the string as a QString, modifying the backing string to ensure
|
||||||
|
/// the copy only happens once.
|
||||||
|
QString qt() const
|
||||||
|
{
|
||||||
|
return std::visit(
|
||||||
|
[this](auto &&arg) -> QString {
|
||||||
|
using T = std::decay_t<std::remove_cvref_t<decltype(arg)>>;
|
||||||
|
if constexpr (std::is_same_v<T, std::string>)
|
||||||
|
{
|
||||||
|
auto qtString = QString::fromStdString(arg);
|
||||||
|
this->backingString = qtString;
|
||||||
|
return qtString;
|
||||||
|
}
|
||||||
|
else if constexpr (std::is_same_v<T, QString>)
|
||||||
|
{
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
static_assert(DEPENDENT_FALSE<T>,
|
||||||
|
"unknown type in variant");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this->backingString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::variant<std::string, QString> backingString;
|
||||||
|
};
|
||||||
|
|
||||||
|
boost::json::result_for<String, boost::json::value>::type tag_invoke(
|
||||||
|
boost::json::try_value_to_tag<String>, const boost::json::value &jvRoot);
|
||||||
|
|
||||||
|
} // namespace chatterino::eventsub::lib
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[tool.black]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
@@ -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
|
||||||
@@ -271,6 +271,13 @@ public:
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventsub::IController *getEventSub() override
|
||||||
|
{
|
||||||
|
assert(false && "EmptyApplication::getEventSub was called without "
|
||||||
|
"being initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
QTemporaryDir settingsDir;
|
QTemporaryDir settingsDir;
|
||||||
Paths paths_;
|
Paths paths_;
|
||||||
Args args_;
|
Args args_;
|
||||||
|
|||||||
@@ -427,6 +427,15 @@ public:
|
|||||||
FailureCallback<QString> failureCallback),
|
FailureCallback<QString> failureCallback),
|
||||||
(override));
|
(override));
|
||||||
|
|
||||||
|
MOCK_METHOD(void, createEventSubSubscription,
|
||||||
|
(const eventsub::SubscriptionRequest &request,
|
||||||
|
const QString &sessionID,
|
||||||
|
ResultCallback<HelixCreateEventSubSubscriptionResponse>
|
||||||
|
successCallback,
|
||||||
|
(FailureCallback<HelixCreateEventSubSubscriptionError, QString>
|
||||||
|
failureCallback)),
|
||||||
|
(override));
|
||||||
|
|
||||||
MOCK_METHOD(void, update, (QString clientId, QString oauthToken),
|
MOCK_METHOD(void, update, (QString clientId, QString oauthToken),
|
||||||
(override));
|
(override));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015, 2016, 2017 Howard Hinnant
|
||||||
|
Copyright (c) 2016 Adrian Colomitchi
|
||||||
|
Copyright (c) 2017 Florian Dang
|
||||||
|
Copyright (c) 2017 Paul Thompson
|
||||||
|
Copyright (c) 2018, 2019 Tomasz Kamiński
|
||||||
|
Copyright (c) 2019 Jiangang Zhuang
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
Our apologies. When the previous paragraph was written, lowercase had not yet
|
||||||
|
been invented (that would involve another several millennia of evolution).
|
||||||
|
We did not mean to shout.
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "providers/pronouns/Pronouns.hpp"
|
#include "providers/pronouns/Pronouns.hpp"
|
||||||
#include "providers/seventv/SeventvAPI.hpp"
|
#include "providers/seventv/SeventvAPI.hpp"
|
||||||
#include "providers/seventv/SeventvEmotes.hpp"
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
|
#include "providers/twitch/eventsub/Controller.hpp"
|
||||||
#include "providers/twitch/TwitchBadges.hpp"
|
#include "providers/twitch/TwitchBadges.hpp"
|
||||||
#include "singletons/ImageUploader.hpp"
|
#include "singletons/ImageUploader.hpp"
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
@@ -120,6 +121,18 @@ SeventvEventAPI *makeSeventvEventAPI(Settings &settings)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventsub::IController *makeEventSubController(Settings &settings)
|
||||||
|
{
|
||||||
|
bool enabled = settings.enableExperimentalEventSub;
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
return new eventsub::Controller();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new eventsub::DummyController();
|
||||||
|
}
|
||||||
|
|
||||||
const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv";
|
const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv";
|
||||||
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
@@ -180,6 +193,7 @@ Application::Application(Settings &_settings, const Paths &paths,
|
|||||||
, streamerMode(new StreamerMode)
|
, streamerMode(new StreamerMode)
|
||||||
, twitchUsers(new TwitchUsers)
|
, twitchUsers(new TwitchUsers)
|
||||||
, pronouns(new pronouns::Pronouns)
|
, pronouns(new pronouns::Pronouns)
|
||||||
|
, eventSub(makeEventSubController(_settings))
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
, plugins(new PluginController(paths))
|
, plugins(new PluginController(paths))
|
||||||
#endif
|
#endif
|
||||||
@@ -576,6 +590,14 @@ pronouns::Pronouns *Application::getPronouns()
|
|||||||
return this->pronouns.get();
|
return this->pronouns.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventsub::IController *Application::getEventSub()
|
||||||
|
{
|
||||||
|
assertInGuiThread();
|
||||||
|
assert(this->eventSub);
|
||||||
|
|
||||||
|
return this->eventSub.get();
|
||||||
|
}
|
||||||
|
|
||||||
void Application::save()
|
void Application::save()
|
||||||
{
|
{
|
||||||
this->commands->save();
|
this->commands->save();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user