diff --git a/.clang-tidy b/.clang-tidy index 434f04f7..7bee4c0b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -44,6 +44,11 @@ CheckOptions: - key: readability-identifier-naming.FunctionIgnoredRegexp value: ^(TEST|MOCK_METHOD)$ + - key: readability-identifier-naming.GlobalFunctionCase + value: camelBack + - key: readability-identifier-naming.GlobalFunctionIgnoredRegexp + value: ^(tag_invoke)$ + - key: readability-identifier-naming.MemberCase value: camelBack - key: readability-identifier-naming.PrivateMemberIgnoredRegexp diff --git a/.gitmodules b/.gitmodules index cfb74599..f5ea4a66 100644 --- a/.gitmodules +++ b/.gitmodules @@ -49,3 +49,6 @@ [submodule "lib/sol2"] path = lib/sol2 url = https://github.com/ThePhD/sol2.git +[submodule "lib/certify"] + path = lib/certify + url = https://github.com/Chatterino/certify diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e50441..8d97e2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Bugfix: Fixed announcements not showing up in mentions tab. (#5857) - Bugfix: Fixed the reply button showing for inline whispers and announcements. (#5863) - 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: Updated Conan dependencies. (#5776) - Dev: Disable QT keywords (i.e. `emit`, `slots`, and `signals`). (#5882) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad9bdeff..18cc7216 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,7 @@ find_package(PajladaSignals REQUIRED) find_package(LRUCache REQUIRED) find_package(MagicEnum REQUIRED) find_package(Doxygen) +find_package(BoostCertify REQUIRED) if (USE_SYSTEM_PAJLADA_SETTINGS) find_package(PajladaSettings REQUIRED) @@ -229,6 +230,8 @@ if (BUILD_WITH_CRASHPAD) add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler") 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 # 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 diff --git a/cmake/FindBoostCertify.cmake b/cmake/FindBoostCertify.cmake new file mode 100644 index 00000000..7dc04c3a --- /dev/null +++ b/cmake/FindBoostCertify.cmake @@ -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) diff --git a/lib/certify b/lib/certify new file mode 160000 index 00000000..a448a391 --- /dev/null +++ b/lib/certify @@ -0,0 +1 @@ +Subproject commit a448a3915ddac716ce76e4b8cbf0e7f4153ed1e2 diff --git a/lib/twitch-eventsub-ws/.gitignore b/lib/twitch-eventsub-ws/.gitignore new file mode 100644 index 00000000..c18dd8d8 --- /dev/null +++ b/lib/twitch-eventsub-ws/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/lib/twitch-eventsub-ws/.isort.cfg b/lib/twitch-eventsub-ws/.isort.cfg new file mode 100644 index 00000000..25287508 --- /dev/null +++ b/lib/twitch-eventsub-ws/.isort.cfg @@ -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 diff --git a/lib/twitch-eventsub-ws/.prettierignore b/lib/twitch-eventsub-ws/.prettierignore new file mode 100644 index 00000000..133b7bb5 --- /dev/null +++ b/lib/twitch-eventsub-ws/.prettierignore @@ -0,0 +1,5 @@ +.pytest_cache/ +venv/ +build/ + +compile_commands.json diff --git a/lib/twitch-eventsub-ws/CMakeLists.txt b/lib/twitch-eventsub-ws/CMakeLists.txt new file mode 100644 index 00000000..585c3a22 --- /dev/null +++ b/lib/twitch-eventsub-ws/CMakeLists.txt @@ -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) diff --git a/lib/twitch-eventsub-ws/README.md b/lib/twitch-eventsub-ws/README.md new file mode 100644 index 00000000..b77b4985 --- /dev/null +++ b/lib/twitch-eventsub-ws/README.md @@ -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 . +``` diff --git a/lib/twitch-eventsub-ws/TODO.md b/lib/twitch-eventsub-ws/TODO.md new file mode 100644 index 00000000..fafe4f65 --- /dev/null +++ b/lib/twitch-eventsub-ws/TODO.md @@ -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 diff --git a/lib/twitch-eventsub-ws/ast/.gitignore b/lib/twitch-eventsub-ws/ast/.gitignore new file mode 100644 index 00000000..24b7b056 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/.gitignore @@ -0,0 +1,2 @@ +/venv +/__pycache__ diff --git a/lib/twitch-eventsub-ws/ast/README.md b/lib/twitch-eventsub-ws/ast/README.md new file mode 100644 index 00000000..e381cdde --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/README.md @@ -0,0 +1,36 @@ +## Available scripts + +- `generate-and-replace-dir.py` + Usage: `./generate-and-replace-dir.py ` + 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 ` + Generates definitions & implementations for the given header file and write them to temporary files. + +- `replace-in-file.py` + Usage: `./replace-in-file.py ` + 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` diff --git a/lib/twitch-eventsub-ws/ast/generate-and-replace-dir.py b/lib/twitch-eventsub-ws/ast/generate-and-replace-dir.py new file mode 100755 index 00000000..36d9d976 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/generate-and-replace-dir.py @@ -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]} ") + 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() diff --git a/lib/twitch-eventsub-ws/ast/generate.py b/lib/twitch-eventsub-ws/ast/generate.py new file mode 100755 index 00000000..5caa089a --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/generate.py @@ -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]} ") + 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() diff --git a/lib/twitch-eventsub-ws/ast/get-builtin-include-dirs.py b/lib/twitch-eventsub-ws/ast/get-builtin-include-dirs.py new file mode 100755 index 00000000..907118e5 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/get-builtin-include-dirs.py @@ -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() diff --git a/lib/twitch-eventsub-ws/ast/lib/__init__.py b/lib/twitch-eventsub-ws/ast/lib/__init__.py new file mode 100644 index 00000000..27f95dcb --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/__init__.py @@ -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", +] diff --git a/lib/twitch-eventsub-ws/ast/lib/build.py b/lib/twitch-eventsub-ws/ast/lib/build.py new file mode 100644 index 00000000..eda39ea1 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/build.py @@ -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 diff --git a/lib/twitch-eventsub-ws/ast/lib/comment_commands.py b/lib/twitch-eventsub-ws/ast/lib/comment_commands.py new file mode 100644 index 00000000..ecd92d5b --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/comment_commands.py @@ -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"(?", input_str).lower() + case other: + log.warning(f"Unknown transformation '{other}', ignoring") + return input_str diff --git a/lib/twitch-eventsub-ws/ast/lib/enum.py b/lib/twitch-eventsub-ws/ast/lib/enum.py new file mode 100644 index 00000000..4b248579 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/enum.py @@ -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}") diff --git a/lib/twitch-eventsub-ws/ast/lib/enum_constant.py b/lib/twitch-eventsub-ws/ast/lib/enum_constant.py new file mode 100644 index 00000000..ddf74922 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/enum_constant.py @@ -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}" diff --git a/lib/twitch-eventsub-ws/ast/lib/format.py b/lib/twitch-eventsub-ws/ast/lib/format.py new file mode 100644 index 00000000..0aba499e --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/format.py @@ -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() diff --git a/lib/twitch-eventsub-ws/ast/lib/generate.py b/lib/twitch-eventsub-ws/ast/lib/generate.py new file mode 100644 index 00000000..c5a291d2 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/generate.py @@ -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) diff --git a/lib/twitch-eventsub-ws/ast/lib/helpers.py b/lib/twitch-eventsub-ws/ast/lib/helpers.py new file mode 100644 index 00000000..8dfd4e98 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/helpers.py @@ -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) diff --git a/lib/twitch-eventsub-ws/ast/lib/jinja_env.py b/lib/twitch-eventsub-ws/ast/lib/jinja_env.py new file mode 100644 index 00000000..1c605a3d --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/jinja_env.py @@ -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 diff --git a/lib/twitch-eventsub-ws/ast/lib/logging.py b/lib/twitch-eventsub-ws/ast/lib/logging.py new file mode 100644 index 00000000..338d168d --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/logging.py @@ -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) diff --git a/lib/twitch-eventsub-ws/ast/lib/member.py b/lib/twitch-eventsub-ws/ast/lib/member.py new file mode 100644 index 00000000..065cc057 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/member.py @@ -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> {self.name}" diff --git a/lib/twitch-eventsub-ws/ast/lib/membertype.py b/lib/twitch-eventsub-ws/ast/lib/membertype.py new file mode 100644 index 00000000..cf376063 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/membertype.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class MemberType(Enum): + BASIC = 1 + VECTOR = 2 + OPTIONAL = 3 + OPTIONAL_VECTOR = 4 diff --git a/lib/twitch-eventsub-ws/ast/lib/replace.py b/lib/twitch-eventsub-ws/ast/lib/replace.py new file mode 100644 index 00000000..9b02a5af --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/replace.py @@ -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) diff --git a/lib/twitch-eventsub-ws/ast/lib/struct.py b/lib/twitch-eventsub-ws/ast/lib/struct.py new file mode 100644 index 00000000..894384c6 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/struct.py @@ -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}") diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/enum-definition.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/enum-definition.tmpl new file mode 100644 index 00000000..a7bf90ff --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/enum-definition.tmpl @@ -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); diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/enum-implementation.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/enum-implementation.tmpl new file mode 100644 index 00000000..2d2f6378 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/enum-implementation.tmpl @@ -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}; +} diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/error-failed-to-deserialize.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/error-failed-to-deserialize.tmpl new file mode 100644 index 00000000..2d573ebf --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/error-failed-to-deserialize.tmpl @@ -0,0 +1 @@ +return {{field.name}}.error(); diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/error-missing-field.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/error-missing-field.tmpl new file mode 100644 index 00000000..a92d3e8e --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/error-missing-field.tmpl @@ -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}}}; diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/field-basic.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/field-basic.tmpl new file mode 100644 index 00000000..2f7495af --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/field-basic.tmpl @@ -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 %} +} + diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/field-optional-vector.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/field-optional-vector.tmpl new file mode 100644 index 00000000..362fd5b8 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/field-optional-vector.tmpl @@ -0,0 +1 @@ +assert(false && "OPTIONAL VECTOR FIELD UNIMPLEMENTED"); diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/field-optional.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/field-optional.tmpl new file mode 100644 index 00000000..429e6a7a --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/field-optional.tmpl @@ -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 %} +} + diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/field-vector.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/field-vector.tmpl new file mode 100644 index 00000000..eb710f2c --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/field-vector.tmpl @@ -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>(*jv{{field.name}}); +if ({{field.name}}.has_error()) +{ + {% include 'error-failed-to-deserialize.tmpl' indent content %} +} + diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/initializer-basic.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-basic.tmpl new file mode 100644 index 00000000..bae60582 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-basic.tmpl @@ -0,0 +1 @@ +.{{field.name}} = std::move({{field.name}}.value()), diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/initializer-optional-vector.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-optional-vector.tmpl new file mode 100644 index 00000000..8037bf16 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-optional-vector.tmpl @@ -0,0 +1 @@ +assert(false && "OPTIONAL VECTOR INITIALIZER UNIMPLEMENTED"); diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/initializer-optional.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-optional.tmpl new file mode 100644 index 00000000..8964263a --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-optional.tmpl @@ -0,0 +1 @@ +.{{field.name}} = std::move({{field.name}}), diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/initializer-vector.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-vector.tmpl new file mode 100644 index 00000000..fef6c660 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-vector.tmpl @@ -0,0 +1 @@ +.{{field.name}} = {{field.name}}.value(), diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/struct-definition.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/struct-definition.tmpl new file mode 100644 index 00000000..73403c5c --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/struct-definition.tmpl @@ -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); diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/struct-implementation.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/struct-implementation.tmpl new file mode 100644 index 00000000..a756a944 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/templates/struct-implementation.tmpl @@ -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 %} + }; +} diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/__init__.py b/lib/twitch-eventsub-ws/ast/lib/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/resources/const.hpp b/lib/twitch-eventsub-ws/ast/lib/tests/resources/const.hpp new file mode 100644 index 00000000..d61e4911 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/resources/const.hpp @@ -0,0 +1,14 @@ +#include +#include + +struct Pod { +}; + +struct Const { + const int a; + const bool b; + const char c; + const Pod d; + const std::vector e; + const std::optional f; +}; diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/resources/optional.hpp b/lib/twitch-eventsub-ws/ast/lib/tests/resources/optional.hpp new file mode 100644 index 00000000..b1ecf8d9 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/resources/optional.hpp @@ -0,0 +1,5 @@ +#include + +struct Optional { + std::optional a; +}; diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/resources/simple.hpp b/lib/twitch-eventsub-ws/ast/lib/tests/resources/simple.hpp new file mode 100644 index 00000000..648b657d --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/resources/simple.hpp @@ -0,0 +1,5 @@ +struct Simple { + int a; + bool b; + char c; +}; diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/resources/string.hpp b/lib/twitch-eventsub-ws/ast/lib/tests/resources/string.hpp new file mode 100644 index 00000000..384f0b9e --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/resources/string.hpp @@ -0,0 +1,10 @@ +#include +#include +#include + +struct String { + std::string a; + const std::string b; + std::vector c; + std::optional d; +}; diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/resources/test.hpp b/lib/twitch-eventsub-ws/ast/lib/tests/resources/test.hpp new file mode 100644 index 00000000..7fffdfcf --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/resources/test.hpp @@ -0,0 +1,27 @@ +#include +#include + +struct Foo { + struct InnerFoo { + int asd; + }; + + int test; +}; + +struct S { + Foo a; + std::vector as; + /** + * 4 + * 5 + * json_rename=vec_of_ints + **/ + std::vector vecOfInts; + std::optional ab; + /** + * json_dont_fail_on_deserialization=True + **/ + std::optional ac; + int b; +}; diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/resources/vector-pod.hpp b/lib/twitch-eventsub-ws/ast/lib/tests/resources/vector-pod.hpp new file mode 100644 index 00000000..56ed4c32 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/resources/vector-pod.hpp @@ -0,0 +1,11 @@ +#include + +struct Pod { + int a; + bool b; + char c; +}; + +struct VectorPod { + std::vector a; +}; diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/resources/vector.hpp b/lib/twitch-eventsub-ws/ast/lib/tests/resources/vector.hpp new file mode 100644 index 00000000..12efc9f0 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/resources/vector.hpp @@ -0,0 +1,6 @@ +#include + +struct Vector { + std::vector a; + std::vector> b; +}; diff --git a/lib/twitch-eventsub-ws/ast/lib/tests/test_build_structs.py b/lib/twitch-eventsub-ws/ast/lib/tests/test_build_structs.py new file mode 100644 index 00000000..de387ada --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/tests/test_build_structs.py @@ -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" + + +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() diff --git a/lib/twitch-eventsub-ws/ast/lib/walker.py b/lib/twitch-eventsub-ws/ast/lib/walker.py new file mode 100644 index 00000000..3a3b52a5 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/lib/walker.py @@ -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) diff --git a/lib/twitch-eventsub-ws/ast/replace-in-file.py b/lib/twitch-eventsub-ws/ast/replace-in-file.py new file mode 100755 index 00000000..e2a9259c --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/replace-in-file.py @@ -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]} ") + 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() diff --git a/lib/twitch-eventsub-ws/ast/requirements-dev.txt b/lib/twitch-eventsub-ws/ast/requirements-dev.txt new file mode 100644 index 00000000..e95b7aca --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/requirements-dev.txt @@ -0,0 +1,4 @@ +black==23.3.0 +flake8==6.0.0 +mypy==1.3.0 +isort==5.12.0 diff --git a/lib/twitch-eventsub-ws/ast/requirements.txt b/lib/twitch-eventsub-ws/ast/requirements.txt new file mode 100644 index 00000000..a3e94456 --- /dev/null +++ b/lib/twitch-eventsub-ws/ast/requirements.txt @@ -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 diff --git a/lib/twitch-eventsub-ws/cmake/GenerateJson.cmake b/lib/twitch-eventsub-ws/cmake/GenerateJson.cmake new file mode 100644 index 00000000..bf6b2078 --- /dev/null +++ b/lib/twitch-eventsub-ws/cmake/GenerateJson.cmake @@ -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() diff --git a/lib/twitch-eventsub-ws/example/CMakeLists.txt b/lib/twitch-eventsub-ws/example/CMakeLists.txt new file mode 100644 index 00000000..34f1b387 --- /dev/null +++ b/lib/twitch-eventsub-ws/example/CMakeLists.txt @@ -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 () diff --git a/lib/twitch-eventsub-ws/example/main.cpp b/lib/twitch-eventsub-ws/example/main.cpp new file mode 100644 index 00000000..338d4fe6 --- /dev/null +++ b/lib/twitch-eventsub-ws/example/main.cpp @@ -0,0 +1,152 @@ +#include "twitch-eventsub-ws/listener.hpp" +#include "twitch-eventsub-ws/session.hpp" +#include "twitch-eventsub-ws/string.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +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(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(ctx, sslContext, + std::make_unique()) + ->run(host, port, path, userAgent); + + ctx.run(); + } + catch (std::exception &e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } +} diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/chrono.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/chrono.hpp new file mode 100644 index 00000000..4d13f7c3 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/chrono.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include +#include + +namespace chatterino::eventsub::lib { + +struct AsISO8601 { +}; + +boost::json::result_for::type + tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot, const AsISO8601 &); + +} // namespace chatterino::eventsub::lib diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/date.h b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/date.h new file mode 100644 index 00000000..6960e8cd --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/date.h @@ -0,0 +1,8234 @@ +#ifndef DATE_H +#define DATE_H + +// 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. + +#ifndef HAS_STRING_VIEW +# if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +# define HAS_STRING_VIEW 1 +# else +# define HAS_STRING_VIEW 0 +# endif +#endif // HAS_STRING_VIEW + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if HAS_STRING_VIEW +# include +#endif +#include +#include + +#ifdef __GNUC__ +# pragma GCC diagnostic push +# if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 7) +# pragma GCC diagnostic ignored "-Wpedantic" +# endif +# if __GNUC__ < 5 + // GCC 4.9 Bug 61489 Wrong warning with -Wmissing-field-initializers +# pragma GCC diagnostic ignored "-Wmissing-field-initializers" +# endif +#endif + +#ifdef _MSC_VER +# pragma warning(push) +// warning C4127: conditional expression is constant +# pragma warning(disable : 4127) +#endif + +namespace date +{ + +//---------------+ +// Configuration | +//---------------+ + +#ifndef ONLY_C_LOCALE +# define ONLY_C_LOCALE 0 +#endif + +#if defined(_MSC_VER) && (!defined(__clang__) || (_MSC_VER < 1910)) +// MSVC +# ifndef _SILENCE_CXX17_UNCAUGHT_EXCEPTION_DEPRECATION_WARNING +# define _SILENCE_CXX17_UNCAUGHT_EXCEPTION_DEPRECATION_WARNING +# endif +# if _MSC_VER < 1910 +// before VS2017 +# define CONSTDATA const +# define CONSTCD11 +# define CONSTCD14 +# define NOEXCEPT _NOEXCEPT +# else +// VS2017 and later +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 constexpr +# define NOEXCEPT noexcept +# endif + +#elif defined(__SUNPRO_CC) && __SUNPRO_CC <= 0x5150 +// Oracle Developer Studio 12.6 and earlier +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 +# define NOEXCEPT noexcept + +#elif __cplusplus >= 201402 +// C++14 +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 constexpr +# define NOEXCEPT noexcept +#else +// C++11 +# define CONSTDATA constexpr const +# define CONSTCD11 constexpr +# define CONSTCD14 +# define NOEXCEPT noexcept +#endif + +#ifndef HAS_UNCAUGHT_EXCEPTIONS +# if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +# define HAS_UNCAUGHT_EXCEPTIONS 1 +# else +# define HAS_UNCAUGHT_EXCEPTIONS 0 +# endif +#endif // HAS_UNCAUGHT_EXCEPTIONS + +#ifndef HAS_VOID_T +# if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +# define HAS_VOID_T 1 +# else +# define HAS_VOID_T 0 +# endif +#endif // HAS_VOID_T + +// Protect from Oracle sun macro +#ifdef sun +# undef sun +#endif + +// Work around for a NVCC compiler bug which causes it to fail +// to compile std::ratio_{multiply,divide} when used directly +// in the std::chrono::duration template instantiations below +namespace detail { +template +using ratio_multiply = decltype(std::ratio_multiply{}); + +template +using ratio_divide = decltype(std::ratio_divide{}); +} // namespace detail + +//-----------+ +// Interface | +//-----------+ + +// durations + +using days = std::chrono::duration + , std::chrono::hours::period>>; + +using weeks = std::chrono::duration + , days::period>>; + +using years = std::chrono::duration + , days::period>>; + +using months = std::chrono::duration + >>; + +// time_point + +template + using sys_time = std::chrono::time_point; + +using sys_days = sys_time; +using sys_seconds = sys_time; + +struct local_t {}; + +template + using local_time = std::chrono::time_point; + +using local_seconds = local_time; +using local_days = local_time; + +// types + +struct last_spec +{ + explicit last_spec() = default; +}; + +class day; +class month; +class year; + +class weekday; +class weekday_indexed; +class weekday_last; + +class month_day; +class month_day_last; +class month_weekday; +class month_weekday_last; + +class year_month; + +class year_month_day; +class year_month_day_last; +class year_month_weekday; +class year_month_weekday_last; + +// date composition operators + +CONSTCD11 year_month operator/(const year& y, const month& m) NOEXCEPT; +CONSTCD11 year_month operator/(const year& y, int m) NOEXCEPT; + +CONSTCD11 month_day operator/(const day& d, const month& m) NOEXCEPT; +CONSTCD11 month_day operator/(const day& d, int m) NOEXCEPT; +CONSTCD11 month_day operator/(const month& m, const day& d) NOEXCEPT; +CONSTCD11 month_day operator/(const month& m, int d) NOEXCEPT; +CONSTCD11 month_day operator/(int m, const day& d) NOEXCEPT; + +CONSTCD11 month_day_last operator/(const month& m, last_spec) NOEXCEPT; +CONSTCD11 month_day_last operator/(int m, last_spec) NOEXCEPT; +CONSTCD11 month_day_last operator/(last_spec, const month& m) NOEXCEPT; +CONSTCD11 month_day_last operator/(last_spec, int m) NOEXCEPT; + +CONSTCD11 month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT; +CONSTCD11 month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT; +CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT; +CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT; + +CONSTCD11 month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT; +CONSTCD11 month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT; +CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT; +CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT; + +CONSTCD11 year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT; +CONSTCD11 year_month_day operator/(const year_month& ym, int d) NOEXCEPT; +CONSTCD11 year_month_day operator/(const year& y, const month_day& md) NOEXCEPT; +CONSTCD11 year_month_day operator/(int y, const month_day& md) NOEXCEPT; +CONSTCD11 year_month_day operator/(const month_day& md, const year& y) NOEXCEPT; +CONSTCD11 year_month_day operator/(const month_day& md, int y) NOEXCEPT; + +CONSTCD11 + year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT; +CONSTCD11 + year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const year& y, const month_weekday& mwd) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(int y, const month_weekday& mwd) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const month_weekday& mwd, const year& y) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator/(const month_weekday& mwd, int y) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(int y, const month_weekday_last& mwdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator/(const month_weekday_last& mwdl, int y) NOEXCEPT; + +// Detailed interface + +// day + +class day +{ + unsigned char d_; + +public: + day() = default; + explicit CONSTCD11 day(unsigned d) NOEXCEPT; + + CONSTCD14 day& operator++() NOEXCEPT; + CONSTCD14 day operator++(int) NOEXCEPT; + CONSTCD14 day& operator--() NOEXCEPT; + CONSTCD14 day operator--(int) NOEXCEPT; + + CONSTCD14 day& operator+=(const days& d) NOEXCEPT; + CONSTCD14 day& operator-=(const days& d) NOEXCEPT; + + CONSTCD11 explicit operator unsigned() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator!=(const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator< (const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator> (const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator<=(const day& x, const day& y) NOEXCEPT; +CONSTCD11 bool operator>=(const day& x, const day& y) NOEXCEPT; + +CONSTCD11 day operator+(const day& x, const days& y) NOEXCEPT; +CONSTCD11 day operator+(const days& x, const day& y) NOEXCEPT; +CONSTCD11 day operator-(const day& x, const days& y) NOEXCEPT; +CONSTCD11 days operator-(const day& x, const day& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const day& d); + +// month + +class month +{ + unsigned char m_; + +public: + month() = default; + explicit CONSTCD11 month(unsigned m) NOEXCEPT; + + CONSTCD14 month& operator++() NOEXCEPT; + CONSTCD14 month operator++(int) NOEXCEPT; + CONSTCD14 month& operator--() NOEXCEPT; + CONSTCD14 month operator--(int) NOEXCEPT; + + CONSTCD14 month& operator+=(const months& m) NOEXCEPT; + CONSTCD14 month& operator-=(const months& m) NOEXCEPT; + + CONSTCD11 explicit operator unsigned() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator< (const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator> (const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator<=(const month& x, const month& y) NOEXCEPT; +CONSTCD11 bool operator>=(const month& x, const month& y) NOEXCEPT; + +CONSTCD14 month operator+(const month& x, const months& y) NOEXCEPT; +CONSTCD14 month operator+(const months& x, const month& y) NOEXCEPT; +CONSTCD14 month operator-(const month& x, const months& y) NOEXCEPT; +CONSTCD14 months operator-(const month& x, const month& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month& m); + +// year + +class year +{ + short y_; + +public: + year() = default; + explicit CONSTCD11 year(int y) NOEXCEPT; + + CONSTCD14 year& operator++() NOEXCEPT; + CONSTCD14 year operator++(int) NOEXCEPT; + CONSTCD14 year& operator--() NOEXCEPT; + CONSTCD14 year operator--(int) NOEXCEPT; + + CONSTCD14 year& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 year operator-() const NOEXCEPT; + CONSTCD11 year operator+() const NOEXCEPT; + + CONSTCD11 bool is_leap() const NOEXCEPT; + + CONSTCD11 explicit operator int() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; + + static CONSTCD11 year min() NOEXCEPT { return year{-32767}; } + static CONSTCD11 year max() NOEXCEPT { return year{32767}; } +}; + +CONSTCD11 bool operator==(const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator!=(const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator< (const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator> (const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator<=(const year& x, const year& y) NOEXCEPT; +CONSTCD11 bool operator>=(const year& x, const year& y) NOEXCEPT; + +CONSTCD11 year operator+(const year& x, const years& y) NOEXCEPT; +CONSTCD11 year operator+(const years& x, const year& y) NOEXCEPT; +CONSTCD11 year operator-(const year& x, const years& y) NOEXCEPT; +CONSTCD11 years operator-(const year& x, const year& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year& y); + +// weekday + +class weekday +{ + unsigned char wd_; +public: + weekday() = default; + explicit CONSTCD11 weekday(unsigned wd) NOEXCEPT; + CONSTCD14 weekday(const sys_days& dp) NOEXCEPT; + CONSTCD14 explicit weekday(const local_days& dp) NOEXCEPT; + + CONSTCD14 weekday& operator++() NOEXCEPT; + CONSTCD14 weekday operator++(int) NOEXCEPT; + CONSTCD14 weekday& operator--() NOEXCEPT; + CONSTCD14 weekday operator--(int) NOEXCEPT; + + CONSTCD14 weekday& operator+=(const days& d) NOEXCEPT; + CONSTCD14 weekday& operator-=(const days& d) NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; + + CONSTCD11 unsigned c_encoding() const NOEXCEPT; + CONSTCD11 unsigned iso_encoding() const NOEXCEPT; + + CONSTCD11 weekday_indexed operator[](unsigned index) const NOEXCEPT; + CONSTCD11 weekday_last operator[](last_spec) const NOEXCEPT; + +private: + static CONSTCD14 unsigned char weekday_from_days(int z) NOEXCEPT; + + friend CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; + friend CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; + friend CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; + template + friend std::basic_ostream& + operator<<(std::basic_ostream& os, const weekday& wd); + friend class weekday_indexed; +}; + +CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; +CONSTCD11 bool operator!=(const weekday& x, const weekday& y) NOEXCEPT; + +CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; +CONSTCD14 weekday operator+(const days& x, const weekday& y) NOEXCEPT; +CONSTCD14 weekday operator-(const weekday& x, const days& y) NOEXCEPT; +CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday& wd); + +// weekday_indexed + +class weekday_indexed +{ + unsigned char wd_ : 4; + unsigned char index_ : 4; + +public: + weekday_indexed() = default; + CONSTCD11 weekday_indexed(const date::weekday& wd, unsigned index) NOEXCEPT; + + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 unsigned index() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; +CONSTCD11 bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_indexed& wdi); + +// weekday_last + +class weekday_last +{ + date::weekday wd_; + +public: + explicit CONSTCD11 weekday_last(const date::weekday& wd) NOEXCEPT; + + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT; +CONSTCD11 bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_last& wdl); + +namespace detail +{ + +struct unspecified_month_disambiguator {}; + +} // namespace detail + +// year_month + +class year_month +{ + date::year y_; + date::month m_; + +public: + year_month() = default; + CONSTCD11 year_month(const date::year& y, const date::month& m) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + + template + CONSTCD14 year_month& operator+=(const months& dm) NOEXCEPT; + template + CONSTCD14 year_month& operator-=(const months& dm) NOEXCEPT; + CONSTCD14 year_month& operator+=(const years& dy) NOEXCEPT; + CONSTCD14 year_month& operator-=(const years& dy) NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator!=(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator< (const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator> (const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator<=(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 bool operator>=(const year_month& x, const year_month& y) NOEXCEPT; + +template +CONSTCD14 year_month operator+(const year_month& ym, const months& dm) NOEXCEPT; +template +CONSTCD14 year_month operator+(const months& dm, const year_month& ym) NOEXCEPT; +template +CONSTCD14 year_month operator-(const year_month& ym, const months& dm) NOEXCEPT; + +CONSTCD11 months operator-(const year_month& x, const year_month& y) NOEXCEPT; +CONSTCD11 year_month operator+(const year_month& ym, const years& dy) NOEXCEPT; +CONSTCD11 year_month operator+(const years& dy, const year_month& ym) NOEXCEPT; +CONSTCD11 year_month operator-(const year_month& ym, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month& ym); + +// month_day + +class month_day +{ + date::month m_; + date::day d_; + +public: + month_day() = default; + CONSTCD11 month_day(const date::month& m, const date::day& d) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::day day() const NOEXCEPT; + + CONSTCD14 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator< (const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator> (const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator<=(const month_day& x, const month_day& y) NOEXCEPT; +CONSTCD11 bool operator>=(const month_day& x, const month_day& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day& md); + +// month_day_last + +class month_day_last +{ + date::month m_; + +public: + CONSTCD11 explicit month_day_last(const date::month& m) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator< (const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator> (const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT; +CONSTCD11 bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day_last& mdl); + +// month_weekday + +class month_weekday +{ + date::month m_; + date::weekday_indexed wdi_; +public: + CONSTCD11 month_weekday(const date::month& m, + const date::weekday_indexed& wdi) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday_indexed weekday_indexed() const NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT; +CONSTCD11 bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday& mwd); + +// month_weekday_last + +class month_weekday_last +{ + date::month m_; + date::weekday_last wdl_; + +public: + CONSTCD11 month_weekday_last(const date::month& m, + const date::weekday_last& wd) NOEXCEPT; + + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday_last weekday_last() const NOEXCEPT; + + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 + bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; +CONSTCD11 + bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday_last& mwdl); + +// class year_month_day + +class year_month_day +{ + date::year y_; + date::month m_; + date::day d_; + +public: + year_month_day() = default; + CONSTCD11 year_month_day(const date::year& y, const date::month& m, + const date::day& d) NOEXCEPT; + CONSTCD14 year_month_day(const year_month_day_last& ymdl) NOEXCEPT; + + CONSTCD14 year_month_day(sys_days dp) NOEXCEPT; + CONSTCD14 explicit year_month_day(local_days dp) NOEXCEPT; + + template + CONSTCD14 year_month_day& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_day& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_day& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_day& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::day day() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD14 bool ok() const NOEXCEPT; + +private: + static CONSTCD14 year_month_day from_days(days dp) NOEXCEPT; + CONSTCD14 days to_days() const NOEXCEPT; +}; + +CONSTCD11 bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator< (const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator> (const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT; +CONSTCD11 bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT; + +template +CONSTCD14 year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT; +template +CONSTCD14 year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT; +template +CONSTCD14 year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT; +CONSTCD11 year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT; +CONSTCD11 year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT; +CONSTCD11 year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day& ymd); + +// year_month_day_last + +class year_month_day_last +{ + date::year y_; + date::month_day_last mdl_; + +public: + CONSTCD11 year_month_day_last(const date::year& y, + const date::month_day_last& mdl) NOEXCEPT; + + template + CONSTCD14 year_month_day_last& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_day_last& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_day_last& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_day_last& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::month_day_last month_day_last() const NOEXCEPT; + CONSTCD14 date::day day() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; +}; + +CONSTCD11 + bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator< (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator> (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; +CONSTCD11 + bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; + +template +CONSTCD14 +year_month_day_last +operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; + +template +CONSTCD14 +year_month_day_last +operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT; + +CONSTCD11 +year_month_day_last +operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; + +CONSTCD11 +year_month_day_last +operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT; + +template +CONSTCD14 +year_month_day_last +operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; + +CONSTCD11 +year_month_day_last +operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day_last& ymdl); + +// year_month_weekday + +class year_month_weekday +{ + date::year y_; + date::month m_; + date::weekday_indexed wdi_; + +public: + year_month_weekday() = default; + CONSTCD11 year_month_weekday(const date::year& y, const date::month& m, + const date::weekday_indexed& wdi) NOEXCEPT; + CONSTCD14 year_month_weekday(const sys_days& dp) NOEXCEPT; + CONSTCD14 explicit year_month_weekday(const local_days& dp) NOEXCEPT; + + template + CONSTCD14 year_month_weekday& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_weekday& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_weekday& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_weekday& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 unsigned index() const NOEXCEPT; + CONSTCD11 date::weekday_indexed weekday_indexed() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD14 bool ok() const NOEXCEPT; + +private: + static CONSTCD14 year_month_weekday from_days(days dp) NOEXCEPT; + CONSTCD14 days to_days() const NOEXCEPT; +}; + +CONSTCD11 + bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; +CONSTCD11 + bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday +operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday +operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday +operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; + +CONSTCD11 +year_month_weekday +operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi); + +// year_month_weekday_last + +class year_month_weekday_last +{ + date::year y_; + date::month m_; + date::weekday_last wdl_; + +public: + CONSTCD11 year_month_weekday_last(const date::year& y, const date::month& m, + const date::weekday_last& wdl) NOEXCEPT; + + template + CONSTCD14 year_month_weekday_last& operator+=(const months& m) NOEXCEPT; + template + CONSTCD14 year_month_weekday_last& operator-=(const months& m) NOEXCEPT; + CONSTCD14 year_month_weekday_last& operator+=(const years& y) NOEXCEPT; + CONSTCD14 year_month_weekday_last& operator-=(const years& y) NOEXCEPT; + + CONSTCD11 date::year year() const NOEXCEPT; + CONSTCD11 date::month month() const NOEXCEPT; + CONSTCD11 date::weekday weekday() const NOEXCEPT; + CONSTCD11 date::weekday_last weekday_last() const NOEXCEPT; + + CONSTCD14 operator sys_days() const NOEXCEPT; + CONSTCD14 explicit operator local_days() const NOEXCEPT; + CONSTCD11 bool ok() const NOEXCEPT; + +private: + CONSTCD14 days to_days() const NOEXCEPT; +}; + +CONSTCD11 +bool +operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; + +CONSTCD11 +bool +operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday_last +operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT; + +template +CONSTCD14 +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; + +CONSTCD11 +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; + +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl); + +#if !defined(_MSC_VER) || (_MSC_VER >= 1900) +inline namespace literals +{ + +CONSTCD11 date::day operator "" _d(unsigned long long d) NOEXCEPT; +CONSTCD11 date::year operator "" _y(unsigned long long y) NOEXCEPT; + +} // inline namespace literals +#endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) + +// CONSTDATA date::month January{1}; +// CONSTDATA date::month February{2}; +// CONSTDATA date::month March{3}; +// CONSTDATA date::month April{4}; +// CONSTDATA date::month May{5}; +// CONSTDATA date::month June{6}; +// CONSTDATA date::month July{7}; +// CONSTDATA date::month August{8}; +// CONSTDATA date::month September{9}; +// CONSTDATA date::month October{10}; +// CONSTDATA date::month November{11}; +// CONSTDATA date::month December{12}; +// +// CONSTDATA date::weekday Sunday{0u}; +// CONSTDATA date::weekday Monday{1u}; +// CONSTDATA date::weekday Tuesday{2u}; +// CONSTDATA date::weekday Wednesday{3u}; +// CONSTDATA date::weekday Thursday{4u}; +// CONSTDATA date::weekday Friday{5u}; +// CONSTDATA date::weekday Saturday{6u}; + +#if HAS_VOID_T + +template > +struct is_clock + : std::false_type +{}; + +template +struct is_clock> + : std::true_type +{}; + +template inline constexpr bool is_clock_v = is_clock::value; + +#endif // HAS_VOID_T + +//----------------+ +// Implementation | +//----------------+ + +// utilities +namespace detail { + +template> +class save_istream +{ +protected: + std::basic_ios& is_; + CharT fill_; + std::ios::fmtflags flags_; + std::streamsize precision_; + std::streamsize width_; + std::basic_ostream* tie_; + std::locale loc_; + +public: + ~save_istream() + { + is_.fill(fill_); + is_.flags(flags_); + is_.precision(precision_); + is_.width(width_); + is_.imbue(loc_); + is_.tie(tie_); + } + + save_istream(const save_istream&) = delete; + save_istream& operator=(const save_istream&) = delete; + + explicit save_istream(std::basic_ios& is) + : is_(is) + , fill_(is.fill()) + , flags_(is.flags()) + , precision_(is.precision()) + , width_(is.width(0)) + , tie_(is.tie(nullptr)) + , loc_(is.getloc()) + { + if (tie_ != nullptr) + tie_->flush(); + } +}; + +template> +class save_ostream + : private save_istream +{ +public: + ~save_ostream() + { + if ((this->flags_ & std::ios::unitbuf) && +#if HAS_UNCAUGHT_EXCEPTIONS + std::uncaught_exceptions() == 0 && +#else + !std::uncaught_exception() && +#endif + this->is_.good()) + this->is_.rdbuf()->pubsync(); + } + + save_ostream(const save_ostream&) = delete; + save_ostream& operator=(const save_ostream&) = delete; + + explicit save_ostream(std::basic_ios& os) + : save_istream(os) + { + } +}; + +template +struct choose_trunc_type +{ + static const int digits = std::numeric_limits::digits; + using type = typename std::conditional + < + digits < 32, + std::int32_t, + typename std::conditional + < + digits < 64, + std::int64_t, +#ifdef __SIZEOF_INT128__ + __int128 +#else + std::int64_t +#endif + >::type + >::type; +}; + +template +CONSTCD11 +inline +typename std::enable_if +< + !std::chrono::treat_as_floating_point::value, + T +>::type +trunc(T t) NOEXCEPT +{ + return t; +} + +template +CONSTCD14 +inline +typename std::enable_if +< + std::chrono::treat_as_floating_point::value, + T +>::type +trunc(T t) NOEXCEPT +{ + using std::numeric_limits; + using I = typename choose_trunc_type::type; + CONSTDATA auto digits = numeric_limits::digits; + static_assert(digits < numeric_limits::digits, ""); + CONSTDATA auto max = I{1} << (digits-1); + CONSTDATA auto min = -max; + const auto negative = t < T{0}; + if (min <= t && t <= max && t != 0 && t == t) + { + t = static_cast(static_cast(t)); + if (t == 0 && negative) + t = -t; + } + return t; +} + +template +struct static_gcd +{ + static const std::intmax_t value = static_gcd::value; +}; + +template +struct static_gcd +{ + static const std::intmax_t value = Xp; +}; + +template <> +struct static_gcd<0, 0> +{ + static const std::intmax_t value = 1; +}; + +template +struct no_overflow +{ +private: + static const std::intmax_t gcd_n1_n2 = static_gcd::value; + static const std::intmax_t gcd_d1_d2 = static_gcd::value; + static const std::intmax_t n1 = R1::num / gcd_n1_n2; + static const std::intmax_t d1 = R1::den / gcd_d1_d2; + static const std::intmax_t n2 = R2::num / gcd_n1_n2; + static const std::intmax_t d2 = R2::den / gcd_d1_d2; +#ifdef __cpp_constexpr + static const std::intmax_t max = std::numeric_limits::max(); +#else + static const std::intmax_t max = LLONG_MAX; +#endif + + template + struct mul // overflow == false + { + static const std::intmax_t value = Xp * Yp; + }; + + template + struct mul + { + static const std::intmax_t value = 1; + }; + +public: + static const bool value = (n1 <= max / d2) && (n2 <= max / d1); + typedef std::ratio::value, + mul::value> type; +}; + +} // detail + +// trunc towards zero +template +CONSTCD11 +inline +typename std::enable_if +< + detail::no_overflow::value, + To +>::type +trunc(const std::chrono::duration& d) +{ + return To{detail::trunc(std::chrono::duration_cast(d).count())}; +} + +template +CONSTCD11 +inline +typename std::enable_if +< + !detail::no_overflow::value, + To +>::type +trunc(const std::chrono::duration& d) +{ + using std::chrono::duration_cast; + using std::chrono::duration; + using rep = typename std::common_type::type; + return To{detail::trunc(duration_cast(duration_cast>(d)).count())}; +} + +#ifndef HAS_CHRONO_ROUNDING +# if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 190023918 || (_MSC_FULL_VER >= 190000000 && defined (__clang__))) +# define HAS_CHRONO_ROUNDING 1 +# elif defined(__cpp_lib_chrono) && __cplusplus > 201402 && __cpp_lib_chrono >= 201510 +# define HAS_CHRONO_ROUNDING 1 +# elif defined(_LIBCPP_VERSION) && __cplusplus > 201402 && _LIBCPP_VERSION >= 3800 +# define HAS_CHRONO_ROUNDING 1 +# else +# define HAS_CHRONO_ROUNDING 0 +# endif +#endif // HAS_CHRONO_ROUNDING + +#if HAS_CHRONO_ROUNDING == 0 + +// round down +template +CONSTCD14 +inline +typename std::enable_if +< + detail::no_overflow::value, + To +>::type +floor(const std::chrono::duration& d) +{ + auto t = trunc(d); + if (t > d) + return t - To{1}; + return t; +} + +template +CONSTCD14 +inline +typename std::enable_if +< + !detail::no_overflow::value, + To +>::type +floor(const std::chrono::duration& d) +{ + using rep = typename std::common_type::type; + return floor(floor>(d)); +} + +// round to nearest, to even on tie +template +CONSTCD14 +inline +To +round(const std::chrono::duration& d) +{ + auto t0 = floor(d); + auto t1 = t0 + To{1}; + if (t1 == To{0} && t0 < To{0}) + t1 = -t1; + auto diff0 = d - t0; + auto diff1 = t1 - d; + if (diff0 == diff1) + { + if (t0 - trunc(t0/2)*2 == To{0}) + return t0; + return t1; + } + if (diff0 < diff1) + return t0; + return t1; +} + +// round up +template +CONSTCD14 +inline +To +ceil(const std::chrono::duration& d) +{ + auto t = trunc(d); + if (t < d) + return t + To{1}; + return t; +} + +template ::is_signed + >::type> +CONSTCD11 +std::chrono::duration +abs(std::chrono::duration d) +{ + return d >= d.zero() ? d : static_cast(-d); +} + +// round down +template +CONSTCD11 +inline +std::chrono::time_point +floor(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{date::floor(tp.time_since_epoch())}; +} + +// round to nearest, to even on tie +template +CONSTCD11 +inline +std::chrono::time_point +round(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{round(tp.time_since_epoch())}; +} + +// round up +template +CONSTCD11 +inline +std::chrono::time_point +ceil(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{ceil(tp.time_since_epoch())}; +} + +#else // HAS_CHRONO_ROUNDING == 1 + +using std::chrono::floor; +using std::chrono::ceil; +using std::chrono::round; +using std::chrono::abs; + +#endif // HAS_CHRONO_ROUNDING + +namespace detail +{ + +template +CONSTCD14 +inline +typename std::enable_if +< + !std::chrono::treat_as_floating_point::value, + To +>::type +round_i(const std::chrono::duration& d) +{ + return round(d); +} + +template +CONSTCD14 +inline +typename std::enable_if +< + std::chrono::treat_as_floating_point::value, + To +>::type +round_i(const std::chrono::duration& d) +{ + return d; +} + +template +CONSTCD11 +inline +std::chrono::time_point +round_i(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{round_i(tp.time_since_epoch())}; +} + +} // detail + +// trunc towards zero +template +CONSTCD11 +inline +std::chrono::time_point +trunc(const std::chrono::time_point& tp) +{ + using std::chrono::time_point; + return time_point{trunc(tp.time_since_epoch())}; +} + +// day + +CONSTCD11 inline day::day(unsigned d) NOEXCEPT : d_(static_cast(d)) {} +CONSTCD14 inline day& day::operator++() NOEXCEPT {++d_; return *this;} +CONSTCD14 inline day day::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline day& day::operator--() NOEXCEPT {--d_; return *this;} +CONSTCD14 inline day day::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} +CONSTCD14 inline day& day::operator+=(const days& d) NOEXCEPT {*this = *this + d; return *this;} +CONSTCD14 inline day& day::operator-=(const days& d) NOEXCEPT {*this = *this - d; return *this;} +CONSTCD11 inline day::operator unsigned() const NOEXCEPT {return d_;} +CONSTCD11 inline bool day::ok() const NOEXCEPT {return 1 <= d_ && d_ <= 31;} + +CONSTCD11 +inline +bool +operator==(const day& x, const day& y) NOEXCEPT +{ + return static_cast(x) == static_cast(y); +} + +CONSTCD11 +inline +bool +operator!=(const day& x, const day& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const day& x, const day& y) NOEXCEPT +{ + return static_cast(x) < static_cast(y); +} + +CONSTCD11 +inline +bool +operator>(const day& x, const day& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const day& x, const day& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const day& x, const day& y) NOEXCEPT +{ + return !(x < y); +} + +CONSTCD11 +inline +days +operator-(const day& x, const day& y) NOEXCEPT +{ + return days{static_cast(static_cast(x) + - static_cast(y))}; +} + +CONSTCD11 +inline +day +operator+(const day& x, const days& y) NOEXCEPT +{ + return day{static_cast(x) + static_cast(y.count())}; +} + +CONSTCD11 +inline +day +operator+(const days& x, const day& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD11 +inline +day +operator-(const day& x, const days& y) NOEXCEPT +{ + return x + -y; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const day& d) +{ + detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << static_cast(d); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const day& d) +{ + detail::low_level_fmt(os, d); + if (!d.ok()) + os << " is not a valid day"; + return os; +} + +// month + +CONSTCD11 inline month::month(unsigned m) NOEXCEPT : m_(static_cast(m)) {} +CONSTCD14 inline month& month::operator++() NOEXCEPT {*this += months{1}; return *this;} +CONSTCD14 inline month month::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline month& month::operator--() NOEXCEPT {*this -= months{1}; return *this;} +CONSTCD14 inline month month::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} + +CONSTCD14 +inline +month& +month::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +CONSTCD14 +inline +month& +month::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD11 inline month::operator unsigned() const NOEXCEPT {return m_;} +CONSTCD11 inline bool month::ok() const NOEXCEPT {return 1 <= m_ && m_ <= 12;} + +CONSTCD11 +inline +bool +operator==(const month& x, const month& y) NOEXCEPT +{ + return static_cast(x) == static_cast(y); +} + +CONSTCD11 +inline +bool +operator!=(const month& x, const month& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const month& x, const month& y) NOEXCEPT +{ + return static_cast(x) < static_cast(y); +} + +CONSTCD11 +inline +bool +operator>(const month& x, const month& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const month& x, const month& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const month& x, const month& y) NOEXCEPT +{ + return !(x < y); +} + +CONSTCD14 +inline +months +operator-(const month& x, const month& y) NOEXCEPT +{ + auto const d = static_cast(x) - static_cast(y); + return months(d <= 11 ? d : d + 12); +} + +CONSTCD14 +inline +month +operator+(const month& x, const months& y) NOEXCEPT +{ + auto const mu = static_cast(static_cast(x)) + y.count() - 1; + auto const yr = (mu >= 0 ? mu : mu-11) / 12; + return month{static_cast(mu - yr * 12 + 1)}; +} + +CONSTCD14 +inline +month +operator+(const months& x, const month& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD14 +inline +month +operator-(const month& x, const months& y) NOEXCEPT +{ + return x + -y; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month& m) +{ + if (m.ok()) + { + CharT fmt[] = {'%', 'b', 0}; + os << format(os.getloc(), fmt, m); + } + else + os << static_cast(m); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month& m) +{ + detail::low_level_fmt(os, m); + if (!m.ok()) + os << " is not a valid month"; + return os; +} + +// year + +CONSTCD11 inline year::year(int y) NOEXCEPT : y_(static_cast(y)) {} +CONSTCD14 inline year& year::operator++() NOEXCEPT {++y_; return *this;} +CONSTCD14 inline year year::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline year& year::operator--() NOEXCEPT {--y_; return *this;} +CONSTCD14 inline year year::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} +CONSTCD14 inline year& year::operator+=(const years& y) NOEXCEPT {*this = *this + y; return *this;} +CONSTCD14 inline year& year::operator-=(const years& y) NOEXCEPT {*this = *this - y; return *this;} +CONSTCD11 inline year year::operator-() const NOEXCEPT {return year{-y_};} +CONSTCD11 inline year year::operator+() const NOEXCEPT {return *this;} + +CONSTCD11 +inline +bool +year::is_leap() const NOEXCEPT +{ + return y_ % 4 == 0 && (y_ % 100 != 0 || y_ % 400 == 0); +} + +CONSTCD11 inline year::operator int() const NOEXCEPT {return y_;} + +CONSTCD11 +inline +bool +year::ok() const NOEXCEPT +{ + return y_ != std::numeric_limits::min(); +} + +CONSTCD11 +inline +bool +operator==(const year& x, const year& y) NOEXCEPT +{ + return static_cast(x) == static_cast(y); +} + +CONSTCD11 +inline +bool +operator!=(const year& x, const year& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year& x, const year& y) NOEXCEPT +{ + return static_cast(x) < static_cast(y); +} + +CONSTCD11 +inline +bool +operator>(const year& x, const year& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year& x, const year& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year& x, const year& y) NOEXCEPT +{ + return !(x < y); +} + +CONSTCD11 +inline +years +operator-(const year& x, const year& y) NOEXCEPT +{ + return years{static_cast(x) - static_cast(y)}; +} + +CONSTCD11 +inline +year +operator+(const year& x, const years& y) NOEXCEPT +{ + return year{static_cast(x) + y.count()}; +} + +CONSTCD11 +inline +year +operator+(const years& x, const year& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD11 +inline +year +operator-(const year& x, const years& y) NOEXCEPT +{ + return year{static_cast(x) - y.count()}; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const year& y) +{ + detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::internal); + os.width(4 + (y < year{0})); + os.imbue(std::locale::classic()); + os << static_cast(y); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year& y) +{ + detail::low_level_fmt(os, y); + if (!y.ok()) + os << " is not a valid year"; + return os; +} + +// weekday + +CONSTCD14 +inline +unsigned char +weekday::weekday_from_days(int z) NOEXCEPT +{ + auto u = static_cast(z); + return static_cast(z >= -4 ? (u+4) % 7 : u % 7); +} + +CONSTCD11 +inline +weekday::weekday(unsigned wd) NOEXCEPT + : wd_(static_cast(wd != 7 ? wd : 0)) + {} + +CONSTCD14 +inline +weekday::weekday(const sys_days& dp) NOEXCEPT + : wd_(weekday_from_days(dp.time_since_epoch().count())) + {} + +CONSTCD14 +inline +weekday::weekday(const local_days& dp) NOEXCEPT + : wd_(weekday_from_days(dp.time_since_epoch().count())) + {} + +CONSTCD14 inline weekday& weekday::operator++() NOEXCEPT {*this += days{1}; return *this;} +CONSTCD14 inline weekday weekday::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} +CONSTCD14 inline weekday& weekday::operator--() NOEXCEPT {*this -= days{1}; return *this;} +CONSTCD14 inline weekday weekday::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} + +CONSTCD14 +inline +weekday& +weekday::operator+=(const days& d) NOEXCEPT +{ + *this = *this + d; + return *this; +} + +CONSTCD14 +inline +weekday& +weekday::operator-=(const days& d) NOEXCEPT +{ + *this = *this - d; + return *this; +} + +CONSTCD11 inline bool weekday::ok() const NOEXCEPT {return wd_ <= 6;} + +CONSTCD11 +inline +unsigned weekday::c_encoding() const NOEXCEPT +{ + return unsigned{wd_}; +} + +CONSTCD11 +inline +unsigned weekday::iso_encoding() const NOEXCEPT +{ + return unsigned{((wd_ == 0u) ? 7u : wd_)}; +} + +CONSTCD11 +inline +bool +operator==(const weekday& x, const weekday& y) NOEXCEPT +{ + return x.wd_ == y.wd_; +} + +CONSTCD11 +inline +bool +operator!=(const weekday& x, const weekday& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD14 +inline +days +operator-(const weekday& x, const weekday& y) NOEXCEPT +{ + auto const wdu = x.wd_ - y.wd_; + auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; + return days{wdu - wk * 7}; +} + +CONSTCD14 +inline +weekday +operator+(const weekday& x, const days& y) NOEXCEPT +{ + auto const wdu = static_cast(static_cast(x.wd_)) + y.count(); + auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; + return weekday{static_cast(wdu - wk * 7)}; +} + +CONSTCD14 +inline +weekday +operator+(const days& x, const weekday& y) NOEXCEPT +{ + return y + x; +} + +CONSTCD14 +inline +weekday +operator-(const weekday& x, const days& y) NOEXCEPT +{ + return x + -y; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const weekday& wd) +{ + if (wd.ok()) + { + CharT fmt[] = {'%', 'a', 0}; + os << format(fmt, wd); + } + else + os << wd.c_encoding(); + return os; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday& wd) +{ + detail::low_level_fmt(os, wd); + if (!wd.ok()) + os << " is not a valid weekday"; + return os; +} + +#if !defined(_MSC_VER) || (_MSC_VER >= 1900) +inline namespace literals +{ + +CONSTCD11 +inline +date::day +operator "" _d(unsigned long long d) NOEXCEPT +{ + return date::day{static_cast(d)}; +} + +CONSTCD11 +inline +date::year +operator "" _y(unsigned long long y) NOEXCEPT +{ + return date::year(static_cast(y)); +} +#endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) + +CONSTDATA date::last_spec last{}; + +CONSTDATA date::month jan{1}; +CONSTDATA date::month feb{2}; +CONSTDATA date::month mar{3}; +CONSTDATA date::month apr{4}; +CONSTDATA date::month may{5}; +CONSTDATA date::month jun{6}; +CONSTDATA date::month jul{7}; +CONSTDATA date::month aug{8}; +CONSTDATA date::month sep{9}; +CONSTDATA date::month oct{10}; +CONSTDATA date::month nov{11}; +CONSTDATA date::month dec{12}; + +CONSTDATA date::weekday sun{0u}; +CONSTDATA date::weekday mon{1u}; +CONSTDATA date::weekday tue{2u}; +CONSTDATA date::weekday wed{3u}; +CONSTDATA date::weekday thu{4u}; +CONSTDATA date::weekday fri{5u}; +CONSTDATA date::weekday sat{6u}; + +#if !defined(_MSC_VER) || (_MSC_VER >= 1900) +} // inline namespace literals +#endif + +CONSTDATA date::month January{1}; +CONSTDATA date::month February{2}; +CONSTDATA date::month March{3}; +CONSTDATA date::month April{4}; +CONSTDATA date::month May{5}; +CONSTDATA date::month June{6}; +CONSTDATA date::month July{7}; +CONSTDATA date::month August{8}; +CONSTDATA date::month September{9}; +CONSTDATA date::month October{10}; +CONSTDATA date::month November{11}; +CONSTDATA date::month December{12}; + +CONSTDATA date::weekday Monday{1}; +CONSTDATA date::weekday Tuesday{2}; +CONSTDATA date::weekday Wednesday{3}; +CONSTDATA date::weekday Thursday{4}; +CONSTDATA date::weekday Friday{5}; +CONSTDATA date::weekday Saturday{6}; +CONSTDATA date::weekday Sunday{7}; + +// weekday_indexed + +CONSTCD11 +inline +weekday +weekday_indexed::weekday() const NOEXCEPT +{ + return date::weekday{static_cast(wd_)}; +} + +CONSTCD11 inline unsigned weekday_indexed::index() const NOEXCEPT {return index_;} + +CONSTCD11 +inline +bool +weekday_indexed::ok() const NOEXCEPT +{ + return weekday().ok() && 1 <= index_ && index_ <= 5; +} + +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wconversion" +#endif // __GNUC__ + +CONSTCD11 +inline +weekday_indexed::weekday_indexed(const date::weekday& wd, unsigned index) NOEXCEPT + : wd_(static_cast(static_cast(wd.wd_))) + , index_(static_cast(index)) + {} + +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif // __GNUC__ + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const weekday_indexed& wdi) +{ + return low_level_fmt(os, wdi.weekday()) << '[' << wdi.index() << ']'; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_indexed& wdi) +{ + detail::low_level_fmt(os, wdi); + if (!wdi.ok()) + os << " is not a valid weekday_indexed"; + return os; +} + +CONSTCD11 +inline +weekday_indexed +weekday::operator[](unsigned index) const NOEXCEPT +{ + return {*this, index}; +} + +CONSTCD11 +inline +bool +operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT +{ + return x.weekday() == y.weekday() && x.index() == y.index(); +} + +CONSTCD11 +inline +bool +operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT +{ + return !(x == y); +} + +// weekday_last + +CONSTCD11 inline date::weekday weekday_last::weekday() const NOEXCEPT {return wd_;} +CONSTCD11 inline bool weekday_last::ok() const NOEXCEPT {return wd_.ok();} +CONSTCD11 inline weekday_last::weekday_last(const date::weekday& wd) NOEXCEPT : wd_(wd) {} + +CONSTCD11 +inline +bool +operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT +{ + return x.weekday() == y.weekday(); +} + +CONSTCD11 +inline +bool +operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT +{ + return !(x == y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const weekday_last& wdl) +{ + return low_level_fmt(os, wdl.weekday()) << "[last]"; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const weekday_last& wdl) +{ + detail::low_level_fmt(os, wdl); + if (!wdl.ok()) + os << " is not a valid weekday_last"; + return os; +} + +CONSTCD11 +inline +weekday_last +weekday::operator[](last_spec) const NOEXCEPT +{ + return weekday_last{*this}; +} + +// year_month + +CONSTCD11 +inline +year_month::year_month(const date::year& y, const date::month& m) NOEXCEPT + : y_(y) + , m_(m) + {} + +CONSTCD11 inline year year_month::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month::month() const NOEXCEPT {return m_;} +CONSTCD11 inline bool year_month::ok() const NOEXCEPT {return y_.ok() && m_.ok();} + +template +CONSTCD14 +inline +year_month& +year_month::operator+=(const months& dm) NOEXCEPT +{ + *this = *this + dm; + return *this; +} + +template +CONSTCD14 +inline +year_month& +year_month::operator-=(const months& dm) NOEXCEPT +{ + *this = *this - dm; + return *this; +} + +CONSTCD14 +inline +year_month& +year_month::operator+=(const years& dy) NOEXCEPT +{ + *this = *this + dy; + return *this; +} + +CONSTCD14 +inline +year_month& +year_month::operator-=(const years& dy) NOEXCEPT +{ + *this = *this - dy; + return *this; +} + +CONSTCD11 +inline +bool +operator==(const year_month& x, const year_month& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month& x, const year_month& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year_month& x, const year_month& y) NOEXCEPT +{ + return x.year() < y.year() ? true + : (x.year() > y.year() ? false + : (x.month() < y.month())); +} + +CONSTCD11 +inline +bool +operator>(const year_month& x, const year_month& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year_month& x, const year_month& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year_month& x, const year_month& y) NOEXCEPT +{ + return !(x < y); +} + +template +CONSTCD14 +inline +year_month +operator+(const year_month& ym, const months& dm) NOEXCEPT +{ + auto dmi = static_cast(static_cast(ym.month())) - 1 + dm.count(); + auto dy = (dmi >= 0 ? dmi : dmi-11) / 12; + dmi = dmi - dy * 12 + 1; + return (ym.year() + years(dy)) / month(static_cast(dmi)); +} + +template +CONSTCD14 +inline +year_month +operator+(const months& dm, const year_month& ym) NOEXCEPT +{ + return ym + dm; +} + +template +CONSTCD14 +inline +year_month +operator-(const year_month& ym, const months& dm) NOEXCEPT +{ + return ym + -dm; +} + +CONSTCD11 +inline +months +operator-(const year_month& x, const year_month& y) NOEXCEPT +{ + return (x.year() - y.year()) + + months(static_cast(x.month()) - static_cast(y.month())); +} + +CONSTCD11 +inline +year_month +operator+(const year_month& ym, const years& dy) NOEXCEPT +{ + return (ym.year() + dy) / ym.month(); +} + +CONSTCD11 +inline +year_month +operator+(const years& dy, const year_month& ym) NOEXCEPT +{ + return ym + dy; +} + +CONSTCD11 +inline +year_month +operator-(const year_month& ym, const years& dy) NOEXCEPT +{ + return ym + -dy; +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const year_month& ym) +{ + low_level_fmt(os, ym.year()) << '/'; + return low_level_fmt(os, ym.month()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month& ym) +{ + detail::low_level_fmt(os, ym); + if (!ym.ok()) + os << " is not a valid year_month"; + return os; +} + +// month_day + +CONSTCD11 +inline +month_day::month_day(const date::month& m, const date::day& d) NOEXCEPT + : m_(m) + , d_(d) + {} + +CONSTCD11 inline date::month month_day::month() const NOEXCEPT {return m_;} +CONSTCD11 inline date::day month_day::day() const NOEXCEPT {return d_;} + +CONSTCD14 +inline +bool +month_day::ok() const NOEXCEPT +{ + CONSTDATA date::day d[] = + { + date::day(31), date::day(29), date::day(31), + date::day(30), date::day(31), date::day(30), + date::day(31), date::day(31), date::day(30), + date::day(31), date::day(30), date::day(31) + }; + return m_.ok() && date::day{1} <= d_ && d_ <= d[static_cast(m_)-1]; +} + +CONSTCD11 +inline +bool +operator==(const month_day& x, const month_day& y) NOEXCEPT +{ + return x.month() == y.month() && x.day() == y.day(); +} + +CONSTCD11 +inline +bool +operator!=(const month_day& x, const month_day& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const month_day& x, const month_day& y) NOEXCEPT +{ + return x.month() < y.month() ? true + : (x.month() > y.month() ? false + : (x.day() < y.day())); +} + +CONSTCD11 +inline +bool +operator>(const month_day& x, const month_day& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const month_day& x, const month_day& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const month_day& x, const month_day& y) NOEXCEPT +{ + return !(x < y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_day& md) +{ + low_level_fmt(os, md.month()) << '/'; + return low_level_fmt(os, md.day()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day& md) +{ + detail::low_level_fmt(os, md); + if (!md.ok()) + os << " is not a valid month_day"; + return os; +} + +// month_day_last + +CONSTCD11 inline month month_day_last::month() const NOEXCEPT {return m_;} +CONSTCD11 inline bool month_day_last::ok() const NOEXCEPT {return m_.ok();} +CONSTCD11 inline month_day_last::month_day_last(const date::month& m) NOEXCEPT : m_(m) {} + +CONSTCD11 +inline +bool +operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return x.month() == y.month(); +} + +CONSTCD11 +inline +bool +operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return x.month() < y.month(); +} + +CONSTCD11 +inline +bool +operator>(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT +{ + return !(x < y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_day_last& mdl) +{ + return low_level_fmt(os, mdl.month()) << "/last"; +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_day_last& mdl) +{ + detail::low_level_fmt(os, mdl); + if (!mdl.ok()) + os << " is not a valid month_day_last"; + return os; +} + +// month_weekday + +CONSTCD11 +inline +month_weekday::month_weekday(const date::month& m, + const date::weekday_indexed& wdi) NOEXCEPT + : m_(m) + , wdi_(wdi) + {} + +CONSTCD11 inline month month_weekday::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday_indexed +month_weekday::weekday_indexed() const NOEXCEPT +{ + return wdi_; +} + +CONSTCD11 +inline +bool +month_weekday::ok() const NOEXCEPT +{ + return m_.ok() && wdi_.ok(); +} + +CONSTCD11 +inline +bool +operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT +{ + return x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); +} + +CONSTCD11 +inline +bool +operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT +{ + return !(x == y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_weekday& mwd) +{ + low_level_fmt(os, mwd.month()) << '/'; + return low_level_fmt(os, mwd.weekday_indexed()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday& mwd) +{ + detail::low_level_fmt(os, mwd); + if (!mwd.ok()) + os << " is not a valid month_weekday"; + return os; +} + +// month_weekday_last + +CONSTCD11 +inline +month_weekday_last::month_weekday_last(const date::month& m, + const date::weekday_last& wdl) NOEXCEPT + : m_(m) + , wdl_(wdl) + {} + +CONSTCD11 inline month month_weekday_last::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday_last +month_weekday_last::weekday_last() const NOEXCEPT +{ + return wdl_; +} + +CONSTCD11 +inline +bool +month_weekday_last::ok() const NOEXCEPT +{ + return m_.ok() && wdl_.ok(); +} + +CONSTCD11 +inline +bool +operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT +{ + return x.month() == y.month() && x.weekday_last() == y.weekday_last(); +} + +CONSTCD11 +inline +bool +operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT +{ + return !(x == y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const month_weekday_last& mwdl) +{ + low_level_fmt(os, mwdl.month()) << '/'; + return low_level_fmt(os, mwdl.weekday_last()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const month_weekday_last& mwdl) +{ + detail::low_level_fmt(os, mwdl); + if (!mwdl.ok()) + os << " is not a valid month_weekday_last"; + return os; +} + +// year_month_day_last + +CONSTCD11 +inline +year_month_day_last::year_month_day_last(const date::year& y, + const date::month_day_last& mdl) NOEXCEPT + : y_(y) + , mdl_(mdl) + {} + +template +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_day_last& +year_month_day_last::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD11 inline year year_month_day_last::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_day_last::month() const NOEXCEPT {return mdl_.month();} + +CONSTCD11 +inline +month_day_last +year_month_day_last::month_day_last() const NOEXCEPT +{ + return mdl_; +} + +CONSTCD14 +inline +day +year_month_day_last::day() const NOEXCEPT +{ + CONSTDATA date::day d[] = + { + date::day(31), date::day(28), date::day(31), + date::day(30), date::day(31), date::day(30), + date::day(31), date::day(31), date::day(30), + date::day(31), date::day(30), date::day(31) + }; + return (month() != February || !y_.is_leap()) && mdl_.ok() ? + d[static_cast(month()) - 1] : date::day{29}; +} + +CONSTCD14 +inline +year_month_day_last::operator sys_days() const NOEXCEPT +{ + return sys_days(year()/month()/day()); +} + +CONSTCD14 +inline +year_month_day_last::operator local_days() const NOEXCEPT +{ + return local_days(year()/month()/day()); +} + +CONSTCD11 +inline +bool +year_month_day_last::ok() const NOEXCEPT +{ + return y_.ok() && mdl_.ok(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return x.year() == y.year() && x.month_day_last() == y.month_day_last(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return x.year() < y.year() ? true + : (x.year() > y.year() ? false + : (x.month_day_last() < y.month_day_last())); +} + +CONSTCD11 +inline +bool +operator>(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT +{ + return !(x < y); +} + +namespace detail +{ + +template +std::basic_ostream& +low_level_fmt(std::basic_ostream& os, const year_month_day_last& ymdl) +{ + low_level_fmt(os, ymdl.year()) << '/'; + return low_level_fmt(os, ymdl.month_day_last()); +} + +} // namespace detail + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day_last& ymdl) +{ + detail::low_level_fmt(os, ymdl); + if (!ymdl.ok()) + os << " is not a valid year_month_day_last"; + return os; +} + +template +CONSTCD14 +inline +year_month_day_last +operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT +{ + return (ymdl.year() / ymdl.month() + dm) / last; +} + +template +CONSTCD14 +inline +year_month_day_last +operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT +{ + return ymdl + dm; +} + +template +CONSTCD14 +inline +year_month_day_last +operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT +{ + return ymdl + (-dm); +} + +CONSTCD11 +inline +year_month_day_last +operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT +{ + return {ymdl.year()+dy, ymdl.month_day_last()}; +} + +CONSTCD11 +inline +year_month_day_last +operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT +{ + return ymdl + dy; +} + +CONSTCD11 +inline +year_month_day_last +operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT +{ + return ymdl + (-dy); +} + +// year_month_day + +CONSTCD11 +inline +year_month_day::year_month_day(const date::year& y, const date::month& m, + const date::day& d) NOEXCEPT + : y_(y) + , m_(m) + , d_(d) + {} + +CONSTCD14 +inline +year_month_day::year_month_day(const year_month_day_last& ymdl) NOEXCEPT + : y_(ymdl.year()) + , m_(ymdl.month()) + , d_(ymdl.day()) + {} + +CONSTCD14 +inline +year_month_day::year_month_day(sys_days dp) NOEXCEPT + : year_month_day(from_days(dp.time_since_epoch())) + {} + +CONSTCD14 +inline +year_month_day::year_month_day(local_days dp) NOEXCEPT + : year_month_day(from_days(dp.time_since_epoch())) + {} + +CONSTCD11 inline year year_month_day::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_day::month() const NOEXCEPT {return m_;} +CONSTCD11 inline day year_month_day::day() const NOEXCEPT {return d_;} + +template +CONSTCD14 +inline +year_month_day& +year_month_day::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_day& +year_month_day::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_day& +year_month_day::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_day& +year_month_day::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD14 +inline +days +year_month_day::to_days() const NOEXCEPT +{ + static_assert(std::numeric_limits::digits >= 18, + "This algorithm has not been ported to a 16 bit unsigned integer"); + static_assert(std::numeric_limits::digits >= 20, + "This algorithm has not been ported to a 16 bit signed integer"); + auto const y = static_cast(y_) - (m_ <= February); + auto const m = static_cast(m_); + auto const d = static_cast(d_); + auto const era = (y >= 0 ? y : y-399) / 400; + auto const yoe = static_cast(y - era * 400); // [0, 399] + auto const doy = (153*(m > 2 ? m-3 : m+9) + 2)/5 + d-1; // [0, 365] + auto const doe = yoe * 365 + yoe/4 - yoe/100 + doy; // [0, 146096] + return days{era * 146097 + static_cast(doe) - 719468}; +} + +CONSTCD14 +inline +year_month_day::operator sys_days() const NOEXCEPT +{ + return sys_days{to_days()}; +} + +CONSTCD14 +inline +year_month_day::operator local_days() const NOEXCEPT +{ + return local_days{to_days()}; +} + +CONSTCD14 +inline +bool +year_month_day::ok() const NOEXCEPT +{ + if (!(y_.ok() && m_.ok())) + return false; + return date::day{1} <= d_ && d_ <= (y_ / m_ / last).day(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month() && x.day() == y.day(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return !(x == y); +} + +CONSTCD11 +inline +bool +operator<(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return x.year() < y.year() ? true + : (x.year() > y.year() ? false + : (x.month() < y.month() ? true + : (x.month() > y.month() ? false + : (x.day() < y.day())))); +} + +CONSTCD11 +inline +bool +operator>(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return y < x; +} + +CONSTCD11 +inline +bool +operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return !(y < x); +} + +CONSTCD11 +inline +bool +operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT +{ + return !(x < y); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_day& ymd) +{ + detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.imbue(std::locale::classic()); + os << static_cast(ymd.year()) << '-'; + os.width(2); + os << static_cast(ymd.month()) << '-'; + os.width(2); + os << static_cast(ymd.day()); + if (!ymd.ok()) + os << " is not a valid year_month_day"; + return os; +} + +CONSTCD14 +inline +year_month_day +year_month_day::from_days(days dp) NOEXCEPT +{ + static_assert(std::numeric_limits::digits >= 18, + "This algorithm has not been ported to a 16 bit unsigned integer"); + static_assert(std::numeric_limits::digits >= 20, + "This algorithm has not been ported to a 16 bit signed integer"); + auto const z = dp.count() + 719468; + auto const era = (z >= 0 ? z : z - 146096) / 146097; + auto const doe = static_cast(z - era * 146097); // [0, 146096] + auto const yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; // [0, 399] + auto const y = static_cast(yoe) + era * 400; + auto const doy = doe - (365*yoe + yoe/4 - yoe/100); // [0, 365] + auto const mp = (5*doy + 2)/153; // [0, 11] + auto const d = doy - (153*mp+2)/5 + 1; // [1, 31] + auto const m = mp < 10 ? mp+3 : mp-9; // [1, 12] + return year_month_day{date::year{y + (m <= 2)}, date::month(m), date::day(d)}; +} + +template +CONSTCD14 +inline +year_month_day +operator+(const year_month_day& ymd, const months& dm) NOEXCEPT +{ + return (ymd.year() / ymd.month() + dm) / ymd.day(); +} + +template +CONSTCD14 +inline +year_month_day +operator+(const months& dm, const year_month_day& ymd) NOEXCEPT +{ + return ymd + dm; +} + +template +CONSTCD14 +inline +year_month_day +operator-(const year_month_day& ymd, const months& dm) NOEXCEPT +{ + return ymd + (-dm); +} + +CONSTCD11 +inline +year_month_day +operator+(const year_month_day& ymd, const years& dy) NOEXCEPT +{ + return (ymd.year() + dy) / ymd.month() / ymd.day(); +} + +CONSTCD11 +inline +year_month_day +operator+(const years& dy, const year_month_day& ymd) NOEXCEPT +{ + return ymd + dy; +} + +CONSTCD11 +inline +year_month_day +operator-(const year_month_day& ymd, const years& dy) NOEXCEPT +{ + return ymd + (-dy); +} + +// year_month_weekday + +CONSTCD11 +inline +year_month_weekday::year_month_weekday(const date::year& y, const date::month& m, + const date::weekday_indexed& wdi) + NOEXCEPT + : y_(y) + , m_(m) + , wdi_(wdi) + {} + +CONSTCD14 +inline +year_month_weekday::year_month_weekday(const sys_days& dp) NOEXCEPT + : year_month_weekday(from_days(dp.time_since_epoch())) + {} + +CONSTCD14 +inline +year_month_weekday::year_month_weekday(const local_days& dp) NOEXCEPT + : year_month_weekday(from_days(dp.time_since_epoch())) + {} + +template +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_weekday& +year_month_weekday::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD11 inline year year_month_weekday::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_weekday::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday +year_month_weekday::weekday() const NOEXCEPT +{ + return wdi_.weekday(); +} + +CONSTCD11 +inline +unsigned +year_month_weekday::index() const NOEXCEPT +{ + return wdi_.index(); +} + +CONSTCD11 +inline +weekday_indexed +year_month_weekday::weekday_indexed() const NOEXCEPT +{ + return wdi_; +} + +CONSTCD14 +inline +year_month_weekday::operator sys_days() const NOEXCEPT +{ + return sys_days{to_days()}; +} + +CONSTCD14 +inline +year_month_weekday::operator local_days() const NOEXCEPT +{ + return local_days{to_days()}; +} + +CONSTCD14 +inline +bool +year_month_weekday::ok() const NOEXCEPT +{ + if (!y_.ok() || !m_.ok() || !wdi_.weekday().ok() || wdi_.index() < 1) + return false; + if (wdi_.index() <= 4) + return true; + auto d2 = wdi_.weekday() - date::weekday(static_cast(y_/m_/1)) + + days((wdi_.index()-1)*7 + 1); + return static_cast(d2.count()) <= static_cast((y_/m_/last).day()); +} + +CONSTCD14 +inline +year_month_weekday +year_month_weekday::from_days(days d) NOEXCEPT +{ + sys_days dp{d}; + auto const wd = date::weekday(dp); + auto const ymd = year_month_day(dp); + return {ymd.year(), ymd.month(), wd[(static_cast(ymd.day())-1)/7+1]}; +} + +CONSTCD14 +inline +days +year_month_weekday::to_days() const NOEXCEPT +{ + auto d = sys_days(y_/m_/1); + return (d + (wdi_.weekday() - date::weekday(d) + days{(wdi_.index()-1)*7}) + ).time_since_epoch(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month() && + x.weekday_indexed() == y.weekday_indexed(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT +{ + return !(x == y); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi) +{ + detail::low_level_fmt(os, ymwdi.year()) << '/'; + detail::low_level_fmt(os, ymwdi.month()) << '/'; + detail::low_level_fmt(os, ymwdi.weekday_indexed()); + if (!ymwdi.ok()) + os << " is not a valid year_month_weekday"; + return os; +} + +template +CONSTCD14 +inline +year_month_weekday +operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT +{ + return (ymwd.year() / ymwd.month() + dm) / ymwd.weekday_indexed(); +} + +template +CONSTCD14 +inline +year_month_weekday +operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT +{ + return ymwd + dm; +} + +template +CONSTCD14 +inline +year_month_weekday +operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT +{ + return ymwd + (-dm); +} + +CONSTCD11 +inline +year_month_weekday +operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT +{ + return {ymwd.year()+dy, ymwd.month(), ymwd.weekday_indexed()}; +} + +CONSTCD11 +inline +year_month_weekday +operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT +{ + return ymwd + dy; +} + +CONSTCD11 +inline +year_month_weekday +operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT +{ + return ymwd + (-dy); +} + +// year_month_weekday_last + +CONSTCD11 +inline +year_month_weekday_last::year_month_weekday_last(const date::year& y, + const date::month& m, + const date::weekday_last& wdl) NOEXCEPT + : y_(y) + , m_(m) + , wdl_(wdl) + {} + +template +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator+=(const months& m) NOEXCEPT +{ + *this = *this + m; + return *this; +} + +template +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator-=(const months& m) NOEXCEPT +{ + *this = *this - m; + return *this; +} + +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator+=(const years& y) NOEXCEPT +{ + *this = *this + y; + return *this; +} + +CONSTCD14 +inline +year_month_weekday_last& +year_month_weekday_last::operator-=(const years& y) NOEXCEPT +{ + *this = *this - y; + return *this; +} + +CONSTCD11 inline year year_month_weekday_last::year() const NOEXCEPT {return y_;} +CONSTCD11 inline month year_month_weekday_last::month() const NOEXCEPT {return m_;} + +CONSTCD11 +inline +weekday +year_month_weekday_last::weekday() const NOEXCEPT +{ + return wdl_.weekday(); +} + +CONSTCD11 +inline +weekday_last +year_month_weekday_last::weekday_last() const NOEXCEPT +{ + return wdl_; +} + +CONSTCD14 +inline +year_month_weekday_last::operator sys_days() const NOEXCEPT +{ + return sys_days{to_days()}; +} + +CONSTCD14 +inline +year_month_weekday_last::operator local_days() const NOEXCEPT +{ + return local_days{to_days()}; +} + +CONSTCD11 +inline +bool +year_month_weekday_last::ok() const NOEXCEPT +{ + return y_.ok() && m_.ok() && wdl_.ok(); +} + +CONSTCD14 +inline +days +year_month_weekday_last::to_days() const NOEXCEPT +{ + auto const d = sys_days(y_/m_/last); + return (d - (date::weekday{d} - wdl_.weekday())).time_since_epoch(); +} + +CONSTCD11 +inline +bool +operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT +{ + return x.year() == y.year() && x.month() == y.month() && + x.weekday_last() == y.weekday_last(); +} + +CONSTCD11 +inline +bool +operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT +{ + return !(x == y); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl) +{ + detail::low_level_fmt(os, ymwdl.year()) << '/'; + detail::low_level_fmt(os, ymwdl.month()) << '/'; + detail::low_level_fmt(os, ymwdl.weekday_last()); + if (!ymwdl.ok()) + os << " is not a valid year_month_weekday_last"; + return os; +} + +template +CONSTCD14 +inline +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT +{ + return (ymwdl.year() / ymwdl.month() + dm) / ymwdl.weekday_last(); +} + +template +CONSTCD14 +inline +year_month_weekday_last +operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT +{ + return ymwdl + dm; +} + +template +CONSTCD14 +inline +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT +{ + return ymwdl + (-dm); +} + +CONSTCD11 +inline +year_month_weekday_last +operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT +{ + return {ymwdl.year()+dy, ymwdl.month(), ymwdl.weekday_last()}; +} + +CONSTCD11 +inline +year_month_weekday_last +operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT +{ + return ymwdl + dy; +} + +CONSTCD11 +inline +year_month_weekday_last +operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT +{ + return ymwdl + (-dy); +} + +// year_month from operator/() + +CONSTCD11 +inline +year_month +operator/(const year& y, const month& m) NOEXCEPT +{ + return {y, m}; +} + +CONSTCD11 +inline +year_month +operator/(const year& y, int m) NOEXCEPT +{ + return y / month(static_cast(m)); +} + +// month_day from operator/() + +CONSTCD11 +inline +month_day +operator/(const month& m, const day& d) NOEXCEPT +{ + return {m, d}; +} + +CONSTCD11 +inline +month_day +operator/(const day& d, const month& m) NOEXCEPT +{ + return m / d; +} + +CONSTCD11 +inline +month_day +operator/(const month& m, int d) NOEXCEPT +{ + return m / day(static_cast(d)); +} + +CONSTCD11 +inline +month_day +operator/(int m, const day& d) NOEXCEPT +{ + return month(static_cast(m)) / d; +} + +CONSTCD11 inline month_day operator/(const day& d, int m) NOEXCEPT {return m / d;} + +// month_day_last from operator/() + +CONSTCD11 +inline +month_day_last +operator/(const month& m, last_spec) NOEXCEPT +{ + return month_day_last{m}; +} + +CONSTCD11 +inline +month_day_last +operator/(last_spec, const month& m) NOEXCEPT +{ + return m/last; +} + +CONSTCD11 +inline +month_day_last +operator/(int m, last_spec) NOEXCEPT +{ + return month(static_cast(m))/last; +} + +CONSTCD11 +inline +month_day_last +operator/(last_spec, int m) NOEXCEPT +{ + return m/last; +} + +// month_weekday from operator/() + +CONSTCD11 +inline +month_weekday +operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT +{ + return {m, wdi}; +} + +CONSTCD11 +inline +month_weekday +operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT +{ + return m / wdi; +} + +CONSTCD11 +inline +month_weekday +operator/(int m, const weekday_indexed& wdi) NOEXCEPT +{ + return month(static_cast(m)) / wdi; +} + +CONSTCD11 +inline +month_weekday +operator/(const weekday_indexed& wdi, int m) NOEXCEPT +{ + return m / wdi; +} + +// month_weekday_last from operator/() + +CONSTCD11 +inline +month_weekday_last +operator/(const month& m, const weekday_last& wdl) NOEXCEPT +{ + return {m, wdl}; +} + +CONSTCD11 +inline +month_weekday_last +operator/(const weekday_last& wdl, const month& m) NOEXCEPT +{ + return m / wdl; +} + +CONSTCD11 +inline +month_weekday_last +operator/(int m, const weekday_last& wdl) NOEXCEPT +{ + return month(static_cast(m)) / wdl; +} + +CONSTCD11 +inline +month_weekday_last +operator/(const weekday_last& wdl, int m) NOEXCEPT +{ + return m / wdl; +} + +// year_month_day from operator/() + +CONSTCD11 +inline +year_month_day +operator/(const year_month& ym, const day& d) NOEXCEPT +{ + return {ym.year(), ym.month(), d}; +} + +CONSTCD11 +inline +year_month_day +operator/(const year_month& ym, int d) NOEXCEPT +{ + return ym / day(static_cast(d)); +} + +CONSTCD11 +inline +year_month_day +operator/(const year& y, const month_day& md) NOEXCEPT +{ + return y / md.month() / md.day(); +} + +CONSTCD11 +inline +year_month_day +operator/(int y, const month_day& md) NOEXCEPT +{ + return year(y) / md; +} + +CONSTCD11 +inline +year_month_day +operator/(const month_day& md, const year& y) NOEXCEPT +{ + return y / md; +} + +CONSTCD11 +inline +year_month_day +operator/(const month_day& md, int y) NOEXCEPT +{ + return year(y) / md; +} + +// year_month_day_last from operator/() + +CONSTCD11 +inline +year_month_day_last +operator/(const year_month& ym, last_spec) NOEXCEPT +{ + return {ym.year(), month_day_last{ym.month()}}; +} + +CONSTCD11 +inline +year_month_day_last +operator/(const year& y, const month_day_last& mdl) NOEXCEPT +{ + return {y, mdl}; +} + +CONSTCD11 +inline +year_month_day_last +operator/(int y, const month_day_last& mdl) NOEXCEPT +{ + return year(y) / mdl; +} + +CONSTCD11 +inline +year_month_day_last +operator/(const month_day_last& mdl, const year& y) NOEXCEPT +{ + return y / mdl; +} + +CONSTCD11 +inline +year_month_day_last +operator/(const month_day_last& mdl, int y) NOEXCEPT +{ + return year(y) / mdl; +} + +// year_month_weekday from operator/() + +CONSTCD11 +inline +year_month_weekday +operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT +{ + return {ym.year(), ym.month(), wdi}; +} + +CONSTCD11 +inline +year_month_weekday +operator/(const year& y, const month_weekday& mwd) NOEXCEPT +{ + return {y, mwd.month(), mwd.weekday_indexed()}; +} + +CONSTCD11 +inline +year_month_weekday +operator/(int y, const month_weekday& mwd) NOEXCEPT +{ + return year(y) / mwd; +} + +CONSTCD11 +inline +year_month_weekday +operator/(const month_weekday& mwd, const year& y) NOEXCEPT +{ + return y / mwd; +} + +CONSTCD11 +inline +year_month_weekday +operator/(const month_weekday& mwd, int y) NOEXCEPT +{ + return year(y) / mwd; +} + +// year_month_weekday_last from operator/() + +CONSTCD11 +inline +year_month_weekday_last +operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT +{ + return {ym.year(), ym.month(), wdl}; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT +{ + return {y, mwdl.month(), mwdl.weekday_last()}; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(int y, const month_weekday_last& mwdl) NOEXCEPT +{ + return year(y) / mwdl; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT +{ + return y / mwdl; +} + +CONSTCD11 +inline +year_month_weekday_last +operator/(const month_weekday_last& mwdl, int y) NOEXCEPT +{ + return year(y) / mwdl; +} + +template +struct fields; + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const fields& fds, const std::string* abbrev = nullptr, + const std::chrono::seconds* offset_sec = nullptr); + +template +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + fields& fds, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr); + +// hh_mm_ss + +namespace detail +{ + +struct undocumented {explicit undocumented() = default;}; + +// width::value is the number of fractional decimal digits in 1/n +// width<0>::value and width<1>::value are defined to be 0 +// If 1/n takes more than 18 fractional decimal digits, +// the result is truncated to 19. +// Example: width<2>::value == 1 +// Example: width<3>::value == 19 +// Example: width<4>::value == 2 +// Example: width<10>::value == 1 +// Example: width<1000>::value == 3 +template +struct width +{ + static_assert(d > 0, "width called with zero denominator"); + static CONSTDATA unsigned value = 1 + width::value; +}; + +template +struct width +{ + static CONSTDATA unsigned value = 0; +}; + +template +struct static_pow10 +{ +private: + static CONSTDATA std::uint64_t h = static_pow10::value; +public: + static CONSTDATA std::uint64_t value = h * h * (exp % 2 ? 10 : 1); +}; + +template <> +struct static_pow10<0> +{ + static CONSTDATA std::uint64_t value = 1; +}; + +template +class decimal_format_seconds +{ + using CT = typename std::common_type::type; + using rep = typename CT::rep; + static unsigned CONSTDATA trial_width = + detail::width::value; +public: + static unsigned CONSTDATA width = trial_width < 19 ? trial_width : 6u; + using precision = std::chrono::duration::value>>; + +private: + std::chrono::seconds s_; + precision sub_s_; + +public: + CONSTCD11 decimal_format_seconds() + : s_() + , sub_s_() + {} + + CONSTCD11 explicit decimal_format_seconds(const Duration& d) NOEXCEPT + : s_(std::chrono::duration_cast(d)) + , sub_s_(std::chrono::duration_cast(d - s_)) + {} + + CONSTCD14 std::chrono::seconds& seconds() NOEXCEPT {return s_;} + CONSTCD11 std::chrono::seconds seconds() const NOEXCEPT {return s_;} + CONSTCD11 precision subseconds() const NOEXCEPT {return sub_s_;} + + CONSTCD14 precision to_duration() const NOEXCEPT + { + return s_ + sub_s_; + } + + CONSTCD11 bool in_conventional_range() const NOEXCEPT + { + return sub_s_ < std::chrono::seconds{1} && s_ < std::chrono::minutes{1}; + } + + template + friend + std::basic_ostream& + operator<<(std::basic_ostream& os, const decimal_format_seconds& x) + { + return x.print(os, std::chrono::treat_as_floating_point{}); + } + + template + std::basic_ostream& + print(std::basic_ostream& os, std::true_type) const + { + date::detail::save_ostream _(os); + std::chrono::duration d = s_ + sub_s_; + if (d < std::chrono::seconds{10}) + os << '0'; + os.precision(width+6); + os << std::fixed << d.count(); + return os; + } + + template + std::basic_ostream& + print(std::basic_ostream& os, std::false_type) const + { + date::detail::save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << s_.count(); + if (width > 0) + { +#if !ONLY_C_LOCALE + os << std::use_facet>(os.getloc()).decimal_point(); +#else + os << '.'; +#endif + date::detail::save_ostream _s(os); + os.imbue(std::locale::classic()); + os.width(width); + os << sub_s_.count(); + } + return os; + } +}; + +template +inline +CONSTCD11 +typename std::enable_if + < + std::numeric_limits::is_signed, + std::chrono::duration + >::type +abs(std::chrono::duration d) +{ + return d >= d.zero() ? +d : -d; +} + +template +inline +CONSTCD11 +typename std::enable_if + < + !std::numeric_limits::is_signed, + std::chrono::duration + >::type +abs(std::chrono::duration d) +{ + return d; +} + +} // namespace detail + +template +class hh_mm_ss +{ + using dfs = detail::decimal_format_seconds::type>; + + std::chrono::hours h_; + std::chrono::minutes m_; + dfs s_; + bool neg_; + +public: + static unsigned CONSTDATA fractional_width = dfs::width; + using precision = typename dfs::precision; + + CONSTCD11 hh_mm_ss() NOEXCEPT + : hh_mm_ss(Duration::zero()) + {} + + CONSTCD11 explicit hh_mm_ss(Duration d) NOEXCEPT + : h_(std::chrono::duration_cast(detail::abs(d))) + , m_(std::chrono::duration_cast(detail::abs(d)) - h_) + , s_(detail::abs(d) - h_ - m_) + , neg_(d < Duration::zero()) + {} + + CONSTCD11 std::chrono::hours hours() const NOEXCEPT {return h_;} + CONSTCD11 std::chrono::minutes minutes() const NOEXCEPT {return m_;} + CONSTCD11 std::chrono::seconds seconds() const NOEXCEPT {return s_.seconds();} + CONSTCD14 std::chrono::seconds& + seconds(detail::undocumented) NOEXCEPT {return s_.seconds();} + CONSTCD11 precision subseconds() const NOEXCEPT {return s_.subseconds();} + CONSTCD11 bool is_negative() const NOEXCEPT {return neg_;} + + CONSTCD11 explicit operator precision() const NOEXCEPT {return to_duration();} + CONSTCD11 precision to_duration() const NOEXCEPT + {return (s_.to_duration() + m_ + h_) * (1-2*neg_);} + + CONSTCD11 bool in_conventional_range() const NOEXCEPT + { + return !neg_ && h_ < days{1} && m_ < std::chrono::hours{1} && + s_.in_conventional_range(); + } + +private: + + template + friend + std::basic_ostream& + operator<<(std::basic_ostream& os, hh_mm_ss const& tod) + { + if (tod.is_negative()) + os << '-'; + if (tod.h_ < std::chrono::hours{10}) + os << '0'; + os << tod.h_.count() << ':'; + if (tod.m_ < std::chrono::minutes{10}) + os << '0'; + os << tod.m_.count() << ':' << tod.s_; + return os; + } + + template + friend + std::basic_ostream& + date::to_stream(std::basic_ostream& os, const CharT* fmt, + const fields& fds, const std::string* abbrev, + const std::chrono::seconds* offset_sec); + + template + friend + std::basic_istream& + date::from_stream(std::basic_istream& is, const CharT* fmt, + fields& fds, + std::basic_string* abbrev, std::chrono::minutes* offset); +}; + +inline +CONSTCD14 +bool +is_am(std::chrono::hours const& h) NOEXCEPT +{ + using std::chrono::hours; + return hours{0} <= h && h < hours{12}; +} + +inline +CONSTCD14 +bool +is_pm(std::chrono::hours const& h) NOEXCEPT +{ + using std::chrono::hours; + return hours{12} <= h && h < hours{24}; +} + +inline +CONSTCD14 +std::chrono::hours +make12(std::chrono::hours h) NOEXCEPT +{ + using std::chrono::hours; + if (h < hours{12}) + { + if (h == hours{0}) + h = hours{12}; + } + else + { + if (h != hours{12}) + h = h - hours{12}; + } + return h; +} + +inline +CONSTCD14 +std::chrono::hours +make24(std::chrono::hours h, bool is_pm) NOEXCEPT +{ + using std::chrono::hours; + if (is_pm) + { + if (h != hours{12}) + h = h + hours{12}; + } + else if (h == hours{12}) + h = hours{0}; + return h; +} + +template +using time_of_day = hh_mm_ss; + +template +CONSTCD11 +inline +hh_mm_ss> +make_time(const std::chrono::duration& d) +{ + return hh_mm_ss>(d); +} + +template +inline +typename std::enable_if +< + !std::is_convertible::value, + std::basic_ostream& +>::type +operator<<(std::basic_ostream& os, const sys_time& tp) +{ + auto const dp = date::floor(tp); + return os << year_month_day(dp) << ' ' << make_time(tp-dp); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const sys_days& dp) +{ + return os << year_month_day(dp); +} + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, const local_time& ut) +{ + return (os << sys_time{ut.time_since_epoch()}); +} + +namespace detail +{ + +template +class string_literal; + +template +inline +CONSTCD14 +string_literal::type, + N1 + N2 - 1> +operator+(const string_literal& x, const string_literal& y) NOEXCEPT; + +template +class string_literal +{ + CharT p_[N]; + + CONSTCD11 string_literal() NOEXCEPT + : p_{} + {} + +public: + using const_iterator = const CharT*; + + string_literal(string_literal const&) = default; + string_literal& operator=(string_literal const&) = delete; + + template ::type> + CONSTCD11 string_literal(CharT c) NOEXCEPT + : p_{c} + { + } + + template ::type> + CONSTCD11 string_literal(CharT c1, CharT c2) NOEXCEPT + : p_{c1, c2} + { + } + + template ::type> + CONSTCD11 string_literal(CharT c1, CharT c2, CharT c3) NOEXCEPT + : p_{c1, c2, c3} + { + } + + CONSTCD14 string_literal(const CharT(&a)[N]) NOEXCEPT + : p_{} + { + for (std::size_t i = 0; i < N; ++i) + p_[i] = a[i]; + } + + template ::type> + CONSTCD14 string_literal(const char(&a)[N]) NOEXCEPT + : p_{} + { + for (std::size_t i = 0; i < N; ++i) + p_[i] = a[i]; + } + + template ::value>::type> + CONSTCD14 string_literal(string_literal const& a) NOEXCEPT + : p_{} + { + for (std::size_t i = 0; i < N; ++i) + p_[i] = a[i]; + } + + CONSTCD11 const CharT* data() const NOEXCEPT {return p_;} + CONSTCD11 std::size_t size() const NOEXCEPT {return N-1;} + + CONSTCD11 const_iterator begin() const NOEXCEPT {return p_;} + CONSTCD11 const_iterator end() const NOEXCEPT {return p_ + N-1;} + + CONSTCD11 CharT const& operator[](std::size_t n) const NOEXCEPT + { + return p_[n]; + } + + template + friend + std::basic_ostream& + operator<<(std::basic_ostream& os, const string_literal& s) + { + return os << s.p_; + } + + template + friend + CONSTCD14 + string_literal::type, + N1 + N2 - 1> + operator+(const string_literal& x, const string_literal& y) NOEXCEPT; +}; + +template +CONSTCD11 +inline +string_literal +operator+(const string_literal& x, const string_literal& y) NOEXCEPT +{ + return string_literal(x[0], y[0]); +} + +template +CONSTCD11 +inline +string_literal +operator+(const string_literal& x, const string_literal& y) NOEXCEPT +{ + return string_literal(x[0], x[1], y[0]); +} + +template +CONSTCD14 +inline +string_literal::type, + N1 + N2 - 1> +operator+(const string_literal& x, const string_literal& y) NOEXCEPT +{ + using CT = typename std::conditional::type; + + string_literal r; + std::size_t i = 0; + for (; i < N1-1; ++i) + r.p_[i] = CT(x.p_[i]); + for (std::size_t j = 0; j < N2; ++j, ++i) + r.p_[i] = CT(y.p_[j]); + + return r; +} + + +template +inline +std::basic_string +operator+(std::basic_string x, const string_literal& y) +{ + x.append(y.data(), y.size()); + return x; +} + +#if __cplusplus >= 201402 && (!defined(__EDG_VERSION__) || __EDG_VERSION__ > 411) \ + && (!defined(__SUNPRO_CC) || __SUNPRO_CC > 0x5150) + +template ::value || + std::is_same::value || + std::is_same::value || + std::is_same::value>> +CONSTCD14 +inline +string_literal +msl(CharT c) NOEXCEPT +{ + return string_literal{c}; +} + +CONSTCD14 +inline +std::size_t +to_string_len(std::intmax_t i) +{ + std::size_t r = 0; + do + { + i /= 10; + ++r; + } while (i > 0); + return r; +} + +template +CONSTCD14 +inline +std::enable_if_t +< + N < 10, + string_literal +> +msl() NOEXCEPT +{ + return msl(char(N % 10 + '0')); +} + +template +CONSTCD14 +inline +std::enable_if_t +< + 10 <= N, + string_literal +> +msl() NOEXCEPT +{ + return msl() + msl(char(N % 10 + '0')); +} + +template +CONSTCD14 +inline +std::enable_if_t +< + std::ratio::type::den != 1, + string_literal::type::num) + + to_string_len(std::ratio::type::den) + 4> +> +msl(std::ratio) NOEXCEPT +{ + using R = typename std::ratio::type; + return msl(CharT{'['}) + msl() + msl(CharT{'/'}) + + msl() + msl(CharT{']'}); +} + +template +CONSTCD14 +inline +std::enable_if_t +< + std::ratio::type::den == 1, + string_literal::type::num) + 3> +> +msl(std::ratio) NOEXCEPT +{ + using R = typename std::ratio::type; + return msl(CharT{'['}) + msl() + msl(CharT{']'}); +} + + +#else // __cplusplus < 201402 || (defined(__EDG_VERSION__) && __EDG_VERSION__ <= 411) + +inline +std::string +to_string(std::uint64_t x) +{ + return std::to_string(x); +} + +template +inline +std::basic_string +to_string(std::uint64_t x) +{ + auto y = std::to_string(x); + return std::basic_string(y.begin(), y.end()); +} + +template +inline +typename std::enable_if +< + std::ratio::type::den != 1, + std::basic_string +>::type +msl(std::ratio) +{ + using R = typename std::ratio::type; + return std::basic_string(1, '[') + to_string(R::num) + CharT{'/'} + + to_string(R::den) + CharT{']'}; +} + +template +inline +typename std::enable_if +< + std::ratio::type::den == 1, + std::basic_string +>::type +msl(std::ratio) +{ + using R = typename std::ratio::type; + return std::basic_string(1, '[') + to_string(R::num) + CharT{']'}; +} + +#endif // __cplusplus < 201402 || (defined(__EDG_VERSION__) && __EDG_VERSION__ <= 411) + +template +CONSTCD11 +inline +string_literal +msl(std::atto) NOEXCEPT +{ + return string_literal{'a'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::femto) NOEXCEPT +{ + return string_literal{'f'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::pico) NOEXCEPT +{ + return string_literal{'p'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::nano) NOEXCEPT +{ + return string_literal{'n'}; +} + +template +CONSTCD11 +inline +typename std::enable_if +< + std::is_same::value, + string_literal +>::type +msl(std::micro) NOEXCEPT +{ + return string_literal{'\xC2', '\xB5'}; +} + +template +CONSTCD11 +inline +typename std::enable_if +< + !std::is_same::value, + string_literal +>::type +msl(std::micro) NOEXCEPT +{ + return string_literal{CharT{static_cast('\xB5')}}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::milli) NOEXCEPT +{ + return string_literal{'m'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::centi) NOEXCEPT +{ + return string_literal{'c'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::deca) NOEXCEPT +{ + return string_literal{'d', 'a'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::deci) NOEXCEPT +{ + return string_literal{'d'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::hecto) NOEXCEPT +{ + return string_literal{'h'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::kilo) NOEXCEPT +{ + return string_literal{'k'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::mega) NOEXCEPT +{ + return string_literal{'M'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::giga) NOEXCEPT +{ + return string_literal{'G'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::tera) NOEXCEPT +{ + return string_literal{'T'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::peta) NOEXCEPT +{ + return string_literal{'P'}; +} + +template +CONSTCD11 +inline +string_literal +msl(std::exa) NOEXCEPT +{ + return string_literal{'E'}; +} + +template +CONSTCD11 +inline +auto +get_units(Period p) + -> decltype(msl(p) + string_literal{'s'}) +{ + return msl(p) + string_literal{'s'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<1>) +{ + return string_literal{'s'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<3600>) +{ + return string_literal{'h'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<60>) +{ + return string_literal{'m', 'i', 'n'}; +} + +template +CONSTCD11 +inline +string_literal +get_units(std::ratio<86400>) +{ + return string_literal{'d'}; +} + +template > +struct make_string; + +template <> +struct make_string +{ + template + static + std::string + from(Rep n) + { + return std::to_string(n); + } +}; + +template +struct make_string +{ + template + static + std::basic_string + from(Rep n) + { + auto s = std::to_string(n); + return std::basic_string(s.begin(), s.end()); + } +}; + +template <> +struct make_string +{ + template + static + std::wstring + from(Rep n) + { + return std::to_wstring(n); + } +}; + +template +struct make_string +{ + template + static + std::basic_string + from(Rep n) + { + auto s = std::to_wstring(n); + return std::basic_string(s.begin(), s.end()); + } +}; + +} // namespace detail + +// to_stream + +CONSTDATA year nanyear{-32768}; + +template +struct fields +{ + year_month_day ymd{nanyear/0/0}; + weekday wd{8u}; + hh_mm_ss tod{}; + bool has_tod = false; + +#if !defined(__clang__) && defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ <= 409) + fields() : ymd{nanyear/0/0}, wd{8u}, tod{}, has_tod{false} {} +#else + fields() = default; +#endif + + fields(year_month_day ymd_) : ymd(ymd_) {} + fields(weekday wd_) : wd(wd_) {} + fields(hh_mm_ss tod_) : tod(tod_), has_tod(true) {} + + fields(year_month_day ymd_, weekday wd_) : ymd(ymd_), wd(wd_) {} + fields(year_month_day ymd_, hh_mm_ss tod_) : ymd(ymd_), tod(tod_), + has_tod(true) {} + + fields(weekday wd_, hh_mm_ss tod_) : wd(wd_), tod(tod_), has_tod(true) {} + + fields(year_month_day ymd_, weekday wd_, hh_mm_ss tod_) + : ymd(ymd_) + , wd(wd_) + , tod(tod_) + , has_tod(true) + {} +}; + +namespace detail +{ + +template +unsigned +extract_weekday(std::basic_ostream& os, const fields& fds) +{ + if (!fds.ymd.ok() && !fds.wd.ok()) + { + // fds does not contain a valid weekday + os.setstate(std::ios::failbit); + return 8; + } + weekday wd; + if (fds.ymd.ok()) + { + wd = weekday{sys_days(fds.ymd)}; + if (fds.wd.ok() && wd != fds.wd) + { + // fds.ymd and fds.wd are inconsistent + os.setstate(std::ios::failbit); + return 8; + } + } + else + wd = fds.wd; + return static_cast((wd - Sunday).count()); +} + +template +unsigned +extract_month(std::basic_ostream& os, const fields& fds) +{ + if (!fds.ymd.month().ok()) + { + // fds does not contain a valid month + os.setstate(std::ios::failbit); + return 0; + } + return static_cast(fds.ymd.month()); +} + +} // namespace detail + +#if ONLY_C_LOCALE + +namespace detail +{ + +inline +std::pair +weekday_names() +{ + static const std::string nm[] = + { + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + }; + return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); +} + +inline +std::pair +month_names() +{ + static const std::string nm[] = + { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + }; + return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); +} + +inline +std::pair +ampm_names() +{ + static const std::string nm[] = + { + "AM", + "PM" + }; + return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); +} + +template +FwdIter +scan_keyword(std::basic_istream& is, FwdIter kb, FwdIter ke) +{ + size_t nkw = static_cast(std::distance(kb, ke)); + const unsigned char doesnt_match = '\0'; + const unsigned char might_match = '\1'; + const unsigned char does_match = '\2'; + unsigned char statbuf[100]; + unsigned char* status = statbuf; + std::unique_ptr stat_hold(0, free); + if (nkw > sizeof(statbuf)) + { + status = (unsigned char*)std::malloc(nkw); + if (status == nullptr) + throw std::bad_alloc(); + stat_hold.reset(status); + } + size_t n_might_match = nkw; // At this point, any keyword might match + size_t n_does_match = 0; // but none of them definitely do + // Initialize all statuses to might_match, except for "" keywords are does_match + unsigned char* st = status; + for (auto ky = kb; ky != ke; ++ky, ++st) + { + if (!ky->empty()) + *st = might_match; + else + { + *st = does_match; + --n_might_match; + ++n_does_match; + } + } + // While there might be a match, test keywords against the next CharT + for (size_t indx = 0; is && n_might_match > 0; ++indx) + { + // Peek at the next CharT but don't consume it + auto ic = is.peek(); + if (ic == EOF) + { + is.setstate(std::ios::eofbit); + break; + } + auto c = static_cast(toupper(static_cast(ic))); + bool consume = false; + // For each keyword which might match, see if the indx character is c + // If a match if found, consume c + // If a match is found, and that is the last character in the keyword, + // then that keyword matches. + // If the keyword doesn't match this character, then change the keyword + // to doesn't match + st = status; + for (auto ky = kb; ky != ke; ++ky, ++st) + { + if (*st == might_match) + { + if (c == static_cast(toupper(static_cast((*ky)[indx])))) + { + consume = true; + if (ky->size() == indx+1) + { + *st = does_match; + --n_might_match; + ++n_does_match; + } + } + else + { + *st = doesnt_match; + --n_might_match; + } + } + } + // consume if we matched a character + if (consume) + { + (void)is.get(); + // If we consumed a character and there might be a matched keyword that + // was marked matched on a previous iteration, then such keywords + // are now marked as not matching. + if (n_might_match + n_does_match > 1) + { + st = status; + for (auto ky = kb; ky != ke; ++ky, ++st) + { + if (*st == does_match && ky->size() != indx+1) + { + *st = doesnt_match; + --n_does_match; + } + } + } + } + } + // We've exited the loop because we hit eof and/or we have no more "might matches". + // Return the first matching result + for (st = status; kb != ke; ++kb, ++st) + if (*st == does_match) + break; + if (kb == ke) + is.setstate(std::ios::failbit); + return kb; +} + +} // namespace detail + +#endif // ONLY_C_LOCALE + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const fields& fds, const std::string* abbrev, + const std::chrono::seconds* offset_sec) +{ +#if ONLY_C_LOCALE + using detail::weekday_names; + using detail::month_names; + using detail::ampm_names; +#endif + using detail::save_ostream; + using detail::get_units; + using detail::extract_weekday; + using detail::extract_month; + using std::ios; + using std::chrono::duration_cast; + using std::chrono::seconds; + using std::chrono::minutes; + using std::chrono::hours; + date::detail::save_ostream ss(os); + os.fill(' '); + os.flags(std::ios::skipws | std::ios::dec); + os.width(0); + tm tm{}; + bool insert_negative = fds.has_tod && fds.tod.to_duration() < Duration::zero(); +#if !ONLY_C_LOCALE + auto& facet = std::use_facet>(os.getloc()); +#endif + const CharT* command = nullptr; + CharT modified = CharT{}; + for (; *fmt; ++fmt) + { + switch (*fmt) + { + case 'a': + case 'A': + if (command) + { + if (modified == CharT{}) + { + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else // ONLY_C_LOCALE + os << weekday_names().first[tm.tm_wday+7*(*fmt == 'a')]; +#endif // ONLY_C_LOCALE + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'b': + case 'B': + case 'h': + if (command) + { + if (modified == CharT{}) + { + tm.tm_mon = static_cast(extract_month(os, fds)) - 1; +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else // ONLY_C_LOCALE + os << month_names().first[tm.tm_mon+12*(*fmt != 'B')]; +#endif // ONLY_C_LOCALE + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'c': + case 'x': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + if (*fmt == 'c' && !fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + tm = std::tm{}; + auto const& ymd = fds.ymd; + auto ld = local_days(ymd); + if (*fmt == 'c') + { + tm.tm_sec = static_cast(fds.tod.seconds().count()); + tm.tm_min = static_cast(fds.tod.minutes().count()); + tm.tm_hour = static_cast(fds.tod.hours().count()); + } + tm.tm_mday = static_cast(static_cast(ymd.day())); + tm.tm_mon = static_cast(extract_month(os, fds) - 1); + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + CharT f[3] = {'%'}; + auto fe = std::begin(f) + 1; + if (modified == CharT{'E'}) + *fe++ = modified; + *fe++ = *fmt; + facet.put(os, os, os.fill(), &tm, std::begin(f), fe); +#else // ONLY_C_LOCALE + if (*fmt == 'c') + { + auto wd = static_cast(extract_weekday(os, fds)); + os << weekday_names().first[static_cast(wd)+7] + << ' '; + os << month_names().first[extract_month(os, fds)-1+12] << ' '; + auto d = static_cast(static_cast(fds.ymd.day())); + if (d < 10) + os << ' '; + os << d << ' ' + << make_time(duration_cast(fds.tod.to_duration())) + << ' ' << fds.ymd.year(); + + } + else // *fmt == 'x' + { + auto const& ymd = fds.ymd; + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << static_cast(ymd.month()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.day()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.year()) % 100; + } +#endif // ONLY_C_LOCALE + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'C': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.year().ok()) + os.setstate(std::ios::failbit); + auto y = static_cast(fds.ymd.year()); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + if (y >= 0) + { + os.width(2); + os << y/100; + } + else + { + os << CharT{'-'}; + os.width(2); + os << -(y-99)/100; + } + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'E'}) + { + tm.tm_year = y - 1900; + CharT f[3] = {'%', 'E', 'C'}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'd': + case 'e': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.day().ok()) + os.setstate(std::ios::failbit); + auto d = static_cast(static_cast(fds.ymd.day())); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + save_ostream _(os); + if (*fmt == CharT{'d'}) + os.fill('0'); + else + os.fill(' '); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << d; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + tm.tm_mday = d; + CharT f[3] = {'%', 'O', *fmt}; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'D': + if (command) + { + if (modified == CharT{}) + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto const& ymd = fds.ymd; + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << static_cast(ymd.month()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.day()) << CharT{'/'}; + os.width(2); + os << static_cast(ymd.year()) % 100; + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'F': + if (command) + { + if (modified == CharT{}) + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto const& ymd = fds.ymd; + save_ostream _(os); + os.imbue(std::locale::classic()); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(4); + os << static_cast(ymd.year()) << CharT{'-'}; + os.width(2); + os << static_cast(ymd.month()) << CharT{'-'}; + os.width(2); + os << static_cast(ymd.day()); + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'g': + case 'G': + if (command) + { + if (modified == CharT{}) + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(fds.ymd); + auto y = year_month_day{ld + days{3}}.year(); + auto start = local_days((y-years{1})/December/Thursday[last]) + + (Monday-Thursday); + if (ld < start) + --y; + if (*fmt == CharT{'G'}) + os << y; + else + { + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(2); + os << std::abs(static_cast(y)) % 100; + } + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'H': + case 'I': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (insert_negative) + { + os << '-'; + insert_negative = false; + } + auto hms = fds.tod; +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto h = *fmt == CharT{'I'} ? date::make12(hms.hours()) : hms.hours(); + if (h < hours{10}) + os << CharT{'0'}; + os << h.count(); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_hour = static_cast(hms.hours().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'j': + if (command) + { + if (modified == CharT{}) + { + if (fds.ymd.ok() || fds.has_tod) + { + days doy; + if (fds.ymd.ok()) + { + auto ld = local_days(fds.ymd); + auto y = fds.ymd.year(); + doy = ld - local_days(y/January/1) + days{1}; + } + else + { + doy = duration_cast(fds.tod.to_duration()); + } + save_ostream _(os); + os.fill('0'); + os.flags(std::ios::dec | std::ios::right); + os.width(3); + os << doy.count(); + } + else + { + os.setstate(std::ios::failbit); + } + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'm': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.month().ok()) + os.setstate(std::ios::failbit); + auto m = static_cast(fds.ymd.month()); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + if (m < 10) + os << CharT{'0'}; + os << m; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_mon = static_cast(m-1); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'M': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (insert_negative) + { + os << '-'; + insert_negative = false; + } +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + if (fds.tod.minutes() < minutes{10}) + os << CharT{'0'}; + os << fds.tod.minutes().count(); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_min = static_cast(fds.tod.minutes().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'n': + if (command) + { + if (modified == CharT{}) + os << CharT{'\n'}; + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'p': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + tm.tm_hour = static_cast(fds.tod.hours().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else + if (date::is_am(fds.tod.hours())) + os << ampm_names().first[0]; + else + os << ampm_names().first[1]; +#endif + } + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'Q': + case 'q': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + auto d = fds.tod.to_duration(); + if (*fmt == 'q') + os << get_units(typename decltype(d)::period::type{}); + else + os << d.count(); + } + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'r': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + const CharT f[] = {'%', *fmt}; + tm.tm_hour = static_cast(fds.tod.hours().count()); + tm.tm_min = static_cast(fds.tod.minutes().count()); + tm.tm_sec = static_cast(fds.tod.seconds().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); +#else + hh_mm_ss tod(duration_cast(fds.tod.to_duration())); + save_ostream _(os); + os.fill('0'); + os.width(2); + os << date::make12(tod.hours()).count() << CharT{':'}; + os.width(2); + os << tod.minutes().count() << CharT{':'}; + os.width(2); + os << tod.seconds().count() << CharT{' '}; + if (date::is_am(tod.hours())) + os << ampm_names().first[0]; + else + os << ampm_names().first[1]; +#endif + } + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'R': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (fds.tod.hours() < hours{10}) + os << CharT{'0'}; + os << fds.tod.hours().count() << CharT{':'}; + if (fds.tod.minutes() < minutes{10}) + os << CharT{'0'}; + os << fds.tod.minutes().count(); + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'S': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + if (insert_negative) + { + os << '-'; + insert_negative = false; + } +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + os << fds.tod.s_; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_sec = static_cast(fds.tod.s_.seconds().count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 't': + if (command) + { + if (modified == CharT{}) + os << CharT{'\t'}; + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'T': + if (command) + { + if (modified == CharT{}) + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); + os << fds.tod; + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'u': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + auto wd = extract_weekday(os, fds); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + os << (wd != 0 ? wd : 7u); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_wday = static_cast(wd); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'U': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + auto const& ymd = fds.ymd; + if (!ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(ymd); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto st = local_days(Sunday[1]/January/ymd.year()); + if (ld < st) + os << CharT{'0'} << CharT{'0'}; + else + { + auto wn = duration_cast(ld - st).count() + 1; + if (wn < 10) + os << CharT{'0'}; + os << wn; + } + } + #if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'V': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(fds.ymd); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto y = year_month_day{ld + days{3}}.year(); + auto st = local_days((y-years{1})/12/Thursday[last]) + + (Monday-Thursday); + if (ld < st) + { + --y; + st = local_days((y - years{1})/12/Thursday[last]) + + (Monday-Thursday); + } + auto wn = duration_cast(ld - st).count() + 1; + if (wn < 10) + os << CharT{'0'}; + os << wn; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + auto const& ymd = fds.ymd; + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'w': + if (command) + { + auto wd = extract_weekday(os, fds); + if (os.fail()) + return os; +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + os << wd; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_wday = static_cast(wd); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + else + { + os << CharT{'%'} << modified << *fmt; + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'W': + if (command) + { + if (modified == CharT{'E'}) + os << CharT{'%'} << modified << *fmt; + else + { + auto const& ymd = fds.ymd; + if (!ymd.ok()) + os.setstate(std::ios::failbit); + auto ld = local_days(ymd); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + auto st = local_days(Monday[1]/January/ymd.year()); + if (ld < st) + os << CharT{'0'} << CharT{'0'}; + else + { + auto wn = duration_cast(ld - st).count() + 1; + if (wn < 10) + os << CharT{'0'}; + os << wn; + } + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = static_cast(ymd.year()) - 1900; + tm.tm_wday = static_cast(extract_weekday(os, fds)); + if (os.fail()) + return os; + tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'X': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.has_tod) + os.setstate(std::ios::failbit); +#if !ONLY_C_LOCALE + tm = std::tm{}; + tm.tm_sec = static_cast(fds.tod.seconds().count()); + tm.tm_min = static_cast(fds.tod.minutes().count()); + tm.tm_hour = static_cast(fds.tod.hours().count()); + CharT f[3] = {'%'}; + auto fe = std::begin(f) + 1; + if (modified == CharT{'E'}) + *fe++ = modified; + *fe++ = *fmt; + facet.put(os, os, os.fill(), &tm, std::begin(f), fe); +#else + os << fds.tod; +#endif + } + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'y': + if (command) + { + if (!fds.ymd.year().ok()) + os.setstate(std::ios::failbit); + auto y = static_cast(fds.ymd.year()); +#if !ONLY_C_LOCALE + if (modified == CharT{}) + { +#endif + y = std::abs(y) % 100; + if (y < 10) + os << CharT{'0'}; + os << y; +#if !ONLY_C_LOCALE + } + else + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = y - 1900; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'Y': + if (command) + { + if (modified == CharT{'O'}) + os << CharT{'%'} << modified << *fmt; + else + { + if (!fds.ymd.year().ok()) + os.setstate(std::ios::failbit); + auto y = fds.ymd.year(); +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + save_ostream _(os); + os.imbue(std::locale::classic()); + os << y; + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'E'}) + { + const CharT f[] = {'%', modified, *fmt}; + tm.tm_year = static_cast(y) - 1900; + facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); + } +#endif + } + modified = CharT{}; + command = nullptr; + } + else + os << *fmt; + break; + case 'z': + if (command) + { + if (offset_sec == nullptr) + { + // Can not format %z with unknown offset + os.setstate(ios::failbit); + return os; + } + auto m = duration_cast(*offset_sec); + auto neg = m < minutes{0}; + m = date::abs(m); + auto h = duration_cast(m); + m -= h; + if (neg) + os << CharT{'-'}; + else + os << CharT{'+'}; + if (h < hours{10}) + os << CharT{'0'}; + os << h.count(); + if (modified != CharT{}) + os << CharT{':'}; + if (m < minutes{10}) + os << CharT{'0'}; + os << m.count(); + command = nullptr; + modified = CharT{}; + } + else + os << *fmt; + break; + case 'Z': + if (command) + { + if (modified == CharT{}) + { + if (abbrev == nullptr) + { + // Can not format %Z with unknown time_zone + os.setstate(ios::failbit); + return os; + } + for (auto c : *abbrev) + os << CharT(c); + } + else + { + os << CharT{'%'} << modified << *fmt; + modified = CharT{}; + } + command = nullptr; + } + else + os << *fmt; + break; + case 'E': + case 'O': + if (command) + { + if (modified == CharT{}) + { + modified = *fmt; + } + else + { + os << CharT{'%'} << modified << *fmt; + command = nullptr; + modified = CharT{}; + } + } + else + os << *fmt; + break; + case '%': + if (command) + { + if (modified == CharT{}) + { + os << CharT{'%'}; + command = nullptr; + } + else + { + os << CharT{'%'} << modified << CharT{'%'}; + command = nullptr; + modified = CharT{}; + } + } + else + command = fmt; + break; + default: + if (command) + { + os << CharT{'%'}; + command = nullptr; + } + if (modified != CharT{}) + { + os << modified; + modified = CharT{}; + } + os << *fmt; + break; + } + } + if (command) + os << CharT{'%'}; + if (modified != CharT{}) + os << modified; + return os; +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const year& y) +{ + using CT = std::chrono::seconds; + fields fds{y/0/0}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const month& m) +{ + using CT = std::chrono::seconds; + fields fds{m/0/nanyear}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const day& d) +{ + using CT = std::chrono::seconds; + fields fds{d/0/nanyear}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const weekday& wd) +{ + using CT = std::chrono::seconds; + fields fds{wd}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const year_month& ym) +{ + using CT = std::chrono::seconds; + fields fds{ym/0}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, const month_day& md) +{ + using CT = std::chrono::seconds; + fields fds{md/nanyear}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const year_month_day& ymd) +{ + using CT = std::chrono::seconds; + fields fds{ymd}; + return to_stream(os, fmt, fds); +} + +template +inline +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const std::chrono::duration& d) +{ + using Duration = std::chrono::duration; + using CT = typename std::common_type::type; + fields fds{hh_mm_ss{d}}; + return to_stream(os, fmt, fds); +} + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const local_time& tp, const std::string* abbrev = nullptr, + const std::chrono::seconds* offset_sec = nullptr) +{ + using CT = typename std::common_type::type; + auto ld = std::chrono::time_point_cast(tp); + fields fds; + if (ld <= tp) + fds = fields{year_month_day{ld}, hh_mm_ss{tp-local_seconds{ld}}}; + else + fds = fields{year_month_day{ld - days{1}}, + hh_mm_ss{days{1} - (local_seconds{ld} - tp)}}; + return to_stream(os, fmt, fds, abbrev, offset_sec); +} + +template +std::basic_ostream& +to_stream(std::basic_ostream& os, const CharT* fmt, + const sys_time& tp) +{ + using std::chrono::seconds; + using CT = typename std::common_type::type; + const std::string abbrev("UTC"); + CONSTDATA seconds offset{0}; + auto sd = std::chrono::time_point_cast(tp); + fields fds; + if (sd <= tp) + fds = fields{year_month_day{sd}, hh_mm_ss{tp-sys_seconds{sd}}}; + else + fds = fields{year_month_day{sd - days{1}}, + hh_mm_ss{days{1} - (sys_seconds{sd} - tp)}}; + return to_stream(os, fmt, fds, &abbrev, &offset); +} + +// format + +template +auto +format(const std::locale& loc, const CharT* fmt, const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt, tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + os.imbue(loc); + to_stream(os, fmt, tp); + return os.str(); +} + +template +auto +format(const CharT* fmt, const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt, tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + to_stream(os, fmt, tp); + return os.str(); +} + +template +auto +format(const std::locale& loc, const std::basic_string& fmt, + const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt.c_str(), tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + os.imbue(loc); + to_stream(os, fmt.c_str(), tp); + return os.str(); +} + +template +auto +format(const std::basic_string& fmt, const Streamable& tp) + -> decltype(to_stream(std::declval&>(), fmt.c_str(), tp), + std::basic_string{}) +{ + std::basic_ostringstream os; + os.exceptions(std::ios::failbit | std::ios::badbit); + to_stream(os, fmt.c_str(), tp); + return os.str(); +} + +// parse + +namespace detail +{ + +template +bool +read_char(std::basic_istream& is, CharT fmt, std::ios::iostate& err) +{ + auto ic = is.get(); + if (Traits::eq_int_type(ic, Traits::eof()) || + !Traits::eq(Traits::to_char_type(ic), fmt)) + { + err |= std::ios::failbit; + is.setstate(std::ios::failbit); + return false; + } + return true; +} + +template +unsigned +read_unsigned(std::basic_istream& is, unsigned m = 1, unsigned M = 10) +{ + unsigned x = 0; + unsigned count = 0; + while (true) + { + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + break; + auto c = static_cast(Traits::to_char_type(ic)); + if (!('0' <= c && c <= '9')) + break; + (void)is.get(); + ++count; + x = 10*x + static_cast(c - '0'); + if (count == M) + break; + } + if (count < m) + is.setstate(std::ios::failbit); + return x; +} + +template +int +read_signed(std::basic_istream& is, unsigned m = 1, unsigned M = 10) +{ + auto ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if (('0' <= c && c <= '9') || c == '-' || c == '+') + { + if (c == '-' || c == '+') + (void)is.get(); + auto x = static_cast(read_unsigned(is, std::max(m, 1u), M)); + if (!is.fail()) + { + if (c == '-') + x = -x; + return x; + } + } + } + if (m > 0) + is.setstate(std::ios::failbit); + return 0; +} + +template +long double +read_long_double(std::basic_istream& is, unsigned m = 1, unsigned M = 10) +{ + unsigned count = 0; + unsigned fcount = 0; + unsigned long long i = 0; + unsigned long long f = 0; + bool parsing_fraction = false; +#if ONLY_C_LOCALE + typename Traits::int_type decimal_point = '.'; +#else + auto decimal_point = Traits::to_int_type( + std::use_facet>(is.getloc()).decimal_point()); +#endif + while (true) + { + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + break; + if (Traits::eq_int_type(ic, decimal_point)) + { + decimal_point = Traits::eof(); + parsing_fraction = true; + } + else + { + auto c = static_cast(Traits::to_char_type(ic)); + if (!('0' <= c && c <= '9')) + break; + if (!parsing_fraction) + { + i = 10*i + static_cast(c - '0'); + } + else + { + f = 10*f + static_cast(c - '0'); + ++fcount; + } + } + (void)is.get(); + if (++count == M) + break; + } + if (count < m) + { + is.setstate(std::ios::failbit); + return 0; + } + return static_cast(i) + static_cast(f)/std::pow(10.L, fcount); +} + +struct rs +{ + int& i; + unsigned m; + unsigned M; +}; + +struct ru +{ + int& i; + unsigned m; + unsigned M; +}; + +struct rld +{ + long double& i; + unsigned m; + unsigned M; +}; + +template +void +read(std::basic_istream&) +{ +} + +template +void +read(std::basic_istream& is, CharT a0, Args&& ...args); + +template +void +read(std::basic_istream& is, rs a0, Args&& ...args); + +template +void +read(std::basic_istream& is, ru a0, Args&& ...args); + +template +void +read(std::basic_istream& is, int a0, Args&& ...args); + +template +void +read(std::basic_istream& is, rld a0, Args&& ...args); + +template +void +read(std::basic_istream& is, CharT a0, Args&& ...args) +{ + // No-op if a0 == CharT{} + if (a0 != CharT{}) + { + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + { + is.setstate(std::ios::failbit | std::ios::eofbit); + return; + } + if (!Traits::eq(Traits::to_char_type(ic), a0)) + { + is.setstate(std::ios::failbit); + return; + } + (void)is.get(); + } + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, rs a0, Args&& ...args) +{ + auto x = read_signed(is, a0.m, a0.M); + if (is.fail()) + return; + a0.i = x; + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, ru a0, Args&& ...args) +{ + auto x = read_unsigned(is, a0.m, a0.M); + if (is.fail()) + return; + a0.i = static_cast(x); + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, int a0, Args&& ...args) +{ + if (a0 != -1) + { + auto u = static_cast(a0); + CharT buf[std::numeric_limits::digits10+2u] = {}; + auto e = buf; + do + { + *e++ = static_cast(CharT(u % 10) + CharT{'0'}); + u /= 10; + } while (u > 0); + std::reverse(buf, e); + for (auto p = buf; p != e && is.rdstate() == std::ios::goodbit; ++p) + read(is, *p); + } + if (is.rdstate() == std::ios::goodbit) + read(is, std::forward(args)...); +} + +template +void +read(std::basic_istream& is, rld a0, Args&& ...args) +{ + auto x = read_long_double(is, a0.m, a0.M); + if (is.fail()) + return; + a0.i = x; + read(is, std::forward(args)...); +} + +template +inline +void +checked_set(T& value, T from, T not_a_value, std::basic_ios& is) +{ + if (!is.fail()) + { + if (value == not_a_value) + value = std::move(from); + else if (value != from) + is.setstate(std::ios::failbit); + } +} + +} // namespace detail; + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + fields& fds, std::basic_string* abbrev, + std::chrono::minutes* offset) +{ + using std::numeric_limits; + using std::ios; + using std::chrono::duration; + using std::chrono::duration_cast; + using std::chrono::seconds; + using std::chrono::minutes; + using std::chrono::hours; + using detail::round_i; + typename std::basic_istream::sentry ok{is, true}; + if (ok) + { + date::detail::save_istream ss(is); + is.fill(' '); + is.flags(std::ios::skipws | std::ios::dec); + is.width(0); +#if !ONLY_C_LOCALE + auto& f = std::use_facet>(is.getloc()); + std::tm tm{}; +#endif + const CharT* command = nullptr; + auto modified = CharT{}; + auto width = -1; + + CONSTDATA int not_a_year = numeric_limits::min(); + CONSTDATA int not_a_2digit_year = 100; + CONSTDATA int not_a_century = not_a_year / 100; + CONSTDATA int not_a_month = 0; + CONSTDATA int not_a_day = 0; + CONSTDATA int not_a_hour = numeric_limits::min(); + CONSTDATA int not_a_hour_12_value = 0; + CONSTDATA int not_a_minute = not_a_hour; + CONSTDATA Duration not_a_second = Duration::min(); + CONSTDATA int not_a_doy = -1; + CONSTDATA int not_a_weekday = 8; + CONSTDATA int not_a_week_num = 100; + CONSTDATA int not_a_ampm = -1; + CONSTDATA minutes not_a_offset = minutes::min(); + + int Y = not_a_year; // c, F, Y * + int y = not_a_2digit_year; // D, x, y * + int g = not_a_2digit_year; // g * + int G = not_a_year; // G * + int C = not_a_century; // C * + int m = not_a_month; // b, B, h, m, c, D, F, x * + int d = not_a_day; // c, d, D, e, F, x * + int j = not_a_doy; // j * + int wd = not_a_weekday; // a, A, u, w * + int H = not_a_hour; // c, H, R, T, X * + int I = not_a_hour_12_value; // I, r * + int p = not_a_ampm; // p, r * + int M = not_a_minute; // c, M, r, R, T, X * + Duration s = not_a_second; // c, r, S, T, X * + int U = not_a_week_num; // U * + int V = not_a_week_num; // V * + int W = not_a_week_num; // W * + std::basic_string temp_abbrev; // Z * + minutes temp_offset = not_a_offset; // z * + + using detail::read; + using detail::rs; + using detail::ru; + using detail::rld; + using detail::checked_set; + for (; *fmt != CharT{} && !is.fail(); ++fmt) + { + switch (*fmt) + { + case 'a': + case 'A': + case 'u': + case 'w': // wd: a, A, u, w + if (command) + { + int trial_wd = not_a_weekday; + if (*fmt == 'a' || *fmt == 'A') + { + if (modified == CharT{}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + is.setstate(err); + if (!is.fail()) + trial_wd = tm.tm_wday; +#else + auto nm = detail::weekday_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + if (!is.fail()) + trial_wd = i % 7; +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + } + else // *fmt == 'u' || *fmt == 'w' + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + read(is, ru{trial_wd, 1, width == -1 ? + 1u : static_cast(width)}); + if (!is.fail()) + { + if (*fmt == 'u') + { + if (!(1 <= trial_wd && trial_wd <= 7)) + { + trial_wd = not_a_weekday; + is.setstate(ios::failbit); + } + else if (trial_wd == 7) + trial_wd = 0; + } + else // *fmt == 'w' + { + if (!(0 <= trial_wd && trial_wd <= 6)) + { + trial_wd = not_a_weekday; + is.setstate(ios::failbit); + } + } + } + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + is.setstate(err); + if (!is.fail()) + trial_wd = tm.tm_wday; + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + } + if (trial_wd != not_a_weekday) + checked_set(wd, trial_wd, not_a_weekday, is); + } + else // !command + read(is, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + break; + case 'b': + case 'B': + case 'h': + if (command) + { + if (modified == CharT{}) + { + int ttm = not_a_month; +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + ttm = tm.tm_mon + 1; + is.setstate(err); +#else + auto nm = detail::month_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + if (!is.fail()) + ttm = i % 12 + 1; +#endif + checked_set(m, ttm, not_a_month, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'c': + if (command) + { + if (modified != CharT{'O'}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + checked_set(m, tm.tm_mon + 1, not_a_month, is); + checked_set(d, tm.tm_mday, not_a_day, is); + checked_set(H, tm.tm_hour, not_a_hour, is); + checked_set(M, tm.tm_min, not_a_minute, is); + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + } + is.setstate(err); +#else + // "%a %b %e %T %Y" + auto nm = detail::weekday_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + checked_set(wd, static_cast(i % 7), not_a_weekday, is); + ws(is); + nm = detail::month_names(); + i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + checked_set(m, static_cast(i % 12 + 1), not_a_month, is); + ws(is); + int td = not_a_day; + read(is, rs{td, 1, 2}); + checked_set(d, td, not_a_day, is); + ws(is); + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + int tH; + int tM; + long double S{}; + read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); + ws(is); + int tY = not_a_year; + read(is, rs{tY, 1, 4u}); + checked_set(Y, tY, not_a_year, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'x': + if (command) + { + if (modified != CharT{'O'}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + checked_set(m, tm.tm_mon + 1, not_a_month, is); + checked_set(d, tm.tm_mday, not_a_day, is); + } + is.setstate(err); +#else + // "%m/%d/%y" + int ty = not_a_2digit_year; + int tm = not_a_month; + int td = not_a_day; + read(is, ru{tm, 1, 2}, CharT{'/'}, ru{td, 1, 2}, CharT{'/'}, + rs{ty, 1, 2}); + checked_set(y, ty, not_a_2digit_year, is); + checked_set(m, tm, not_a_month, is); + checked_set(d, td, not_a_day, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'X': + if (command) + { + if (modified != CharT{'O'}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(H, tm.tm_hour, not_a_hour, is); + checked_set(M, tm.tm_min, not_a_minute, is); + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + } + is.setstate(err); +#else + // "%T" + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + int tH = not_a_hour; + int tM = not_a_minute; + long double S{}; + read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'C': + if (command) + { + int tC = not_a_century; +#if !ONLY_C_LOCALE + if (modified == CharT{}) + { +#endif + read(is, rs{tC, 1, width == -1 ? 2u : static_cast(width)}); +#if !ONLY_C_LOCALE + } + else + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + auto tY = tm.tm_year + 1900; + tC = (tY >= 0 ? tY : tY-99) / 100; + } + is.setstate(err); + } +#endif + checked_set(C, tC, not_a_century, is); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'D': + if (command) + { + if (modified == CharT{}) + { + int tn = not_a_month; + int td = not_a_day; + int ty = not_a_2digit_year; + read(is, ru{tn, 1, 2}, CharT{'\0'}, CharT{'/'}, CharT{'\0'}, + ru{td, 1, 2}, CharT{'\0'}, CharT{'/'}, CharT{'\0'}, + rs{ty, 1, 2}); + checked_set(y, ty, not_a_2digit_year, is); + checked_set(m, tn, not_a_month, is); + checked_set(d, td, not_a_day, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'F': + if (command) + { + if (modified == CharT{}) + { + int tY = not_a_year; + int tn = not_a_month; + int td = not_a_day; + read(is, rs{tY, 1, width == -1 ? 4u : static_cast(width)}, + CharT{'-'}, ru{tn, 1, 2}, CharT{'-'}, ru{td, 1, 2}); + checked_set(Y, tY, not_a_year, is); + checked_set(m, tn, not_a_month, is); + checked_set(d, td, not_a_day, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'd': + case 'e': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int td = not_a_day; + read(is, rs{td, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(d, td, not_a_day, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + command = nullptr; + width = -1; + modified = CharT{}; + if ((err & ios::failbit) == 0) + checked_set(d, tm.tm_mday, not_a_day, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'H': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int tH = not_a_hour; + read(is, ru{tH, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(H, tH, not_a_hour, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(H, tm.tm_hour, not_a_hour, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'I': + if (command) + { + if (modified == CharT{}) + { + int tI = not_a_hour_12_value; + // reads in an hour into I, but most be in [1, 12] + read(is, rs{tI, 1, width == -1 ? 2u : static_cast(width)}); + if (!(1 <= tI && tI <= 12)) + is.setstate(ios::failbit); + checked_set(I, tI, not_a_hour_12_value, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'j': + if (command) + { + if (modified == CharT{}) + { + int tj = not_a_doy; + read(is, ru{tj, 1, width == -1 ? 3u : static_cast(width)}); + checked_set(j, tj, not_a_doy, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'M': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int tM = not_a_minute; + read(is, ru{tM, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(M, tM, not_a_minute, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(M, tm.tm_min, not_a_minute, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'm': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + int tn = not_a_month; + read(is, rs{tn, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(m, tn, not_a_month, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(m, tm.tm_mon + 1, not_a_month, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'n': + case 't': + if (command) + { + if (modified == CharT{}) + { + // %n matches a single white space character + // %t matches 0 or 1 white space characters + auto ic = is.peek(); + if (Traits::eq_int_type(ic, Traits::eof())) + { + ios::iostate err = ios::eofbit; + if (*fmt == 'n') + err |= ios::failbit; + is.setstate(err); + break; + } + if (isspace(ic)) + { + (void)is.get(); + } + else if (*fmt == 'n') + is.setstate(ios::failbit); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'p': + if (command) + { + if (modified == CharT{}) + { + int tp = not_a_ampm; +#if !ONLY_C_LOCALE + tm = std::tm{}; + tm.tm_hour = 1; + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + is.setstate(err); + if (tm.tm_hour == 1) + tp = 0; + else if (tm.tm_hour == 13) + tp = 1; + else + is.setstate(err); +#else + auto nm = detail::ampm_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + tp = static_cast(i); +#endif + checked_set(p, tp, not_a_ampm, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + + break; + case 'r': + if (command) + { + if (modified == CharT{}) + { +#if !ONLY_C_LOCALE + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + { + checked_set(H, tm.tm_hour, not_a_hour, is); + checked_set(M, tm.tm_min, not_a_hour, is); + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + } + is.setstate(err); +#else + // "%I:%M:%S %p" + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + long double S{}; + int tI = not_a_hour_12_value; + int tM = not_a_minute; + read(is, ru{tI, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(I, tI, not_a_hour_12_value, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); + ws(is); + auto nm = detail::ampm_names(); + auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; + checked_set(p, static_cast(i), not_a_ampm, is); +#endif + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'R': + if (command) + { + if (modified == CharT{}) + { + int tH = not_a_hour; + int tM = not_a_minute; + read(is, ru{tH, 1, 2}, CharT{'\0'}, CharT{':'}, CharT{'\0'}, + ru{tM, 1, 2}, CharT{'\0'}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'S': + if (command) + { + #if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'E'}) +#endif + { + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + long double S{}; + read(is, rld{S, 1, width == -1 ? w : static_cast(width)}); + checked_set(s, round_i(duration{S}), + not_a_second, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'O'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(s, duration_cast(seconds{tm.tm_sec}), + not_a_second, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'T': + if (command) + { + if (modified == CharT{}) + { + using dfs = detail::decimal_format_seconds; + CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; + int tH = not_a_hour; + int tM = not_a_minute; + long double S{}; + read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, + CharT{':'}, rld{S, 1, w}); + checked_set(H, tH, not_a_hour, is); + checked_set(M, tM, not_a_minute, is); + checked_set(s, round_i(duration{S}), + not_a_second, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'Y': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#else + if (modified != CharT{'O'}) +#endif + { + int tY = not_a_year; + read(is, rs{tY, 1, width == -1 ? 4u : static_cast(width)}); + checked_set(Y, tY, not_a_year, is); + } +#if !ONLY_C_LOCALE + else if (modified == CharT{'E'}) + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + is.setstate(err); + } +#endif + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'y': + if (command) + { +#if !ONLY_C_LOCALE + if (modified == CharT{}) +#endif + { + int ty = not_a_2digit_year; + read(is, ru{ty, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(y, ty, not_a_2digit_year, is); + } +#if !ONLY_C_LOCALE + else + { + ios::iostate err = ios::goodbit; + f.get(is, nullptr, is, err, &tm, command, fmt+1); + if ((err & ios::failbit) == 0) + checked_set(Y, tm.tm_year + 1900, not_a_year, is); + is.setstate(err); + } +#endif + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'g': + if (command) + { + if (modified == CharT{}) + { + int tg = not_a_2digit_year; + read(is, ru{tg, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(g, tg, not_a_2digit_year, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'G': + if (command) + { + if (modified == CharT{}) + { + int tG = not_a_year; + read(is, rs{tG, 1, width == -1 ? 4u : static_cast(width)}); + checked_set(G, tG, not_a_year, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'U': + if (command) + { + if (modified == CharT{}) + { + int tU = not_a_week_num; + read(is, ru{tU, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(U, tU, not_a_week_num, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'V': + if (command) + { + if (modified == CharT{}) + { + int tV = not_a_week_num; + read(is, ru{tV, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(V, tV, not_a_week_num, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'W': + if (command) + { + if (modified == CharT{}) + { + int tW = not_a_week_num; + read(is, ru{tW, 1, width == -1 ? 2u : static_cast(width)}); + checked_set(W, tW, not_a_week_num, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'E': + case 'O': + if (command) + { + if (modified == CharT{}) + { + modified = *fmt; + } + else + { + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + } + else + read(is, *fmt); + break; + case '%': + if (command) + { + if (modified == CharT{}) + read(is, *fmt); + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + command = fmt; + break; + case 'z': + if (command) + { + int tH, tM; + minutes toff = not_a_offset; + bool neg = false; + auto ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if (c == '-') + neg = true; + } + if (modified == CharT{}) + { + read(is, rs{tH, 2, 2}); + if (!is.fail()) + toff = hours{std::abs(tH)}; + if (is.good()) + { + ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if ('0' <= c && c <= '9') + { + read(is, ru{tM, 2, 2}); + if (!is.fail()) + toff += minutes{tM}; + } + } + } + } + else + { + read(is, rs{tH, 1, 2}); + if (!is.fail()) + toff = hours{std::abs(tH)}; + if (is.good()) + { + ic = is.peek(); + if (!Traits::eq_int_type(ic, Traits::eof())) + { + auto c = static_cast(Traits::to_char_type(ic)); + if (c == ':') + { + (void)is.get(); + read(is, ru{tM, 2, 2}); + if (!is.fail()) + toff += minutes{tM}; + } + } + } + } + if (neg) + toff = -toff; + checked_set(temp_offset, toff, not_a_offset, is); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + case 'Z': + if (command) + { + if (modified == CharT{}) + { + std::basic_string buf; + while (is.rdstate() == std::ios::goodbit) + { + auto i = is.rdbuf()->sgetc(); + if (Traits::eq_int_type(i, Traits::eof())) + { + is.setstate(ios::eofbit); + break; + } + auto wc = Traits::to_char_type(i); + auto c = static_cast(wc); + // is c a valid time zone name or abbreviation character? + if (!(CharT{1} < wc && wc < CharT{127}) || !(isalnum(c) || + c == '_' || c == '/' || c == '-' || c == '+')) + break; + buf.push_back(c); + is.rdbuf()->sbumpc(); + } + if (buf.empty()) + is.setstate(ios::failbit); + checked_set(temp_abbrev, buf, {}, is); + } + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + else + read(is, *fmt); + break; + default: + if (command) + { + if (width == -1 && modified == CharT{} && '0' <= *fmt && *fmt <= '9') + { + width = static_cast(*fmt) - '0'; + while ('0' <= fmt[1] && fmt[1] <= '9') + width = 10*width + static_cast(*++fmt) - '0'; + } + else + { + if (modified == CharT{}) + read(is, CharT{'%'}, width, *fmt); + else + read(is, CharT{'%'}, width, modified, *fmt); + command = nullptr; + width = -1; + modified = CharT{}; + } + } + else // !command + { + if (isspace(static_cast(*fmt))) + { + // space matches 0 or more white space characters + if (is.good()) + ws(is); + } + else + read(is, *fmt); + } + break; + } + } + // is.fail() || *fmt == CharT{} + if (is.rdstate() == ios::goodbit && command) + { + if (modified == CharT{}) + read(is, CharT{'%'}, width); + else + read(is, CharT{'%'}, width, modified); + } + if (!is.fail()) + { + if (y != not_a_2digit_year) + { + // Convert y and an optional C to Y + if (!(0 <= y && y <= 99)) + goto broken; + if (C == not_a_century) + { + if (Y == not_a_year) + { + if (y >= 69) + C = 19; + else + C = 20; + } + else + { + C = (Y >= 0 ? Y : Y-100) / 100; + } + } + int tY; + if (C >= 0) + tY = 100*C + y; + else + tY = 100*(C+1) - (y == 0 ? 100 : y); + if (Y != not_a_year && Y != tY) + goto broken; + Y = tY; + } + if (g != not_a_2digit_year) + { + // Convert g and an optional C to G + if (!(0 <= g && g <= 99)) + goto broken; + if (C == not_a_century) + { + if (G == not_a_year) + { + if (g >= 69) + C = 19; + else + C = 20; + } + else + { + C = (G >= 0 ? G : G-100) / 100; + } + } + int tG; + if (C >= 0) + tG = 100*C + g; + else + tG = 100*(C+1) - (g == 0 ? 100 : g); + if (G != not_a_year && G != tG) + goto broken; + G = tG; + } + if (Y < static_cast(year::min()) || Y > static_cast(year::max())) + Y = not_a_year; + bool computed = false; + if (G != not_a_year && V != not_a_week_num && wd != not_a_weekday) + { + year_month_day ymd_trial = sys_days(year{G-1}/December/Thursday[last]) + + (Monday-Thursday) + weeks{V-1} + + (weekday{static_cast(wd)}-Monday); + if (Y == not_a_year) + Y = static_cast(ymd_trial.year()); + else if (year{Y} != ymd_trial.year()) + goto broken; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + computed = true; + } + if (Y != not_a_year && U != not_a_week_num && wd != not_a_weekday) + { + year_month_day ymd_trial = sys_days(year{Y}/January/Sunday[1]) + + weeks{U-1} + + (weekday{static_cast(wd)} - Sunday); + if (Y == not_a_year) + Y = static_cast(ymd_trial.year()); + else if (year{Y} != ymd_trial.year()) + goto broken; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + computed = true; + } + if (Y != not_a_year && W != not_a_week_num && wd != not_a_weekday) + { + year_month_day ymd_trial = sys_days(year{Y}/January/Monday[1]) + + weeks{W-1} + + (weekday{static_cast(wd)} - Monday); + if (Y == not_a_year) + Y = static_cast(ymd_trial.year()); + else if (year{Y} != ymd_trial.year()) + goto broken; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + computed = true; + } + if (j != not_a_doy && Y != not_a_year) + { + auto ymd_trial = year_month_day{local_days(year{Y}/1/1) + days{j-1}}; + if (m == not_a_month) + m = static_cast(static_cast(ymd_trial.month())); + else if (month(static_cast(m)) != ymd_trial.month()) + goto broken; + if (d == not_a_day) + d = static_cast(static_cast(ymd_trial.day())); + else if (day(static_cast(d)) != ymd_trial.day()) + goto broken; + j = not_a_doy; + } + auto ymd = year{Y}/m/d; + if (ymd.ok()) + { + if (wd == not_a_weekday) + wd = static_cast((weekday(sys_days(ymd)) - Sunday).count()); + else if (wd != static_cast((weekday(sys_days(ymd)) - Sunday).count())) + goto broken; + if (!computed) + { + if (G != not_a_year || V != not_a_week_num) + { + sys_days sd = ymd; + auto G_trial = year_month_day{sd + days{3}}.year(); + auto start = sys_days((G_trial - years{1})/December/Thursday[last]) + + (Monday - Thursday); + if (sd < start) + { + --G_trial; + if (V != not_a_week_num) + start = sys_days((G_trial - years{1})/December/Thursday[last]) + + (Monday - Thursday); + } + if (G != not_a_year && G != static_cast(G_trial)) + goto broken; + if (V != not_a_week_num) + { + auto V_trial = duration_cast(sd - start).count() + 1; + if (V != V_trial) + goto broken; + } + } + if (U != not_a_week_num) + { + auto start = sys_days(Sunday[1]/January/ymd.year()); + auto U_trial = floor(sys_days(ymd) - start).count() + 1; + if (U != U_trial) + goto broken; + } + if (W != not_a_week_num) + { + auto start = sys_days(Monday[1]/January/ymd.year()); + auto W_trial = floor(sys_days(ymd) - start).count() + 1; + if (W != W_trial) + goto broken; + } + } + } + fds.ymd = ymd; + if (I != not_a_hour_12_value) + { + if (!(1 <= I && I <= 12)) + goto broken; + if (p != not_a_ampm) + { + // p is in [0, 1] == [AM, PM] + // Store trial H in I + if (I == 12) + --p; + I += p*12; + // Either set H from I or make sure H and I are consistent + if (H == not_a_hour) + H = I; + else if (I != H) + goto broken; + } + else // p == not_a_ampm + { + // if H, make sure H and I could be consistent + if (H != not_a_hour) + { + if (I == 12) + { + if (H != 0 && H != 12) + goto broken; + } + else if (!(I == H || I == H+12)) + { + goto broken; + } + } + else // I is ambiguous, AM or PM? + goto broken; + } + } + if (H != not_a_hour) + { + fds.has_tod = true; + fds.tod = hh_mm_ss{hours{H}}; + } + if (M != not_a_minute) + { + fds.has_tod = true; + fds.tod.m_ = minutes{M}; + } + if (s != not_a_second) + { + fds.has_tod = true; + fds.tod.s_ = detail::decimal_format_seconds{s}; + } + if (j != not_a_doy) + { + fds.has_tod = true; + fds.tod.h_ += hours{days{j}}; + } + if (wd != not_a_weekday) + fds.wd = weekday{static_cast(wd)}; + if (abbrev != nullptr) + *abbrev = std::move(temp_abbrev); + if (offset != nullptr && temp_offset != not_a_offset) + *offset = temp_offset; + } + return is; + } +broken: + is.setstate(ios::failbit); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, year& y, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.year().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + y = fds.ymd.year(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, month& m, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.month().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + m = fds.ymd.month(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, day& d, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.day().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + d = fds.ymd.day(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, weekday& wd, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.wd.ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + wd = fds.wd; + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, year_month& ym, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.month().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + ym = fds.ymd.year()/fds.ymd.month(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, month_day& md, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.month().ok() || !fds.ymd.day().ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + md = fds.ymd.month()/fds.ymd.day(); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + year_month_day& ymd, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = std::chrono::seconds; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.ok()) + is.setstate(std::ios::failbit); + if (!is.fail()) + ymd = fds.ymd; + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + sys_time& tp, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = typename std::common_type::type; + using detail::round_i; + std::chrono::minutes offset_local{}; + auto offptr = offset ? offset : &offset_local; + fields fds{}; + fds.has_tod = true; + date::from_stream(is, fmt, fds, abbrev, offptr); + if (!fds.ymd.ok() || !fds.tod.in_conventional_range()) + is.setstate(std::ios::failbit); + if (!is.fail()) + tp = round_i(sys_days(fds.ymd) - *offptr + fds.tod.to_duration()); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + local_time& tp, std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using CT = typename std::common_type::type; + using detail::round_i; + fields fds{}; + fds.has_tod = true; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.ymd.ok() || !fds.tod.in_conventional_range()) + is.setstate(std::ios::failbit); + if (!is.fail()) + tp = round_i(local_seconds{local_days(fds.ymd)} + fds.tod.to_duration()); + return is; +} + +template > +std::basic_istream& +from_stream(std::basic_istream& is, const CharT* fmt, + std::chrono::duration& d, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) +{ + using Duration = std::chrono::duration; + using CT = typename std::common_type::type; + using detail::round_i; + fields fds{}; + date::from_stream(is, fmt, fds, abbrev, offset); + if (!fds.has_tod) + is.setstate(std::ios::failbit); + if (!is.fail()) + d = round_i(fds.tod.to_duration()); + return is; +} + +template , + class Alloc = std::allocator> +struct parse_manip +{ + const std::basic_string format_; + Parsable& tp_; + std::basic_string* abbrev_; + std::chrono::minutes* offset_; + +public: + parse_manip(std::basic_string format, Parsable& tp, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) + : format_(std::move(format)) + , tp_(tp) + , abbrev_(abbrev) + , offset_(offset) + {} + +#if HAS_STRING_VIEW + parse_manip(const CharT* format, Parsable& tp, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) + : format_(format) + , tp_(tp) + , abbrev_(abbrev) + , offset_(offset) + {} + + parse_manip(std::basic_string_view format, Parsable& tp, + std::basic_string* abbrev = nullptr, + std::chrono::minutes* offset = nullptr) + : format_(format) + , tp_(tp) + , abbrev_(abbrev) + , offset_(offset) + {} +#endif // HAS_STRING_VIEW +}; + +template +std::basic_istream& +operator>>(std::basic_istream& is, + const parse_manip& x) +{ + return date::from_stream(is, x.format_.c_str(), x.tp_, x.abbrev_, x.offset_); +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp), + parse_manip{format, tp}) +{ + return {format, tp}; +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp, + std::basic_string& abbrev) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp, &abbrev), + parse_manip{format, tp, &abbrev}) +{ + return {format, tp, &abbrev}; +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp, + std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp, + std::declval*>(), + &offset), + parse_manip{format, tp, nullptr, &offset}) +{ + return {format, tp, nullptr, &offset}; +} + +template +inline +auto +parse(const std::basic_string& format, Parsable& tp, + std::basic_string& abbrev, std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), + format.c_str(), tp, &abbrev, &offset), + parse_manip{format, tp, &abbrev, &offset}) +{ + return {format, tp, &abbrev, &offset}; +} + +// const CharT* formats + +template +inline +auto +parse(const CharT* format, Parsable& tp) + -> decltype(date::from_stream(std::declval&>(), format, tp), + parse_manip{format, tp}) +{ + return {format, tp}; +} + +template +inline +auto +parse(const CharT* format, Parsable& tp, std::basic_string& abbrev) + -> decltype(date::from_stream(std::declval&>(), format, + tp, &abbrev), + parse_manip{format, tp, &abbrev}) +{ + return {format, tp, &abbrev}; +} + +template +inline +auto +parse(const CharT* format, Parsable& tp, std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), format, + tp, std::declval*>(), &offset), + parse_manip{format, tp, nullptr, &offset}) +{ + return {format, tp, nullptr, &offset}; +} + +template +inline +auto +parse(const CharT* format, Parsable& tp, + std::basic_string& abbrev, std::chrono::minutes& offset) + -> decltype(date::from_stream(std::declval&>(), format, + tp, &abbrev, &offset), + parse_manip{format, tp, &abbrev, &offset}) +{ + return {format, tp, &abbrev, &offset}; +} + +// duration streaming + +template +inline +std::basic_ostream& +operator<<(std::basic_ostream& os, + const std::chrono::duration& d) +{ + return os << detail::make_string::from(d.count()) + + detail::get_units(typename Period::type{}); +} + +} // namespace date + +#ifdef _MSC_VER +# pragma warning(pop) +#endif + +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + +#endif // DATE_H diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/errors.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/errors.hpp new file mode 100644 index 00000000..7535679a --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/errors.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include + +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 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/helpers.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/helpers.hpp new file mode 100644 index 00000000..ee906c9b --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/helpers.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include +#include + +namespace chatterino::eventsub::lib { + +template +std::optional 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(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 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/listener.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/listener.hpp new file mode 100644 index 00000000..724e075c --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/listener.hpp @@ -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 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/messages/metadata.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/messages/metadata.hpp new file mode 100644 index 00000000..771c22bf --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/messages/metadata.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +#include +#include + +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 subscriptionType; + const std::optional subscriptionVersion; +}; + +// DESERIALIZATION DEFINITION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::messages diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-ban-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-ban-v1.hpp new file mode 100644 index 00000000..1d874308 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-ban-v1.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include + +#include +#include + +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 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::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::channel_ban::v1 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp new file mode 100644 index 00000000..84223d6b --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp @@ -0,0 +1,185 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include + +#include +#include +#include + +/* +{ + ..., + "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 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; + std::optional emote; + std::optional mention; +}; + +/// json_transform=snake_case +struct Message { + std::string text; + std::vector 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 badges; + + std::string messageID; + std::string messageType; + Message message; + + std::optional cheer; + std::optional reply; + std::optional channelPointsCustomRewardID; +}; + +struct Payload { + const subscription::Subscription subscription; + + const Event event; +}; + +// DESERIALIZATION DEFINITION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::channel_chat_message::v1 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-chat-notification-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-chat-notification-v1.hpp new file mode 100644 index 00000000..d84a5036 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-chat-notification-v1.hpp @@ -0,0 +1,267 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include + +#include +#include +#include + +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 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; + std::optional emote; + std::optional 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 streakMonths; + std::string subTier; + bool isPrime; + bool isGift; + bool gifterIsAnonymous; + std::optional gifterUserID; + std::optional gifterUserName; + std::optional gifterUserLogin; +}; + +/// json_transform=snake_case +struct GiftSubscription { + int durationMonths; + std::optional cumulativeTotal; + std::optional streakMonths; + std::string recipientUserID; + std::string recipientUserName; + std::string recipientUserLogin; + std::string subTier; + std::optional communityGiftID; +}; + +/// json_transform=snake_case +struct CommunityGiftSubscription { + std::string id; + int total; + std::string subTier; + std::optional cumulativeTotal; +}; + +/// json_transform=snake_case +struct GiftPaidUpgrade { + bool gifterIsAnonymous; + std::optional gifterUserID; + std::optional gifterUserName; + std::optional 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 gifterUserID; + std::optional gifterUserName; + std::optional 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 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 badges; + std::string systemMessage; + std::string messageID; + Message message; + std::string noticeType; + std::optional sub; + std::optional resub; + std::optional subGift; + std::optional communitySubGift; + std::optional giftPaidUpgrade; + std::optional primePaidUpgrade; + std::optional raid; + std::optional unraid; + std::optional payItForward; + std::optional announcement; + std::optional charityDonation; + std::optional bitsBadgeTier; +}; + +struct Payload { + const subscription::Subscription subscription; + + const Event event; +}; + +// DESERIALIZATION DEFINITION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type + tag_invoke(boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type + tag_invoke(boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::channel_chat_notification::v1 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp new file mode 100644 index 00000000..0f88905f --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp @@ -0,0 +1,452 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" +#include "twitch-eventsub-ws/string.hpp" + +#include + +#include + +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 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 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 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 sourceBroadcasterUserLogin; + /// For Shared Chat events, the user Name (e.g. í…ŒìŠ€íŠžêł„ì •420) of the user who's channel the event took place in + std::optional 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; + std::optional slow; + std::optional vip; + std::optional unvip; + std::optional unmod; + std::optional ban; + std::optional unban; + std::optional timeout; + std::optional untimeout; + std::optional raid; + std::optional unraid; + /// json_rename=delete + std::optional deleteMessage; + std::optional automodTerms; + std::optional unbanRequest; + std::optional warn; + std::optional sharedChatBan; + std::optional sharedChatUnban; + std::optional sharedChatTimeout; + std::optional sharedChatUntimeout; + std::optional sharedChatDelete; +}; + +struct Payload { + subscription::Subscription subscription; + + Event event; +}; + +// DESERIALIZATION DEFINITION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::channel_moderate::v2 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-update-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-update-v1.hpp new file mode 100644 index 00000000..da69d509 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-update-v1.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include + +#include + +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::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::channel_update::v1 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/session-welcome.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/session-welcome.hpp new file mode 100644 index 00000000..d229f254 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/session-welcome.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +#include + +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::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::session_welcome diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/stream-offline-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/stream-offline-v1.hpp new file mode 100644 index 00000000..07651501 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/stream-offline-v1.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include + +#include + +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::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::stream_offline::v1 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/stream-online-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/stream-online-v1.hpp new file mode 100644 index 00000000..e48ca39c --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/stream-online-v1.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include + +#include + +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::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::stream_online::v1 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/subscription.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/subscription.hpp new file mode 100644 index 00000000..fe6ca864 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/subscription.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include + +#include + +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::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot); +// DESERIALIZATION DEFINITION END + +} // namespace chatterino::eventsub::lib::payload::subscription diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/session.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/session.hpp new file mode 100644 index 00000000..6d8ce052 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/session.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +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, + const boost::beast::flat_buffer &buffer); + +// Sends a WebSocket message and prints the response +class Session : public std::enable_shared_from_this +{ + boost::asio::ip::tcp::resolver resolver; + boost::beast::websocket::stream< + boost::beast::ssl_stream> + ws; + boost::beast::flat_buffer buffer; + std::string host; + std::string port; + std::string path; + std::string userAgent; + std::unique_ptr 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); + + // 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 diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/string.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/string.hpp new file mode 100644 index 00000000..d9178ed2 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/string.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include +#include + +namespace chatterino::eventsub::lib { + +template +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>; + if constexpr (std::is_same_v) + { + auto qtString = QString::fromStdString(arg); + this->backingString = qtString; + return qtString; + } + else if constexpr (std::is_same_v) + { + return arg; + } + else + { + static_assert(DEPENDENT_FALSE, + "unknown type in variant"); + } + }, + this->backingString); + } + +private: + mutable std::variant backingString; +}; + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +} // namespace chatterino::eventsub::lib diff --git a/lib/twitch-eventsub-ws/pyproject.toml b/lib/twitch-eventsub-ws/pyproject.toml new file mode 100644 index 00000000..fa7a7384 --- /dev/null +++ b/lib/twitch-eventsub-ws/pyproject.toml @@ -0,0 +1,5 @@ +[tool.black] +line-length = 120 + +[tool.ruff] +line-length = 120 diff --git a/lib/twitch-eventsub-ws/src/CMakeLists.txt b/lib/twitch-eventsub-ws/src/CMakeLists.txt new file mode 100644 index 00000000..e6c0e925 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/CMakeLists.txt @@ -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 + $<$:BOOST_JSON_NO_LIB> + $<$: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 () diff --git a/lib/twitch-eventsub-ws/src/chrono.cpp b/lib/twitch-eventsub-ws/src/chrono.cpp new file mode 100644 index 00000000..bda4477d --- /dev/null +++ b/lib/twitch-eventsub-ws/src/chrono.cpp @@ -0,0 +1,27 @@ +#include "twitch-eventsub-ws/chrono.hpp" + +#include "twitch-eventsub-ws/date.h" + +#include + +namespace chatterino::eventsub::lib { + +boost::json::result_for::type + tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot, const AsISO8601 &) +{ + const auto raw = boost::json::try_value_to(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 diff --git a/lib/twitch-eventsub-ws/src/json.cpp b/lib/twitch-eventsub-ws/src/json.cpp new file mode 100644 index 00000000..9627f94f --- /dev/null +++ b/lib/twitch-eventsub-ws/src/json.cpp @@ -0,0 +1 @@ +#include diff --git a/lib/twitch-eventsub-ws/src/messages/metadata.cpp b/lib/twitch-eventsub-ws/src/messages/metadata.cpp new file mode 100644 index 00000000..40f77e81 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/messages/metadata.cpp @@ -0,0 +1,109 @@ +#include "twitch-eventsub-ws/messages/metadata.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::messages { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*jvmessageTimestamp); + + if (messageTimestamp.has_error()) + { + return messageTimestamp.error(); + } + + std::optional subscriptionType = std::nullopt; + const auto *jvsubscriptionType = root.if_contains("subscription_type"); + if (jvsubscriptionType != nullptr && !jvsubscriptionType->is_null()) + { + auto tsubscriptionType = + boost::json::try_value_to(*jvsubscriptionType); + + if (tsubscriptionType.has_error()) + { + return tsubscriptionType.error(); + } + subscriptionType = std::move(tsubscriptionType.value()); + } + + std::optional subscriptionVersion = std::nullopt; + const auto *jvsubscriptionVersion = + root.if_contains("subscription_version"); + if (jvsubscriptionVersion != nullptr && !jvsubscriptionVersion->is_null()) + { + auto tsubscriptionVersion = + boost::json::try_value_to(*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 diff --git a/lib/twitch-eventsub-ws/src/payloads/README.md b/lib/twitch-eventsub-ws/src/payloads/README.md new file mode 100644 index 00000000..9e4846ca --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/README.md @@ -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 + +#include + +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 + +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( + 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 diff --git a/lib/twitch-eventsub-ws/src/payloads/channel-ban-v1.cpp b/lib/twitch-eventsub-ws/src/payloads/channel-ban-v1.cpp new file mode 100644 index 00000000..6758cab7 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/channel-ban-v1.cpp @@ -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 + +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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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(*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(*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(*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(*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(*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(*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(*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(*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( + *jvbannedAt, AsISO8601()); + + if (bannedAt.has_error()) + { + return bannedAt.error(); + } + + std::optional endsAt = std::nullopt; + const auto *jvendsAt = root.if_contains("ends_at"); + if (jvendsAt != nullptr && !jvendsAt->is_null()) + { + auto tendsAt = + boost::json::try_value_to( + *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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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 diff --git a/lib/twitch-eventsub-ws/src/payloads/channel-chat-message-v1.cpp b/lib/twitch-eventsub-ws/src/payloads/channel-chat-message-v1.cpp new file mode 100644 index 00000000..e9fe0e8d --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/channel-chat-message-v1.cpp @@ -0,0 +1,934 @@ +#include "twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::payload::channel_chat_message::v1 { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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>(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, + 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(*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(*jvtext); + + if (text.has_error()) + { + return text.error(); + } + + std::optional cheermote = std::nullopt; + const auto *jvcheermote = root.if_contains("cheermote"); + if (jvcheermote != nullptr && !jvcheermote->is_null()) + { + auto tcheermote = boost::json::try_value_to(*jvcheermote); + + if (tcheermote.has_error()) + { + return tcheermote.error(); + } + cheermote = std::move(tcheermote.value()); + } + + std::optional emote = std::nullopt; + const auto *jvemote = root.if_contains("emote"); + if (jvemote != nullptr && !jvemote->is_null()) + { + auto temote = boost::json::try_value_to(*jvemote); + + if (temote.has_error()) + { + return temote.error(); + } + emote = std::move(temote.value()); + } + + std::optional mention = std::nullopt; + const auto *jvmention = root.if_contains("mention"); + if (jvmention != nullptr && !jvmention->is_null()) + { + auto tmention = boost::json::try_value_to(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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>(*jvfragments); + if (fragments.has_error()) + { + return fragments.error(); + } + + return Message{ + .text = std::move(text.value()), + .fragments = fragments.value(), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*jvbits); + + if (bits.has_error()) + { + return bits.error(); + } + + return Cheer{ + .bits = std::move(bits.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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(*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(*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(*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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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(*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(*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(*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(*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>(*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(*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(*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(*jvmessage); + + if (message.has_error()) + { + return message.error(); + } + + std::optional cheer = std::nullopt; + const auto *jvcheer = root.if_contains("cheer"); + if (jvcheer != nullptr && !jvcheer->is_null()) + { + auto tcheer = boost::json::try_value_to(*jvcheer); + + if (tcheer.has_error()) + { + return tcheer.error(); + } + cheer = std::move(tcheer.value()); + } + + std::optional reply = std::nullopt; + const auto *jvreply = root.if_contains("reply"); + if (jvreply != nullptr && !jvreply->is_null()) + { + auto treply = boost::json::try_value_to(*jvreply); + + if (treply.has_error()) + { + return treply.error(); + } + reply = std::move(treply.value()); + } + + std::optional 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( + *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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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 diff --git a/lib/twitch-eventsub-ws/src/payloads/channel-chat-notification-v1.cpp b/lib/twitch-eventsub-ws/src/payloads/channel-chat-notification-v1.cpp new file mode 100644 index 00000000..40d249fe --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/channel-chat-notification-v1.cpp @@ -0,0 +1,1845 @@ +#include "twitch-eventsub-ws/payloads/channel-chat-notification-v1.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::payload::channel_chat_notification::v1 { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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>(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, + 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(*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(*jvtext); + + if (text.has_error()) + { + return text.error(); + } + + std::optional cheermote = std::nullopt; + const auto *jvcheermote = root.if_contains("cheermote"); + if (jvcheermote != nullptr && !jvcheermote->is_null()) + { + auto tcheermote = boost::json::try_value_to(*jvcheermote); + + if (tcheermote.has_error()) + { + return tcheermote.error(); + } + cheermote = std::move(tcheermote.value()); + } + + std::optional emote = std::nullopt; + const auto *jvemote = root.if_contains("emote"); + if (jvemote != nullptr && !jvemote->is_null()) + { + auto temote = boost::json::try_value_to(*jvemote); + + if (temote.has_error()) + { + return temote.error(); + } + emote = std::move(temote.value()); + } + + std::optional mention = std::nullopt; + const auto *jvmention = root.if_contains("mention"); + if (jvmention != nullptr && !jvmention->is_null()) + { + auto tmention = boost::json::try_value_to(*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::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Subcription must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvsubTier = root.if_contains("sub_tier"); + if (jvsubTier == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_subTier{"Missing required key sub_tier"}; + return boost::system::error_code{129, error_missing_field_subTier}; + } + + auto subTier = boost::json::try_value_to(*jvsubTier); + + if (subTier.has_error()) + { + return subTier.error(); + } + + const auto *jvisPrime = root.if_contains("is_prime"); + if (jvisPrime == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_isPrime{"Missing required key is_prime"}; + return boost::system::error_code{129, error_missing_field_isPrime}; + } + + auto isPrime = boost::json::try_value_to(*jvisPrime); + + if (isPrime.has_error()) + { + return isPrime.error(); + } + + const auto *jvdurationMonths = root.if_contains("duration_months"); + if (jvdurationMonths == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_durationMonths{ + "Missing required key duration_months"}; + return boost::system::error_code{129, + error_missing_field_durationMonths}; + } + + auto durationMonths = boost::json::try_value_to(*jvdurationMonths); + + if (durationMonths.has_error()) + { + return durationMonths.error(); + } + + return Subcription{ + .subTier = std::move(subTier.value()), + .isPrime = std::move(isPrime.value()), + .durationMonths = std::move(durationMonths.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Resubscription must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvcumulativeMonths = root.if_contains("cumulative_months"); + if (jvcumulativeMonths == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_cumulativeMonths{ + "Missing required key cumulative_months"}; + return boost::system::error_code{129, + error_missing_field_cumulativeMonths}; + } + + auto cumulativeMonths = boost::json::try_value_to(*jvcumulativeMonths); + + if (cumulativeMonths.has_error()) + { + return cumulativeMonths.error(); + } + + const auto *jvdurationMonths = root.if_contains("duration_months"); + if (jvdurationMonths == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_durationMonths{ + "Missing required key duration_months"}; + return boost::system::error_code{129, + error_missing_field_durationMonths}; + } + + auto durationMonths = boost::json::try_value_to(*jvdurationMonths); + + if (durationMonths.has_error()) + { + return durationMonths.error(); + } + + std::optional streakMonths = std::nullopt; + const auto *jvstreakMonths = root.if_contains("streak_months"); + if (jvstreakMonths != nullptr && !jvstreakMonths->is_null()) + { + auto tstreakMonths = boost::json::try_value_to(*jvstreakMonths); + + if (tstreakMonths.has_error()) + { + return tstreakMonths.error(); + } + streakMonths = std::move(tstreakMonths.value()); + } + + const auto *jvsubTier = root.if_contains("sub_tier"); + if (jvsubTier == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_subTier{"Missing required key sub_tier"}; + return boost::system::error_code{129, error_missing_field_subTier}; + } + + auto subTier = boost::json::try_value_to(*jvsubTier); + + if (subTier.has_error()) + { + return subTier.error(); + } + + const auto *jvisPrime = root.if_contains("is_prime"); + if (jvisPrime == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_isPrime{"Missing required key is_prime"}; + return boost::system::error_code{129, error_missing_field_isPrime}; + } + + auto isPrime = boost::json::try_value_to(*jvisPrime); + + if (isPrime.has_error()) + { + return isPrime.error(); + } + + const auto *jvisGift = root.if_contains("is_gift"); + if (jvisGift == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_isGift{ + "Missing required key is_gift"}; + return boost::system::error_code{129, error_missing_field_isGift}; + } + + auto isGift = boost::json::try_value_to(*jvisGift); + + if (isGift.has_error()) + { + return isGift.error(); + } + + const auto *jvgifterIsAnonymous = root.if_contains("gifter_is_anonymous"); + if (jvgifterIsAnonymous == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_gifterIsAnonymous{ + "Missing required key gifter_is_anonymous"}; + return boost::system::error_code{129, + error_missing_field_gifterIsAnonymous}; + } + + auto gifterIsAnonymous = + boost::json::try_value_to(*jvgifterIsAnonymous); + + if (gifterIsAnonymous.has_error()) + { + return gifterIsAnonymous.error(); + } + + std::optional gifterUserID = std::nullopt; + const auto *jvgifterUserID = root.if_contains("gifter_user_id"); + if (jvgifterUserID != nullptr && !jvgifterUserID->is_null()) + { + auto tgifterUserID = + boost::json::try_value_to(*jvgifterUserID); + + if (tgifterUserID.has_error()) + { + return tgifterUserID.error(); + } + gifterUserID = std::move(tgifterUserID.value()); + } + + std::optional gifterUserName = std::nullopt; + const auto *jvgifterUserName = root.if_contains("gifter_user_name"); + if (jvgifterUserName != nullptr && !jvgifterUserName->is_null()) + { + auto tgifterUserName = + boost::json::try_value_to(*jvgifterUserName); + + if (tgifterUserName.has_error()) + { + return tgifterUserName.error(); + } + gifterUserName = std::move(tgifterUserName.value()); + } + + std::optional gifterUserLogin = std::nullopt; + const auto *jvgifterUserLogin = root.if_contains("gifter_user_login"); + if (jvgifterUserLogin != nullptr && !jvgifterUserLogin->is_null()) + { + auto tgifterUserLogin = + boost::json::try_value_to(*jvgifterUserLogin); + + if (tgifterUserLogin.has_error()) + { + return tgifterUserLogin.error(); + } + gifterUserLogin = std::move(tgifterUserLogin.value()); + } + + return Resubscription{ + .cumulativeMonths = std::move(cumulativeMonths.value()), + .durationMonths = std::move(durationMonths.value()), + .streakMonths = std::move(streakMonths), + .subTier = std::move(subTier.value()), + .isPrime = std::move(isPrime.value()), + .isGift = std::move(isGift.value()), + .gifterIsAnonymous = std::move(gifterIsAnonymous.value()), + .gifterUserID = std::move(gifterUserID), + .gifterUserName = std::move(gifterUserName), + .gifterUserLogin = std::move(gifterUserLogin), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "GiftSubscription must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvdurationMonths = root.if_contains("duration_months"); + if (jvdurationMonths == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_durationMonths{ + "Missing required key duration_months"}; + return boost::system::error_code{129, + error_missing_field_durationMonths}; + } + + auto durationMonths = boost::json::try_value_to(*jvdurationMonths); + + if (durationMonths.has_error()) + { + return durationMonths.error(); + } + + std::optional cumulativeTotal = std::nullopt; + const auto *jvcumulativeTotal = root.if_contains("cumulative_total"); + if (jvcumulativeTotal != nullptr && !jvcumulativeTotal->is_null()) + { + auto tcumulativeTotal = + boost::json::try_value_to(*jvcumulativeTotal); + + if (tcumulativeTotal.has_error()) + { + return tcumulativeTotal.error(); + } + cumulativeTotal = std::move(tcumulativeTotal.value()); + } + + std::optional streakMonths = std::nullopt; + const auto *jvstreakMonths = root.if_contains("streak_months"); + if (jvstreakMonths != nullptr && !jvstreakMonths->is_null()) + { + auto tstreakMonths = boost::json::try_value_to(*jvstreakMonths); + + if (tstreakMonths.has_error()) + { + return tstreakMonths.error(); + } + streakMonths = std::move(tstreakMonths.value()); + } + + const auto *jvrecipientUserID = root.if_contains("recipient_user_id"); + if (jvrecipientUserID == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_recipientUserID{ + "Missing required key recipient_user_id"}; + return boost::system::error_code{129, + error_missing_field_recipientUserID}; + } + + auto recipientUserID = + boost::json::try_value_to(*jvrecipientUserID); + + if (recipientUserID.has_error()) + { + return recipientUserID.error(); + } + + const auto *jvrecipientUserName = root.if_contains("recipient_user_name"); + if (jvrecipientUserName == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_recipientUserName{ + "Missing required key recipient_user_name"}; + return boost::system::error_code{129, + error_missing_field_recipientUserName}; + } + + auto recipientUserName = + boost::json::try_value_to(*jvrecipientUserName); + + if (recipientUserName.has_error()) + { + return recipientUserName.error(); + } + + const auto *jvrecipientUserLogin = root.if_contains("recipient_user_login"); + if (jvrecipientUserLogin == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_recipientUserLogin{ + "Missing required key recipient_user_login"}; + return boost::system::error_code{ + 129, error_missing_field_recipientUserLogin}; + } + + auto recipientUserLogin = + boost::json::try_value_to(*jvrecipientUserLogin); + + if (recipientUserLogin.has_error()) + { + return recipientUserLogin.error(); + } + + const auto *jvsubTier = root.if_contains("sub_tier"); + if (jvsubTier == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_subTier{"Missing required key sub_tier"}; + return boost::system::error_code{129, error_missing_field_subTier}; + } + + auto subTier = boost::json::try_value_to(*jvsubTier); + + if (subTier.has_error()) + { + return subTier.error(); + } + + std::optional communityGiftID = std::nullopt; + const auto *jvcommunityGiftID = root.if_contains("community_gift_id"); + if (jvcommunityGiftID != nullptr && !jvcommunityGiftID->is_null()) + { + auto tcommunityGiftID = + boost::json::try_value_to(*jvcommunityGiftID); + + if (tcommunityGiftID.has_error()) + { + return tcommunityGiftID.error(); + } + communityGiftID = std::move(tcommunityGiftID.value()); + } + + return GiftSubscription{ + .durationMonths = std::move(durationMonths.value()), + .cumulativeTotal = std::move(cumulativeTotal), + .streakMonths = std::move(streakMonths), + .recipientUserID = std::move(recipientUserID.value()), + .recipientUserName = std::move(recipientUserName.value()), + .recipientUserLogin = std::move(recipientUserLogin.value()), + .subTier = std::move(subTier.value()), + .communityGiftID = std::move(communityGiftID), + }; +} + +boost::json::result_for::type + tag_invoke(boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "CommunityGiftSubscription 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(*jvid); + + if (id.has_error()) + { + return id.error(); + } + + const auto *jvtotal = root.if_contains("total"); + if (jvtotal == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_total{ + "Missing required key total"}; + return boost::system::error_code{129, error_missing_field_total}; + } + + auto total = boost::json::try_value_to(*jvtotal); + + if (total.has_error()) + { + return total.error(); + } + + const auto *jvsubTier = root.if_contains("sub_tier"); + if (jvsubTier == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_subTier{"Missing required key sub_tier"}; + return boost::system::error_code{129, error_missing_field_subTier}; + } + + auto subTier = boost::json::try_value_to(*jvsubTier); + + if (subTier.has_error()) + { + return subTier.error(); + } + + std::optional cumulativeTotal = std::nullopt; + const auto *jvcumulativeTotal = root.if_contains("cumulative_total"); + if (jvcumulativeTotal != nullptr && !jvcumulativeTotal->is_null()) + { + auto tcumulativeTotal = + boost::json::try_value_to(*jvcumulativeTotal); + + if (tcumulativeTotal.has_error()) + { + return tcumulativeTotal.error(); + } + cumulativeTotal = std::move(tcumulativeTotal.value()); + } + + return CommunityGiftSubscription{ + .id = std::move(id.value()), + .total = std::move(total.value()), + .subTier = std::move(subTier.value()), + .cumulativeTotal = std::move(cumulativeTotal), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "GiftPaidUpgrade must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvgifterIsAnonymous = root.if_contains("gifter_is_anonymous"); + if (jvgifterIsAnonymous == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_gifterIsAnonymous{ + "Missing required key gifter_is_anonymous"}; + return boost::system::error_code{129, + error_missing_field_gifterIsAnonymous}; + } + + auto gifterIsAnonymous = + boost::json::try_value_to(*jvgifterIsAnonymous); + + if (gifterIsAnonymous.has_error()) + { + return gifterIsAnonymous.error(); + } + + std::optional gifterUserID = std::nullopt; + const auto *jvgifterUserID = root.if_contains("gifter_user_id"); + if (jvgifterUserID != nullptr && !jvgifterUserID->is_null()) + { + auto tgifterUserID = + boost::json::try_value_to(*jvgifterUserID); + + if (tgifterUserID.has_error()) + { + return tgifterUserID.error(); + } + gifterUserID = std::move(tgifterUserID.value()); + } + + std::optional gifterUserName = std::nullopt; + const auto *jvgifterUserName = root.if_contains("gifter_user_name"); + if (jvgifterUserName != nullptr && !jvgifterUserName->is_null()) + { + auto tgifterUserName = + boost::json::try_value_to(*jvgifterUserName); + + if (tgifterUserName.has_error()) + { + return tgifterUserName.error(); + } + gifterUserName = std::move(tgifterUserName.value()); + } + + std::optional gifterUserLogin = std::nullopt; + const auto *jvgifterUserLogin = root.if_contains("gifter_user_login"); + if (jvgifterUserLogin != nullptr && !jvgifterUserLogin->is_null()) + { + auto tgifterUserLogin = + boost::json::try_value_to(*jvgifterUserLogin); + + if (tgifterUserLogin.has_error()) + { + return tgifterUserLogin.error(); + } + gifterUserLogin = std::move(tgifterUserLogin.value()); + } + + return GiftPaidUpgrade{ + .gifterIsAnonymous = std::move(gifterIsAnonymous.value()), + .gifterUserID = std::move(gifterUserID), + .gifterUserName = std::move(gifterUserName), + .gifterUserLogin = std::move(gifterUserLogin), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "PrimePaidUpgrade must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvsubTier = root.if_contains("sub_tier"); + if (jvsubTier == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_subTier{"Missing required key sub_tier"}; + return boost::system::error_code{129, error_missing_field_subTier}; + } + + auto subTier = boost::json::try_value_to(*jvsubTier); + + if (subTier.has_error()) + { + return subTier.error(); + } + + return PrimePaidUpgrade{ + .subTier = std::move(subTier.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Raid 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(*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(*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(*jvuserLogin); + + if (userLogin.has_error()) + { + return userLogin.error(); + } + + const auto *jvviewerCount = root.if_contains("viewer_count"); + if (jvviewerCount == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_viewerCount{ + "Missing required key viewer_count"}; + return boost::system::error_code{129, error_missing_field_viewerCount}; + } + + auto viewerCount = boost::json::try_value_to(*jvviewerCount); + + if (viewerCount.has_error()) + { + return viewerCount.error(); + } + + const auto *jvprofileImageURL = root.if_contains("profile_image_url"); + if (jvprofileImageURL == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_profileImageURL{ + "Missing required key profile_image_url"}; + return boost::system::error_code{129, + error_missing_field_profileImageURL}; + } + + auto profileImageURL = + boost::json::try_value_to(*jvprofileImageURL); + + if (profileImageURL.has_error()) + { + return profileImageURL.error(); + } + + return Raid{ + .userID = std::move(userID.value()), + .userName = std::move(userName.value()), + .userLogin = std::move(userLogin.value()), + .viewerCount = std::move(viewerCount.value()), + .profileImageURL = std::move(profileImageURL.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Unraid must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + return Unraid{}; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "PayItForward must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvgifterIsAnonymous = root.if_contains("gifter_is_anonymous"); + if (jvgifterIsAnonymous == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_gifterIsAnonymous{ + "Missing required key gifter_is_anonymous"}; + return boost::system::error_code{129, + error_missing_field_gifterIsAnonymous}; + } + + auto gifterIsAnonymous = + boost::json::try_value_to(*jvgifterIsAnonymous); + + if (gifterIsAnonymous.has_error()) + { + return gifterIsAnonymous.error(); + } + + std::optional gifterUserID = std::nullopt; + const auto *jvgifterUserID = root.if_contains("gifter_user_id"); + if (jvgifterUserID != nullptr && !jvgifterUserID->is_null()) + { + auto tgifterUserID = + boost::json::try_value_to(*jvgifterUserID); + + if (tgifterUserID.has_error()) + { + return tgifterUserID.error(); + } + gifterUserID = std::move(tgifterUserID.value()); + } + + std::optional gifterUserName = std::nullopt; + const auto *jvgifterUserName = root.if_contains("gifter_user_name"); + if (jvgifterUserName != nullptr && !jvgifterUserName->is_null()) + { + auto tgifterUserName = + boost::json::try_value_to(*jvgifterUserName); + + if (tgifterUserName.has_error()) + { + return tgifterUserName.error(); + } + gifterUserName = std::move(tgifterUserName.value()); + } + + std::optional gifterUserLogin = std::nullopt; + const auto *jvgifterUserLogin = root.if_contains("gifter_user_login"); + if (jvgifterUserLogin != nullptr && !jvgifterUserLogin->is_null()) + { + auto tgifterUserLogin = + boost::json::try_value_to(*jvgifterUserLogin); + + if (tgifterUserLogin.has_error()) + { + return tgifterUserLogin.error(); + } + gifterUserLogin = std::move(tgifterUserLogin.value()); + } + + return PayItForward{ + .gifterIsAnonymous = std::move(gifterIsAnonymous.value()), + .gifterUserID = std::move(gifterUserID), + .gifterUserName = std::move(gifterUserName), + .gifterUserLogin = std::move(gifterUserLogin), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Announcement must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + 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(*jvcolor); + + if (color.has_error()) + { + return color.error(); + } + + return Announcement{ + .color = std::move(color.value()), + }; +} + +boost::json::result_for::type + tag_invoke(boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "CharityDonationAmount must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvvalue = root.if_contains("value"); + if (jvvalue == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_value{ + "Missing required key value"}; + return boost::system::error_code{129, error_missing_field_value}; + } + + auto value = boost::json::try_value_to(*jvvalue); + + if (value.has_error()) + { + return value.error(); + } + + const auto *jvdecimalPlaces = root.if_contains("decimal_places"); + if (jvdecimalPlaces == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_decimalPlaces{ + "Missing required key decimal_places"}; + return boost::system::error_code{129, + error_missing_field_decimalPlaces}; + } + + auto decimalPlaces = boost::json::try_value_to(*jvdecimalPlaces); + + if (decimalPlaces.has_error()) + { + return decimalPlaces.error(); + } + + const auto *jvcurrency = root.if_contains("currency"); + if (jvcurrency == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_currency{"Missing required key currency"}; + return boost::system::error_code{129, error_missing_field_currency}; + } + + auto currency = boost::json::try_value_to(*jvcurrency); + + if (currency.has_error()) + { + return currency.error(); + } + + return CharityDonationAmount{ + .value = std::move(value.value()), + .decimalPlaces = std::move(decimalPlaces.value()), + .currency = std::move(currency.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "CharityDonation must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvcharityName = root.if_contains("charity_name"); + if (jvcharityName == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_charityName{ + "Missing required key charity_name"}; + return boost::system::error_code{129, error_missing_field_charityName}; + } + + auto charityName = boost::json::try_value_to(*jvcharityName); + + if (charityName.has_error()) + { + return charityName.error(); + } + + const auto *jvamount = root.if_contains("amount"); + if (jvamount == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_amount{ + "Missing required key amount"}; + return boost::system::error_code{129, error_missing_field_amount}; + } + + auto amount = boost::json::try_value_to(*jvamount); + + if (amount.has_error()) + { + return amount.error(); + } + + return CharityDonation{ + .charityName = std::move(charityName.value()), + .amount = std::move(amount.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "BitsBadgeTier must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + 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(*jvtier); + + if (tier.has_error()) + { + return tier.error(); + } + + return BitsBadgeTier{ + .tier = std::move(tier.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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>(*jvfragments); + if (fragments.has_error()) + { + return fragments.error(); + } + + return Message{ + .text = std::move(text.value()), + .fragments = fragments.value(), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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(*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(*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(*jvchatterUserName); + + if (chatterUserName.has_error()) + { + return chatterUserName.error(); + } + + const auto *jvchatterIsAnonymous = root.if_contains("chatter_is_anonymous"); + if (jvchatterIsAnonymous == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_chatterIsAnonymous{ + "Missing required key chatter_is_anonymous"}; + return boost::system::error_code{ + 129, error_missing_field_chatterIsAnonymous}; + } + + auto chatterIsAnonymous = + boost::json::try_value_to(*jvchatterIsAnonymous); + + if (chatterIsAnonymous.has_error()) + { + return chatterIsAnonymous.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(*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>(*jvbadges); + if (badges.has_error()) + { + return badges.error(); + } + + const auto *jvsystemMessage = root.if_contains("system_message"); + if (jvsystemMessage == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_systemMessage{ + "Missing required key system_message"}; + return boost::system::error_code{129, + error_missing_field_systemMessage}; + } + + auto systemMessage = + boost::json::try_value_to(*jvsystemMessage); + + if (systemMessage.has_error()) + { + return systemMessage.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(*jvmessageID); + + if (messageID.has_error()) + { + return messageID.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(*jvmessage); + + if (message.has_error()) + { + return message.error(); + } + + const auto *jvnoticeType = root.if_contains("notice_type"); + if (jvnoticeType == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_noticeType{"Missing required key notice_type"}; + return boost::system::error_code{129, error_missing_field_noticeType}; + } + + auto noticeType = boost::json::try_value_to(*jvnoticeType); + + if (noticeType.has_error()) + { + return noticeType.error(); + } + + std::optional sub = std::nullopt; + const auto *jvsub = root.if_contains("sub"); + if (jvsub != nullptr && !jvsub->is_null()) + { + auto tsub = boost::json::try_value_to(*jvsub); + + if (tsub.has_error()) + { + return tsub.error(); + } + sub = std::move(tsub.value()); + } + + std::optional resub = std::nullopt; + const auto *jvresub = root.if_contains("resub"); + if (jvresub != nullptr && !jvresub->is_null()) + { + auto tresub = boost::json::try_value_to(*jvresub); + + if (tresub.has_error()) + { + return tresub.error(); + } + resub = std::move(tresub.value()); + } + + std::optional subGift = std::nullopt; + const auto *jvsubGift = root.if_contains("sub_gift"); + if (jvsubGift != nullptr && !jvsubGift->is_null()) + { + auto tsubGift = boost::json::try_value_to(*jvsubGift); + + if (tsubGift.has_error()) + { + return tsubGift.error(); + } + subGift = std::move(tsubGift.value()); + } + + std::optional communitySubGift = std::nullopt; + const auto *jvcommunitySubGift = root.if_contains("community_sub_gift"); + if (jvcommunitySubGift != nullptr && !jvcommunitySubGift->is_null()) + { + auto tcommunitySubGift = + boost::json::try_value_to( + *jvcommunitySubGift); + + if (tcommunitySubGift.has_error()) + { + return tcommunitySubGift.error(); + } + communitySubGift = std::move(tcommunitySubGift.value()); + } + + std::optional giftPaidUpgrade = std::nullopt; + const auto *jvgiftPaidUpgrade = root.if_contains("gift_paid_upgrade"); + if (jvgiftPaidUpgrade != nullptr && !jvgiftPaidUpgrade->is_null()) + { + auto tgiftPaidUpgrade = + boost::json::try_value_to(*jvgiftPaidUpgrade); + + if (tgiftPaidUpgrade.has_error()) + { + return tgiftPaidUpgrade.error(); + } + giftPaidUpgrade = std::move(tgiftPaidUpgrade.value()); + } + + std::optional primePaidUpgrade = std::nullopt; + const auto *jvprimePaidUpgrade = root.if_contains("prime_paid_upgrade"); + if (jvprimePaidUpgrade != nullptr && !jvprimePaidUpgrade->is_null()) + { + auto tprimePaidUpgrade = + boost::json::try_value_to(*jvprimePaidUpgrade); + + if (tprimePaidUpgrade.has_error()) + { + return tprimePaidUpgrade.error(); + } + primePaidUpgrade = std::move(tprimePaidUpgrade.value()); + } + + std::optional raid = std::nullopt; + const auto *jvraid = root.if_contains("raid"); + if (jvraid != nullptr && !jvraid->is_null()) + { + auto traid = boost::json::try_value_to(*jvraid); + + if (traid.has_error()) + { + return traid.error(); + } + raid = std::move(traid.value()); + } + + std::optional unraid = std::nullopt; + const auto *jvunraid = root.if_contains("unraid"); + if (jvunraid != nullptr && !jvunraid->is_null()) + { + auto tunraid = boost::json::try_value_to(*jvunraid); + + if (tunraid.has_error()) + { + return tunraid.error(); + } + unraid = std::move(tunraid.value()); + } + + std::optional payItForward = std::nullopt; + const auto *jvpayItForward = root.if_contains("pay_it_forward"); + if (jvpayItForward != nullptr && !jvpayItForward->is_null()) + { + auto tpayItForward = + boost::json::try_value_to(*jvpayItForward); + + if (tpayItForward.has_error()) + { + return tpayItForward.error(); + } + payItForward = std::move(tpayItForward.value()); + } + + std::optional announcement = std::nullopt; + const auto *jvannouncement = root.if_contains("announcement"); + if (jvannouncement != nullptr && !jvannouncement->is_null()) + { + auto tannouncement = + boost::json::try_value_to(*jvannouncement); + + if (tannouncement.has_error()) + { + return tannouncement.error(); + } + announcement = std::move(tannouncement.value()); + } + + std::optional charityDonation = std::nullopt; + const auto *jvcharityDonation = root.if_contains("charity_donation"); + if (jvcharityDonation != nullptr && !jvcharityDonation->is_null()) + { + auto tcharityDonation = + boost::json::try_value_to(*jvcharityDonation); + + if (tcharityDonation.has_error()) + { + return tcharityDonation.error(); + } + charityDonation = std::move(tcharityDonation.value()); + } + + std::optional bitsBadgeTier = std::nullopt; + const auto *jvbitsBadgeTier = root.if_contains("bits_badge_tier"); + if (jvbitsBadgeTier != nullptr && !jvbitsBadgeTier->is_null()) + { + auto tbitsBadgeTier = + boost::json::try_value_to(*jvbitsBadgeTier); + + if (tbitsBadgeTier.has_error()) + { + return tbitsBadgeTier.error(); + } + bitsBadgeTier = std::move(tbitsBadgeTier.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()), + .chatterIsAnonymous = std::move(chatterIsAnonymous.value()), + .color = std::move(color.value()), + .badges = badges.value(), + .systemMessage = std::move(systemMessage.value()), + .messageID = std::move(messageID.value()), + .message = std::move(message.value()), + .noticeType = std::move(noticeType.value()), + .sub = std::move(sub), + .resub = std::move(resub), + .subGift = std::move(subGift), + .communitySubGift = std::move(communitySubGift), + .giftPaidUpgrade = std::move(giftPaidUpgrade), + .primePaidUpgrade = std::move(primePaidUpgrade), + .raid = std::move(raid), + .unraid = std::move(unraid), + .payItForward = std::move(payItForward), + .announcement = std::move(announcement), + .charityDonation = std::move(charityDonation), + .bitsBadgeTier = std::move(bitsBadgeTier), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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_notification::v1 diff --git a/lib/twitch-eventsub-ws/src/payloads/channel-moderate-v2.cpp b/lib/twitch-eventsub-ws/src/payloads/channel-moderate-v2.cpp new file mode 100644 index 00000000..90322360 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/channel-moderate-v2.cpp @@ -0,0 +1,1840 @@ +#include "twitch-eventsub-ws/payloads/channel-moderate-v2.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +#include + +namespace chatterino::eventsub::lib::payload::channel_moderate::v2 { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_string()) + { + static const error::ApplicationErrorCategory errorMustBeString{ + "Action must be a string"}; + return boost::system::error_code{129, errorMustBeString}; + } + std::string_view eString(jvRoot.get_string()); + + using namespace std::string_view_literals; + if (eString == "ban"sv) + { + return Action::Ban; + } + if (eString == "timeout"sv) + { + return Action::Timeout; + } + if (eString == "unban"sv) + { + return Action::Unban; + } + if (eString == "untimeout"sv) + { + return Action::Untimeout; + } + if (eString == "clear"sv) + { + return Action::Clear; + } + if (eString == "emoteonly"sv) + { + return Action::Emoteonly; + } + if (eString == "emoteonlyoff"sv) + { + return Action::Emoteonlyoff; + } + if (eString == "followers"sv) + { + return Action::Followers; + } + if (eString == "followersoff"sv) + { + return Action::Followersoff; + } + if (eString == "uniquechat"sv) + { + return Action::Uniquechat; + } + if (eString == "uniquechatoff"sv) + { + return Action::Uniquechatoff; + } + if (eString == "slow"sv) + { + return Action::Slow; + } + if (eString == "slowoff"sv) + { + return Action::Slowoff; + } + if (eString == "subscribers"sv) + { + return Action::Subscribers; + } + if (eString == "subscribersoff"sv) + { + return Action::Subscribersoff; + } + if (eString == "unraid"sv) + { + return Action::Unraid; + } + if (eString == "delete"sv) + { + return Action::DeleteMessage; + } + if (eString == "unvip"sv) + { + return Action::Unvip; + } + if (eString == "vip"sv) + { + return Action::Vip; + } + if (eString == "raid"sv) + { + return Action::Raid; + } + if (eString == "add_blocked_term"sv) + { + return Action::AddBlockedTerm; + } + if (eString == "add_permitted_term"sv) + { + return Action::AddPermittedTerm; + } + if (eString == "remove_blocked_term"sv) + { + return Action::RemoveBlockedTerm; + } + if (eString == "remove_permitted_term"sv) + { + return Action::RemovePermittedTerm; + } + if (eString == "mod"sv) + { + return Action::Mod; + } + if (eString == "unmod"sv) + { + return Action::Unmod; + } + if (eString == "approve_unban_request"sv) + { + return Action::ApproveUnbanRequest; + } + if (eString == "deny_unban_request"sv) + { + return Action::DenyUnbanRequest; + } + if (eString == "warn"sv) + { + return Action::Warn; + } + if (eString == "shared_chat_ban"sv) + { + return Action::SharedChatBan; + } + if (eString == "shared_chat_timeout"sv) + { + return Action::SharedChatTimeout; + } + if (eString == "shared_chat_unban"sv) + { + return Action::SharedChatUnban; + } + if (eString == "shared_chat_untimeout"sv) + { + return Action::SharedChatUntimeout; + } + if (eString == "shared_chat_delete"sv) + { + return Action::SharedChatDelete; + } + + static const error::ApplicationErrorCategory errorEnumNameDidNotExist{ + "Action did not have a constant that matched this string"}; + return boost::system::error_code{129, errorEnumNameDidNotExist}; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Followers must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvfollowDurationMinutes = + root.if_contains("follow_duration_minutes"); + if (jvfollowDurationMinutes == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_followDurationMinutes{ + "Missing required key follow_duration_minutes"}; + return boost::system::error_code{ + 129, error_missing_field_followDurationMinutes}; + } + + auto followDurationMinutes = + boost::json::try_value_to(*jvfollowDurationMinutes); + + if (followDurationMinutes.has_error()) + { + return followDurationMinutes.error(); + } + + return Followers{ + .followDurationMinutes = std::move(followDurationMinutes.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Slow must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvwaitTimeSeconds = root.if_contains("wait_time_seconds"); + if (jvwaitTimeSeconds == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_waitTimeSeconds{ + "Missing required key wait_time_seconds"}; + return boost::system::error_code{129, + error_missing_field_waitTimeSeconds}; + } + + auto waitTimeSeconds = boost::json::try_value_to(*jvwaitTimeSeconds); + + if (waitTimeSeconds.has_error()) + { + return waitTimeSeconds.error(); + } + + return Slow{ + .waitTimeSeconds = std::move(waitTimeSeconds.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Vip 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + return Vip{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Unvip 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + return Unvip{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Mod 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + return Mod{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Unmod 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + return Unmod{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Ban 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(*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(*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(*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(*jvreason); + + if (reason.has_error()) + { + return reason.error(); + } + + return Ban{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + .reason = std::move(reason.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Unban 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + return Unban{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Timeout 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(*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(*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(*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(*jvreason); + + if (reason.has_error()) + { + return reason.error(); + } + + const auto *jvexpiresAt = root.if_contains("expires_at"); + if (jvexpiresAt == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_expiresAt{"Missing required key expires_at"}; + return boost::system::error_code{129, error_missing_field_expiresAt}; + } + + auto expiresAt = boost::json::try_value_to(*jvexpiresAt); + + if (expiresAt.has_error()) + { + return expiresAt.error(); + } + + return Timeout{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + .reason = std::move(reason.value()), + .expiresAt = std::move(expiresAt.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Untimeout 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + return Untimeout{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Raid 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + const auto *jvviewerCount = root.if_contains("viewer_count"); + if (jvviewerCount == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_viewerCount{ + "Missing required key viewer_count"}; + return boost::system::error_code{129, error_missing_field_viewerCount}; + } + + auto viewerCount = boost::json::try_value_to(*jvviewerCount); + + if (viewerCount.has_error()) + { + return viewerCount.error(); + } + + return Raid{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + .viewerCount = std::move(viewerCount.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Unraid 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + return Unraid{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Delete 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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.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(*jvmessageID); + + if (messageID.has_error()) + { + return messageID.error(); + } + + const auto *jvmessageBody = root.if_contains("message_body"); + if (jvmessageBody == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_messageBody{ + "Missing required key message_body"}; + return boost::system::error_code{129, error_missing_field_messageBody}; + } + + auto messageBody = boost::json::try_value_to(*jvmessageBody); + + if (messageBody.has_error()) + { + return messageBody.error(); + } + + return Delete{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + .messageID = std::move(messageID.value()), + .messageBody = std::move(messageBody.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "AutomodTerms must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvaction = root.if_contains("action"); + if (jvaction == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_action{ + "Missing required key action"}; + return boost::system::error_code{129, error_missing_field_action}; + } + + auto action = boost::json::try_value_to(*jvaction); + + if (action.has_error()) + { + return action.error(); + } + + const auto *jvlist = root.if_contains("list"); + if (jvlist == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_list{ + "Missing required key list"}; + return boost::system::error_code{129, error_missing_field_list}; + } + + auto list = boost::json::try_value_to(*jvlist); + + if (list.has_error()) + { + return list.error(); + } + + const auto *jvterms = root.if_contains("terms"); + if (jvterms == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_terms{ + "Missing required key terms"}; + return boost::system::error_code{129, error_missing_field_terms}; + } + const auto terms = + boost::json::try_value_to>(*jvterms); + if (terms.has_error()) + { + return terms.error(); + } + + const auto *jvfromAutomod = root.if_contains("from_automod"); + if (jvfromAutomod == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_fromAutomod{ + "Missing required key from_automod"}; + return boost::system::error_code{129, error_missing_field_fromAutomod}; + } + + auto fromAutomod = boost::json::try_value_to(*jvfromAutomod); + + if (fromAutomod.has_error()) + { + return fromAutomod.error(); + } + + return AutomodTerms{ + .action = std::move(action.value()), + .list = std::move(list.value()), + .terms = terms.value(), + .fromAutomod = std::move(fromAutomod.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "UnbanRequest must be an object"}; + return boost::system::error_code{129, errorMustBeObject}; + } + const auto &root = jvRoot.get_object(); + + const auto *jvisApproved = root.if_contains("is_approved"); + if (jvisApproved == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_isApproved{"Missing required key is_approved"}; + return boost::system::error_code{129, error_missing_field_isApproved}; + } + + auto isApproved = boost::json::try_value_to(*jvisApproved); + + if (isApproved.has_error()) + { + return isApproved.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(*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(*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(*jvuserName); + + if (userName.has_error()) + { + return userName.error(); + } + + const auto *jvmoderatorMessage = root.if_contains("moderator_message"); + if (jvmoderatorMessage == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_moderatorMessage{ + "Missing required key moderator_message"}; + return boost::system::error_code{129, + error_missing_field_moderatorMessage}; + } + + auto moderatorMessage = + boost::json::try_value_to(*jvmoderatorMessage); + + if (moderatorMessage.has_error()) + { + return moderatorMessage.error(); + } + + return UnbanRequest{ + .isApproved = std::move(isApproved.value()), + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + .moderatorMessage = std::move(moderatorMessage.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot) +{ + if (!jvRoot.is_object()) + { + static const error::ApplicationErrorCategory errorMustBeObject{ + "Warn 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(*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(*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(*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(*jvreason); + + if (reason.has_error()) + { + return reason.error(); + } + + const auto *jvchatRulesCited = root.if_contains("chat_rules_cited"); + if (jvchatRulesCited == nullptr) + { + static const error::ApplicationErrorCategory + error_missing_field_chatRulesCited{ + "Missing required key chat_rules_cited"}; + return boost::system::error_code{129, + error_missing_field_chatRulesCited}; + } + const auto chatRulesCited = + boost::json::try_value_to>(*jvchatRulesCited); + if (chatRulesCited.has_error()) + { + return chatRulesCited.error(); + } + + return Warn{ + .userID = std::move(userID.value()), + .userLogin = std::move(userLogin.value()), + .userName = std::move(userName.value()), + .reason = std::move(reason.value()), + .chatRulesCited = chatRulesCited.value(), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*jvbroadcasterUserName); + + if (broadcasterUserName.has_error()) + { + return broadcasterUserName.error(); + } + + std::optional sourceBroadcasterUserID = std::nullopt; + const auto *jvsourceBroadcasterUserID = + root.if_contains("source_broadcaster_user_id"); + if (jvsourceBroadcasterUserID != nullptr && + !jvsourceBroadcasterUserID->is_null()) + { + auto tsourceBroadcasterUserID = + boost::json::try_value_to(*jvsourceBroadcasterUserID); + + if (tsourceBroadcasterUserID.has_error()) + { + return tsourceBroadcasterUserID.error(); + } + sourceBroadcasterUserID = std::move(tsourceBroadcasterUserID.value()); + } + + std::optional sourceBroadcasterUserLogin = std::nullopt; + const auto *jvsourceBroadcasterUserLogin = + root.if_contains("source_broadcaster_user_login"); + if (jvsourceBroadcasterUserLogin != nullptr && + !jvsourceBroadcasterUserLogin->is_null()) + { + auto tsourceBroadcasterUserLogin = + boost::json::try_value_to( + *jvsourceBroadcasterUserLogin); + + if (tsourceBroadcasterUserLogin.has_error()) + { + return tsourceBroadcasterUserLogin.error(); + } + sourceBroadcasterUserLogin = + std::move(tsourceBroadcasterUserLogin.value()); + } + + std::optional sourceBroadcasterUserName = std::nullopt; + const auto *jvsourceBroadcasterUserName = + root.if_contains("source_broadcaster_user_name"); + if (jvsourceBroadcasterUserName != nullptr && + !jvsourceBroadcasterUserName->is_null()) + { + auto tsourceBroadcasterUserName = + boost::json::try_value_to( + *jvsourceBroadcasterUserName); + + if (tsourceBroadcasterUserName.has_error()) + { + return tsourceBroadcasterUserName.error(); + } + sourceBroadcasterUserName = + std::move(tsourceBroadcasterUserName.value()); + } + + 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(*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(*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(*jvmoderatorUserName); + + if (moderatorUserName.has_error()) + { + return moderatorUserName.error(); + } + + const auto *jvaction = root.if_contains("action"); + if (jvaction == nullptr) + { + static const error::ApplicationErrorCategory error_missing_field_action{ + "Missing required key action"}; + return boost::system::error_code{129, error_missing_field_action}; + } + + auto action = boost::json::try_value_to(*jvaction); + + if (action.has_error()) + { + return action.error(); + } + + std::optional followers = std::nullopt; + const auto *jvfollowers = root.if_contains("followers"); + if (jvfollowers != nullptr && !jvfollowers->is_null()) + { + auto tfollowers = boost::json::try_value_to(*jvfollowers); + + if (tfollowers.has_error()) + { + return tfollowers.error(); + } + followers = std::move(tfollowers.value()); + } + + std::optional slow = std::nullopt; + const auto *jvslow = root.if_contains("slow"); + if (jvslow != nullptr && !jvslow->is_null()) + { + auto tslow = boost::json::try_value_to(*jvslow); + + if (tslow.has_error()) + { + return tslow.error(); + } + slow = std::move(tslow.value()); + } + + std::optional vip = std::nullopt; + const auto *jvvip = root.if_contains("vip"); + if (jvvip != nullptr && !jvvip->is_null()) + { + auto tvip = boost::json::try_value_to(*jvvip); + + if (tvip.has_error()) + { + return tvip.error(); + } + vip = std::move(tvip.value()); + } + + std::optional unvip = std::nullopt; + const auto *jvunvip = root.if_contains("unvip"); + if (jvunvip != nullptr && !jvunvip->is_null()) + { + auto tunvip = boost::json::try_value_to(*jvunvip); + + if (tunvip.has_error()) + { + return tunvip.error(); + } + unvip = std::move(tunvip.value()); + } + + std::optional unmod = std::nullopt; + const auto *jvunmod = root.if_contains("unmod"); + if (jvunmod != nullptr && !jvunmod->is_null()) + { + auto tunmod = boost::json::try_value_to(*jvunmod); + + if (tunmod.has_error()) + { + return tunmod.error(); + } + unmod = std::move(tunmod.value()); + } + + std::optional ban = std::nullopt; + const auto *jvban = root.if_contains("ban"); + if (jvban != nullptr && !jvban->is_null()) + { + auto tban = boost::json::try_value_to(*jvban); + + if (tban.has_error()) + { + return tban.error(); + } + ban = std::move(tban.value()); + } + + std::optional unban = std::nullopt; + const auto *jvunban = root.if_contains("unban"); + if (jvunban != nullptr && !jvunban->is_null()) + { + auto tunban = boost::json::try_value_to(*jvunban); + + if (tunban.has_error()) + { + return tunban.error(); + } + unban = std::move(tunban.value()); + } + + std::optional timeout = std::nullopt; + const auto *jvtimeout = root.if_contains("timeout"); + if (jvtimeout != nullptr && !jvtimeout->is_null()) + { + auto ttimeout = boost::json::try_value_to(*jvtimeout); + + if (ttimeout.has_error()) + { + return ttimeout.error(); + } + timeout = std::move(ttimeout.value()); + } + + std::optional untimeout = std::nullopt; + const auto *jvuntimeout = root.if_contains("untimeout"); + if (jvuntimeout != nullptr && !jvuntimeout->is_null()) + { + auto tuntimeout = boost::json::try_value_to(*jvuntimeout); + + if (tuntimeout.has_error()) + { + return tuntimeout.error(); + } + untimeout = std::move(tuntimeout.value()); + } + + std::optional raid = std::nullopt; + const auto *jvraid = root.if_contains("raid"); + if (jvraid != nullptr && !jvraid->is_null()) + { + auto traid = boost::json::try_value_to(*jvraid); + + if (traid.has_error()) + { + return traid.error(); + } + raid = std::move(traid.value()); + } + + std::optional unraid = std::nullopt; + const auto *jvunraid = root.if_contains("unraid"); + if (jvunraid != nullptr && !jvunraid->is_null()) + { + auto tunraid = boost::json::try_value_to(*jvunraid); + + if (tunraid.has_error()) + { + return tunraid.error(); + } + unraid = std::move(tunraid.value()); + } + + std::optional deleteMessage = std::nullopt; + const auto *jvdeleteMessage = root.if_contains("delete"); + if (jvdeleteMessage != nullptr && !jvdeleteMessage->is_null()) + { + auto tdeleteMessage = + boost::json::try_value_to(*jvdeleteMessage); + + if (tdeleteMessage.has_error()) + { + return tdeleteMessage.error(); + } + deleteMessage = std::move(tdeleteMessage.value()); + } + + std::optional automodTerms = std::nullopt; + const auto *jvautomodTerms = root.if_contains("automod_terms"); + if (jvautomodTerms != nullptr && !jvautomodTerms->is_null()) + { + auto tautomodTerms = + boost::json::try_value_to(*jvautomodTerms); + + if (tautomodTerms.has_error()) + { + return tautomodTerms.error(); + } + automodTerms = std::move(tautomodTerms.value()); + } + + std::optional unbanRequest = std::nullopt; + const auto *jvunbanRequest = root.if_contains("unban_request"); + if (jvunbanRequest != nullptr && !jvunbanRequest->is_null()) + { + auto tunbanRequest = + boost::json::try_value_to(*jvunbanRequest); + + if (tunbanRequest.has_error()) + { + return tunbanRequest.error(); + } + unbanRequest = std::move(tunbanRequest.value()); + } + + std::optional warn = std::nullopt; + const auto *jvwarn = root.if_contains("warn"); + if (jvwarn != nullptr && !jvwarn->is_null()) + { + auto twarn = boost::json::try_value_to(*jvwarn); + + if (twarn.has_error()) + { + return twarn.error(); + } + warn = std::move(twarn.value()); + } + + std::optional sharedChatBan = std::nullopt; + const auto *jvsharedChatBan = root.if_contains("shared_chat_ban"); + if (jvsharedChatBan != nullptr && !jvsharedChatBan->is_null()) + { + auto tsharedChatBan = boost::json::try_value_to(*jvsharedChatBan); + + if (tsharedChatBan.has_error()) + { + return tsharedChatBan.error(); + } + sharedChatBan = std::move(tsharedChatBan.value()); + } + + std::optional sharedChatUnban = std::nullopt; + const auto *jvsharedChatUnban = root.if_contains("shared_chat_unban"); + if (jvsharedChatUnban != nullptr && !jvsharedChatUnban->is_null()) + { + auto tsharedChatUnban = + boost::json::try_value_to(*jvsharedChatUnban); + + if (tsharedChatUnban.has_error()) + { + return tsharedChatUnban.error(); + } + sharedChatUnban = std::move(tsharedChatUnban.value()); + } + + std::optional sharedChatTimeout = std::nullopt; + const auto *jvsharedChatTimeout = root.if_contains("shared_chat_timeout"); + if (jvsharedChatTimeout != nullptr && !jvsharedChatTimeout->is_null()) + { + auto tsharedChatTimeout = + boost::json::try_value_to(*jvsharedChatTimeout); + + if (tsharedChatTimeout.has_error()) + { + return tsharedChatTimeout.error(); + } + sharedChatTimeout = std::move(tsharedChatTimeout.value()); + } + + std::optional sharedChatUntimeout = std::nullopt; + const auto *jvsharedChatUntimeout = + root.if_contains("shared_chat_untimeout"); + if (jvsharedChatUntimeout != nullptr && !jvsharedChatUntimeout->is_null()) + { + auto tsharedChatUntimeout = + boost::json::try_value_to(*jvsharedChatUntimeout); + + if (tsharedChatUntimeout.has_error()) + { + return tsharedChatUntimeout.error(); + } + sharedChatUntimeout = std::move(tsharedChatUntimeout.value()); + } + + std::optional sharedChatDelete = std::nullopt; + const auto *jvsharedChatDelete = root.if_contains("shared_chat_delete"); + if (jvsharedChatDelete != nullptr && !jvsharedChatDelete->is_null()) + { + auto tsharedChatDelete = + boost::json::try_value_to(*jvsharedChatDelete); + + if (tsharedChatDelete.has_error()) + { + return tsharedChatDelete.error(); + } + sharedChatDelete = std::move(tsharedChatDelete.value()); + } + + return Event{ + .broadcasterUserID = std::move(broadcasterUserID.value()), + .broadcasterUserLogin = std::move(broadcasterUserLogin.value()), + .broadcasterUserName = std::move(broadcasterUserName.value()), + .sourceBroadcasterUserID = std::move(sourceBroadcasterUserID), + .sourceBroadcasterUserLogin = std::move(sourceBroadcasterUserLogin), + .sourceBroadcasterUserName = std::move(sourceBroadcasterUserName), + .moderatorUserID = std::move(moderatorUserID.value()), + .moderatorUserLogin = std::move(moderatorUserLogin.value()), + .moderatorUserName = std::move(moderatorUserName.value()), + .action = std::move(action.value()), + .followers = std::move(followers), + .slow = std::move(slow), + .vip = std::move(vip), + .unvip = std::move(unvip), + .unmod = std::move(unmod), + .ban = std::move(ban), + .unban = std::move(unban), + .timeout = std::move(timeout), + .untimeout = std::move(untimeout), + .raid = std::move(raid), + .unraid = std::move(unraid), + .deleteMessage = std::move(deleteMessage), + .automodTerms = std::move(automodTerms), + .unbanRequest = std::move(unbanRequest), + .warn = std::move(warn), + .sharedChatBan = std::move(sharedChatBan), + .sharedChatUnban = std::move(sharedChatUnban), + .sharedChatTimeout = std::move(sharedChatTimeout), + .sharedChatUntimeout = std::move(sharedChatUntimeout), + .sharedChatDelete = std::move(sharedChatDelete), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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_moderate::v2 diff --git a/lib/twitch-eventsub-ws/src/payloads/channel-update-v1.cpp b/lib/twitch-eventsub-ws/src/payloads/channel-update-v1.cpp new file mode 100644 index 00000000..14994e2a --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/channel-update-v1.cpp @@ -0,0 +1,215 @@ +#include "twitch-eventsub-ws/payloads/channel-update-v1.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::payload::channel_update::v1 { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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(*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(*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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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 diff --git a/lib/twitch-eventsub-ws/src/payloads/new-payload.sh b/lib/twitch-eventsub-ws/src/payloads/new-payload.sh new file mode 100755 index 00000000..5020f3c2 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/new-payload.sh @@ -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 (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 + +#include + +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 + +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" diff --git a/lib/twitch-eventsub-ws/src/payloads/session-welcome.cpp b/lib/twitch-eventsub-ws/src/payloads/session-welcome.cpp new file mode 100644 index 00000000..3d6b86be --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/session-welcome.cpp @@ -0,0 +1,57 @@ +#include "twitch-eventsub-ws/payloads/session-welcome.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::payload::session_welcome { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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 diff --git a/lib/twitch-eventsub-ws/src/payloads/stream-offline-v1.cpp b/lib/twitch-eventsub-ws/src/payloads/stream-offline-v1.cpp new file mode 100644 index 00000000..62f8dfb7 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/stream-offline-v1.cpp @@ -0,0 +1,134 @@ +#include "twitch-eventsub-ws/payloads/stream-offline-v1.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::payload::stream_offline::v1 { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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 diff --git a/lib/twitch-eventsub-ws/src/payloads/stream-online-v1.cpp b/lib/twitch-eventsub-ws/src/payloads/stream-online-v1.cpp new file mode 100644 index 00000000..7d399b7f --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/stream-online-v1.cpp @@ -0,0 +1,182 @@ +#include "twitch-eventsub-ws/payloads/stream-online-v1.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::payload::stream_online::v1 { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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(*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(*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(*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(*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::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*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 diff --git a/lib/twitch-eventsub-ws/src/payloads/subscription.cpp b/lib/twitch-eventsub-ws/src/payloads/subscription.cpp new file mode 100644 index 00000000..1145c36b --- /dev/null +++ b/lib/twitch-eventsub-ws/src/payloads/subscription.cpp @@ -0,0 +1,186 @@ +#include "twitch-eventsub-ws/payloads/subscription.hpp" + +#include "twitch-eventsub-ws/errors.hpp" + +#include + +namespace chatterino::eventsub::lib::payload::subscription { + +// DESERIALIZATION IMPLEMENTATION START +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, 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(*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(*jvsessionID); + + if (sessionID.has_error()) + { + return sessionID.error(); + } + + return Transport{ + .method = std::move(method.value()), + .sessionID = std::move(sessionID.value()), + }; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, + 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(*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(*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(*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(*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(*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(*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(*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 diff --git a/lib/twitch-eventsub-ws/src/session.cpp b/lib/twitch-eventsub-ws/src/session.cpp new file mode 100644 index 00000000..751a6ae7 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/session.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace websocket = beast::websocket; + +namespace chatterino::eventsub::lib { + +// Subscription Type + Subscription Version +using EventSubSubscription = std::pair; + +using NotificationHandlers = std::unordered_map< + EventSubSubscription, + std::function &)>, + boost::hash>; + +using MessageHandlers = std::unordered_map< + std::string, std::function &, + const NotificationHandlers &)>>; + +namespace { + + // Report a failure + void fail(beast::error_code ec, char const *what) + { + std::cerr << what << ": " << ec.message() << "\n"; + } + + template + std::optional parsePayload(const boost::json::value &jv) + { + auto result = boost::json::try_value_to(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(jv); + if (!oPayload) + { + return; + } + listener->onChannelBan(metadata, *oPayload); + }, + }, + { + {"stream.online", "1"}, + [](const auto &metadata, const auto &jv, auto &listener) { + auto oPayload = + parsePayload(jv); + if (!oPayload) + { + return; + } + listener->onStreamOnline(metadata, *oPayload); + }, + }, + { + {"stream.offline", "1"}, + [](const auto &metadata, const auto &jv, auto &listener) { + auto oPayload = + parsePayload(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(jv); + if (!oPayload) + { + return; + } + listener->onChannelUpdate(metadata, *oPayload); + }, + }, + { + {"channel.chat.message", "1"}, + [](const auto &metadata, const auto &jv, auto &listener) { + auto oPayload = + parsePayload( + jv); + if (!oPayload) + { + return; + } + listener->onChannelChatMessage(metadata, *oPayload); + }, + }, + { + {"channel.moderate", "2"}, + [](const auto &metadata, const auto &jv, auto &listener) { + auto oPayload = + parsePayload(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(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, + 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(*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) + : 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(::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 diff --git a/lib/twitch-eventsub-ws/src/string.cpp b/lib/twitch-eventsub-ws/src/string.cpp new file mode 100644 index 00000000..8d83ab80 --- /dev/null +++ b/lib/twitch-eventsub-ws/src/string.cpp @@ -0,0 +1,23 @@ +#include "twitch-eventsub-ws/string.hpp" + +#include +#include + +#include + +namespace chatterino::eventsub::lib { + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag /*tag*/, + const boost::json::value &jvRoot) +{ + auto v = boost::json::try_value_to(jvRoot); + if (v.has_error()) + { + return v.error(); + } + + return String(std::move(v.value())); +} + +} // namespace chatterino::eventsub::lib diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 336e6a1a..2964ec9a 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -271,6 +271,13 @@ public: return nullptr; } + eventsub::IController *getEventSub() override + { + assert(false && "EmptyApplication::getEventSub was called without " + "being initialized"); + return nullptr; + } + QTemporaryDir settingsDir; Paths paths_; Args args_; diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp index ecc6e84d..06f75ab2 100644 --- a/mocks/include/mocks/Helix.hpp +++ b/mocks/include/mocks/Helix.hpp @@ -427,6 +427,15 @@ public: FailureCallback failureCallback), (override)); + MOCK_METHOD(void, createEventSubSubscription, + (const eventsub::SubscriptionRequest &request, + const QString &sessionID, + ResultCallback + successCallback, + (FailureCallback + failureCallback)), + (override)); + MOCK_METHOD(void, update, (QString clientId, QString oauthToken), (override)); diff --git a/resources/licenses/howard-hinnant-date.txt b/resources/licenses/howard-hinnant-date.txt new file mode 100644 index 00000000..efd85467 --- /dev/null +++ b/resources/licenses/howard-hinnant-date.txt @@ -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. diff --git a/src/Application.cpp b/src/Application.cpp index 8cb22110..bac97052 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -18,6 +18,7 @@ #include "providers/pronouns/Pronouns.hpp" #include "providers/seventv/SeventvAPI.hpp" #include "providers/seventv/SeventvEmotes.hpp" +#include "providers/twitch/eventsub/Controller.hpp" #include "providers/twitch/TwitchBadges.hpp" #include "singletons/ImageUploader.hpp" #ifdef CHATTERINO_HAVE_PLUGINS @@ -120,6 +121,18 @@ SeventvEventAPI *makeSeventvEventAPI(Settings &settings) 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"; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) @@ -180,6 +193,7 @@ Application::Application(Settings &_settings, const Paths &paths, , streamerMode(new StreamerMode) , twitchUsers(new TwitchUsers) , pronouns(new pronouns::Pronouns) + , eventSub(makeEventSubController(_settings)) #ifdef CHATTERINO_HAVE_PLUGINS , plugins(new PluginController(paths)) #endif @@ -576,6 +590,14 @@ pronouns::Pronouns *Application::getPronouns() return this->pronouns.get(); } +eventsub::IController *Application::getEventSub() +{ + assertInGuiThread(); + assert(this->eventSub); + + return this->eventSub.get(); +} + void Application::save() { this->commands->save(); diff --git a/src/Application.hpp b/src/Application.hpp index 71145b16..334485f4 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -57,6 +57,9 @@ class ITwitchUsers; namespace pronouns { class Pronouns; } // namespace pronouns +namespace eventsub { + class IController; +} // namespace eventsub class IApplication { @@ -109,6 +112,7 @@ public: virtual IStreamerMode *getStreamerMode() = 0; virtual ITwitchUsers *getTwitchUsers() = 0; virtual pronouns::Pronouns *getPronouns() = 0; + virtual eventsub::IController *getEventSub() = 0; }; class Application : public IApplication @@ -174,6 +178,7 @@ private: std::unique_ptr streamerMode; std::unique_ptr twitchUsers; std::unique_ptr pronouns; + std::unique_ptr eventSub; #ifdef CHATTERINO_HAVE_PLUGINS std::unique_ptr plugins; #endif @@ -221,6 +226,7 @@ public: SeventvEmotes *getSeventvEmotes() override; SeventvEventAPI *getSeventvEventAPI() override; pronouns::Pronouns *getPronouns() override; + eventsub::IController *getEventSub() override; ILinkResolver *getLinkResolver() override; IStreamerMode *getStreamerMode() override; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bcc5311d..d72d3ee1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -428,6 +428,17 @@ set(SOURCE_FILES providers/twitch/TwitchUsers.cpp providers/twitch/TwitchUsers.hpp + providers/twitch/eventsub/Connection.cpp + providers/twitch/eventsub/Connection.hpp + providers/twitch/eventsub/Controller.cpp + providers/twitch/eventsub/Controller.hpp + providers/twitch/eventsub/MessageBuilder.cpp + providers/twitch/eventsub/MessageBuilder.hpp + providers/twitch/eventsub/SubscriptionHandle.cpp + providers/twitch/eventsub/SubscriptionHandle.hpp + providers/twitch/eventsub/SubscriptionRequest.cpp + providers/twitch/eventsub/SubscriptionRequest.hpp + providers/twitch/pubsubmessages/AutoMod.cpp providers/twitch/pubsubmessages/AutoMod.hpp providers/twitch/pubsubmessages/Base.cpp @@ -804,6 +815,8 @@ target_link_libraries(${LIBRARY_PROJECT} LRUCache MagicEnum $<$:Wtsapi32> + twitch-eventsub-ws + BoostCertify ) if (CHATTERINO_PLUGINS) target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2) diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index 554fc48a..2feb4118 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -52,6 +52,8 @@ Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold); Q_LOGGING_CATEGORY(chatterinoTheme, "chatterino.theme", logThreshold); Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold); Q_LOGGING_CATEGORY(chatterinoTwitch, "chatterino.twitch", logThreshold); +Q_LOGGING_CATEGORY(chatterinoTwitchEventSub, "chatterino.twitch.eventsub", + logThreshold); Q_LOGGING_CATEGORY(chatterinoTwitchLiveController, "chatterino.twitch.livecontroller", logThreshold); Q_LOGGING_CATEGORY(chatterinoUpdate, "chatterino.update", logThreshold); diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index f4da615c..0da52a72 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -40,6 +40,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink); Q_DECLARE_LOGGING_CATEGORY(chatterinoTheme); Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer); Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch); +Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitchEventSub); Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitchLiveController); Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate); Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket); diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 3fe39864..15d70f9b 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -461,6 +461,8 @@ CommandController::CommandController(const Paths &paths) this->registerCommand("/debug-test", &commands::debugTest); + this->registerCommand("/debug-eventsub", &commands::debugEventSub); + this->registerCommand("/shield", &commands::shieldModeOn); this->registerCommand("/shieldoff", &commands::shieldModeOff); diff --git a/src/controllers/commands/builtin/chatterino/Debugging.cpp b/src/controllers/commands/builtin/chatterino/Debugging.cpp index 3dad9f68..b11d0c0c 100644 --- a/src/controllers/commands/builtin/chatterino/Debugging.cpp +++ b/src/controllers/commands/builtin/chatterino/Debugging.cpp @@ -9,6 +9,7 @@ #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" +#include "providers/twitch/eventsub/Controller.hpp" #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" @@ -189,4 +190,57 @@ QString debugTest(const CommandContext &ctx) return ""; } +QString debugEventSub(const CommandContext &ctx) +{ + (void)ctx; + + if (ctx.twitchChannel == nullptr) + { + return ""; + } + + // purposefully subscribe multiple times to ensure we can't sub too many times + // or create too many connections + + static eventsub::SubscriptionHandle handle1; + + handle1 = getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ + .subscriptionType = "channel.ban", + .subscriptionVersion = "1", + .conditions = + { + { + "broadcaster_user_id", + ctx.twitchChannel->roomId(), + }, + }, + }); + auto handle2 = + getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ + .subscriptionType = "channel.ban", + .subscriptionVersion = "1", + .conditions = + { + { + "broadcaster_user_id", + ctx.twitchChannel->roomId(), + }, + }, + }); + auto handle3 = + getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ + .subscriptionType = "channel.ban", + .subscriptionVersion = "1", + .conditions = + { + { + "broadcaster_user_id", + ctx.twitchChannel->roomId(), + }, + }, + }); + + return ""; +} + } // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/chatterino/Debugging.hpp b/src/controllers/commands/builtin/chatterino/Debugging.hpp index 0cfa448e..2c02dcb2 100644 --- a/src/controllers/commands/builtin/chatterino/Debugging.hpp +++ b/src/controllers/commands/builtin/chatterino/Debugging.hpp @@ -24,4 +24,6 @@ QString forceImageUnload(const CommandContext &ctx); QString debugTest(const CommandContext &ctx); +QString debugEventSub(const CommandContext &ctx); + } // namespace chatterino::commands diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index afc1b528..7f6af752 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -44,6 +44,8 @@ struct Message { QString id; QString searchText; QString messageText; + // TODO: This field is used ambiguously, it could use a comment or a name change to + // clarify the intent of the field QString loginName; QString displayName; QString localizedName; diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 258d7534..9627d9a4 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -173,6 +173,12 @@ public: void appendOrEmplaceSystemTextAndUpdate(const QString &text, QString &toUpdate); + // Helper method that emplaces some text stylized as system text + // and then appends that text to the QString parameter "toUpdate". + // Returns the TextElement that was emplaced. + TextElement *emplaceSystemTextAndUpdate(const QString &text, + QString &toUpdate); + static void triggerHighlights(const Channel *channel, const HighlightAlert &alert); @@ -287,12 +293,6 @@ private: MessageElement &back(); std::unique_ptr releaseBack(); - // Helper method that emplaces some text stylized as system text - // and then appends that text to the QString parameter "toUpdate". - // Returns the TextElement that was emplaced. - TextElement *emplaceSystemTextAndUpdate(const QString &text, - QString &toUpdate); - void parse(); void parseUsernameColor(const QVariantMap &tags, const QString &userID); void parseUsername(const Communi::IrcMessage *ircMessage, diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 09c6f946..4dfe07a0 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -918,6 +918,22 @@ MentionElement::MentionElement(const QString &displayName, QString loginName_, { } +template +MentionElement::MentionElement(const QString &displayName, QString loginName_, + MessageColor fallbackColor_, QColor userColor_) + : TextElement(displayName, + {MessageElementFlag::Text, MessageElementFlag::Mention}) + , fallbackColor(fallbackColor_) + , userColor(userColor_.isValid() ? userColor_ : fallbackColor_) + , userLoginName(std::move(loginName_)) +{ +} + +template MentionElement::MentionElement(const QString &displayName, + QString loginName_, + MessageColor fallbackColor_, + QColor userColor_); + void MentionElement::addToContainer(MessageLayoutContainer &container, const MessageLayoutContext &ctx) { diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index a749839a..0cda360f 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -340,8 +340,15 @@ private: class MentionElement : public TextElement { public: - MentionElement(const QString &displayName, QString loginName_, - MessageColor fallbackColor_, MessageColor userColor_); + explicit MentionElement(const QString &displayName, QString loginName_, + MessageColor fallbackColor_, + MessageColor userColor_); + /// Deprioritized ctor allowing us to pass through a potentially invalid userColor_ + /// + /// If the userColor_ is invalid, we fall back to the fallbackColor_ + template + explicit MentionElement(const QString &displayName, QString loginName_, + MessageColor fallbackColor_, QColor userColor_); ~MentionElement() override = default; MentionElement(const MentionElement &) = delete; MentionElement(MentionElement &&) = delete; diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index a273c1d1..82793066 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -29,6 +29,7 @@ #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/ChannelPointReward.hpp" +#include "providers/twitch/eventsub/Controller.hpp" #include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/PubSubManager.hpp" #include "providers/twitch/TwitchAccount.hpp" @@ -1472,6 +1473,39 @@ void TwitchChannel::refreshPubSub() { getApp()->getTwitchPubSub()->listenToAutomod(roomId); getApp()->getTwitchPubSub()->listenToLowTrustUsers(roomId); + this->eventSubChannelBanHandle = + getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ + .subscriptionType = "channel.ban", + .subscriptionVersion = "1", + .conditions = + { + { + "broadcaster_user_id", + roomId, + }, + }, + }); + + this->eventSubChannelModerateHandle = + getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ + .subscriptionType = "channel.moderate", + .subscriptionVersion = "2", + .conditions = + { + { + "broadcaster_user_id", + roomId, + }, + { + "moderator_user_id", + currentAccount->getUserId(), + }, + }, + }); + } + else + { + this->eventSubChannelBanHandle.reset(); } getApp()->getTwitchPubSub()->listenToChannelPointRewards(roomId); } diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index eb15e78c..aa91399e 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -8,6 +8,7 @@ #include "common/UniqueAccess.hpp" #include "providers/ffz/FfzBadges.hpp" #include "providers/ffz/FfzEmotes.hpp" +#include "providers/twitch/eventsub/SubscriptionHandle.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "util/QStringHash.hpp" #include "util/ThreadGuard.hpp" @@ -499,6 +500,9 @@ private: pajlada::Signals::SignalHolder signalHolder_; std::vector bSignals_; + eventsub::SubscriptionHandle eventSubChannelBanHandle; + eventsub::SubscriptionHandle eventSubChannelModerateHandle; + friend class TwitchIrcServer; friend class MessageBuilder; friend class IrcMessageHandler; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 223317a7..cef4515d 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -3216,6 +3216,124 @@ void Helix::getFollowedChannel( .execute(); } +void Helix::createEventSubSubscription( + const eventsub::SubscriptionRequest &request, const QString &sessionID, + ResultCallback successCallback, + FailureCallback + failureCallback) +{ + using Error = HelixCreateEventSubSubscriptionError; + + QJsonObject condition; + for (const auto &[conditionKey, conditionValue] : request.conditions) + { + condition.insert(conditionKey, conditionValue); + } + + QJsonObject body; + body.insert("type", request.subscriptionType); + body.insert("version", request.subscriptionVersion); + body.insert("condition", condition); + + QJsonObject transport; + transport.insert("method", "websocket"); + transport.insert("session_id", sessionID); + + body.insert("transport", transport); + + this->makePost("eventsub/subscriptions", {}) + .json(body) + .onSuccess([successCallback](const auto &result) { + if (result.status() != 202) + { + qCWarning(chatterinoTwitchEventSub) + << "Success result for creating eventsub subscription was " + << result.formatError() << "but we expected it to be 202"; + } + + HelixCreateEventSubSubscriptionResponse response( + result.parseJson()); + + successCallback(response); + }) + .onError([failureCallback](const NetworkResult &result) { + if (!result.status()) + { + failureCallback(Error::Forwarded, result.formatError()); + return; + } + + const auto obj = result.parseJson(); + auto message = obj["message"].toString(); + + switch (*result.status()) + { + case 400: { + failureCallback(Error::BadRequest, message); + } + break; + + case 401: { + failureCallback(Error::Unauthorized, message); + } + break; + + case 403: { + failureCallback(Error::Forbidden, message); + } + break; + case 409: { + failureCallback(Error::Conflict, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + case 500: { + if (message.isEmpty()) + { + failureCallback(Error::Forwarded, + "Twitch internal server error"); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + default: { + qCWarning(chatterinoTwitchEventSub) + << "Helix Create EventSub Subscription, unhandled " + "error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Forwarded, message); + } + } + }) + .execute(); +} + +QDebug &operator<<(QDebug &dbg, + const HelixCreateEventSubSubscriptionResponse &data) +{ + dbg << "HelixCreateEventSubSubscriptionResponse{ id:" << data.subscriptionID + << "status:" << data.subscriptionStatus + << "type:" << data.subscriptionType + << "version:" << data.subscriptionVersion + << "condition:" << data.subscriptionCondition + << "createdAt:" << data.subscriptionCreatedAt + << "sessionID:" << data.subscriptionSessionID + << "connectedAt:" << data.subscriptionConnectedAt + << "cost:" << data.subscriptionCost << "total:" << data.total + << "totalCost:" << data.totalCost + << "maxTotalCost:" << data.maxTotalCost << '}'; + return dbg; +} + NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery, NetworkRequestType type) { diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 7a3c6c6f..00dc6e7f 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -2,6 +2,7 @@ #include "common/Aliases.hpp" #include "common/network/NetworkRequest.hpp" +#include "providers/twitch/eventsub/SubscriptionRequest.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "util/Helpers.hpp" #include "util/QStringHash.hpp" @@ -809,6 +810,65 @@ struct HelixPaginationState { bool done; }; +struct HelixCreateEventSubSubscriptionResponse { + QString subscriptionID; + QString subscriptionStatus; + QString subscriptionType; + QString subscriptionVersion; + QJsonObject subscriptionCondition; + QString subscriptionCreatedAt; + QString subscriptionSessionID; + QString subscriptionConnectedAt; + int subscriptionCost; + + int total; + int totalCost; + int maxTotalCost; + + explicit HelixCreateEventSubSubscriptionResponse( + const QJsonObject &jsonObject) + { + { + auto jsonData = jsonObject.value("data").toArray().at(0).toObject(); + this->subscriptionID = jsonData.value("id").toString(); + this->subscriptionStatus = jsonData.value("status").toString(); + this->subscriptionType = jsonData.value("type").toString(); + this->subscriptionVersion = jsonData.value("version").toString(); + this->subscriptionCondition = + jsonData.value("condition").toObject(); + this->subscriptionCreatedAt = + jsonData.value("created_at").toString(); + this->subscriptionSessionID = jsonData.value("transport") + .toObject() + .value("session_id") + .toString(); + this->subscriptionConnectedAt = jsonData.value("transport") + .toObject() + .value("connected_at") + .toString(); + this->subscriptionCost = jsonData.value("cost").toInt(); + } + + this->total = jsonObject.value("total").toInt(); + this->totalCost = jsonObject.value("total_cost").toInt(); + this->maxTotalCost = jsonObject.value("max_total_cost").toInt(); + } + + friend QDebug &operator<<( + QDebug &dbg, const HelixCreateEventSubSubscriptionResponse &data); +}; + +enum class HelixCreateEventSubSubscriptionError : std::uint8_t { + BadRequest, + Unauthorized, + Forbidden, + Conflict, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + class IHelix { public: @@ -1150,6 +1210,13 @@ public: ResultCallback> successCallback, FailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference/#create-eventsub-subscription + virtual void createEventSubSubscription( + const eventsub::SubscriptionRequest &request, const QString &sessionID, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + virtual void update(QString clientId, QString oauthToken) = 0; protected: @@ -1493,6 +1560,13 @@ public: ResultCallback> successCallback, FailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference/#create-eventsub-subscription + void createEventSubSubscription( + const eventsub::SubscriptionRequest &request, const QString &sessionID, + ResultCallback successCallback, + FailureCallback + failureCallback) final; + void update(QString clientId, QString oauthToken) final; static void initialize(); diff --git a/src/providers/twitch/eventsub/Connection.cpp b/src/providers/twitch/eventsub/Connection.cpp new file mode 100644 index 00000000..d3d2f003 --- /dev/null +++ b/src/providers/twitch/eventsub/Connection.cpp @@ -0,0 +1,253 @@ +#include "providers/twitch/eventsub/Connection.hpp" + +#include "Application.hpp" +#include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/eventsub/MessageBuilder.hpp" +#include "providers/twitch/PubSubActions.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "util/PostToThread.hpp" + +#include +#include +#include +#include + +#include + +namespace { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +const auto &LOG = chatterinoTwitchEventSub; + +} // namespace + +namespace chatterino::eventsub { + +void Connection::onSessionWelcome( + lib::messages::Metadata metadata, + lib::payload::session_welcome::Payload payload) +{ + (void)metadata; + qCDebug(LOG) << "On session welcome:" << payload.id.c_str(); + + this->sessionID = QString::fromStdString(payload.id); +} + +void Connection::onNotification(lib::messages::Metadata metadata, + const boost::json::value &jv) +{ + (void)metadata; + auto jsonString = boost::json::serialize(jv); + qCDebug(LOG) << "on notification: " << jsonString.c_str(); +} + +void Connection::onChannelBan(lib::messages::Metadata metadata, + lib::payload::channel_ban::v1::Payload payload) +{ + (void)metadata; + + auto roomID = QString::fromStdString(payload.event.broadcasterUserID); + + BanAction action{}; + + action.timestamp = std::chrono::steady_clock::now(); + action.roomID = roomID; + action.source = ActionUser{ + .id = QString::fromStdString(payload.event.moderatorUserID), + .login = QString::fromStdString(payload.event.moderatorUserLogin), + .displayName = QString::fromStdString(payload.event.moderatorUserName), + }; + action.target = ActionUser{ + .id = QString::fromStdString(payload.event.userID), + .login = QString::fromStdString(payload.event.userLogin), + .displayName = QString::fromStdString(payload.event.userName), + }; + action.reason = QString::fromStdString(payload.event.reason); + if (payload.event.isPermanent) + { + action.duration = 0; + } + else + { + auto timeoutDuration = payload.event.timeoutDuration(); + auto timeoutDurationInSeconds = + std::chrono::duration_cast(timeoutDuration) + .count(); + action.duration = timeoutDurationInSeconds; + } + + auto chan = getApp()->getTwitch()->getChannelOrEmptyByID(roomID); + + runInGuiThread([action{std::move(action)}, chan{std::move(chan)}] { + auto time = QDateTime::currentDateTime(); + MessageBuilder msg(action, time); + msg->flags.set(MessageFlag::PubSub); + chan->addOrReplaceTimeout(msg.release(), QDateTime::currentDateTime()); + }); +} + +void Connection::onStreamOnline( + lib::messages::Metadata metadata, + lib::payload::stream_online::v1::Payload payload) +{ + (void)metadata; + qCDebug(LOG) << "On stream online event for channel" + << payload.event.broadcasterUserLogin.c_str(); +} + +void Connection::onStreamOffline( + lib::messages::Metadata metadata, + lib::payload::stream_offline::v1::Payload payload) +{ + (void)metadata; + qCDebug(LOG) << "On stream offline event for channel" + << payload.event.broadcasterUserLogin.c_str(); +} + +void Connection::onChannelChatNotification( + lib::messages::Metadata metadata, + lib::payload::channel_chat_notification::v1::Payload payload) +{ + (void)metadata; + qCDebug(LOG) << "On channel chat notification for" + << payload.event.broadcasterUserLogin.c_str(); +} + +void Connection::onChannelUpdate( + lib::messages::Metadata metadata, + lib::payload::channel_update::v1::Payload payload) +{ + (void)metadata; + qCDebug(LOG) << "On channel update for" + << payload.event.broadcasterUserLogin.c_str(); +} + +void Connection::onChannelChatMessage( + lib::messages::Metadata metadata, + lib::payload::channel_chat_message::v1::Payload payload) +{ + (void)metadata; + + qCDebug(LOG) << "Channel chat message event for" + << payload.event.broadcasterUserLogin.c_str(); +} + +void Connection::onChannelModerate( + lib::messages::Metadata metadata, + lib::payload::channel_moderate::v2::Payload payload) +{ + (void)metadata; + + using lib::payload::channel_moderate::v2::Action; + + auto channelPtr = getApp()->getTwitch()->getChannelOrEmpty( + payload.event.broadcasterUserLogin.qt()); + if (channelPtr->isEmpty()) + { + qCDebug(LOG) + << "Channel moderate event for broadcaster we're not interested in" + << payload.event.broadcasterUserLogin.qt(); + return; + } + + auto *channel = dynamic_cast(channelPtr.get()); + if (channel == nullptr) + { + qCDebug(LOG) + << "Channel moderate event for broadcaster is not a Twitch channel?" + << payload.event.broadcasterUserLogin.qt(); + return; + } + + const auto now = QDateTime::currentDateTime(); + + switch (payload.event.action) + { + case Action::Vip: { + const auto &oAction = payload.event.vip; + + if (!oAction.has_value()) + { + qCWarning(LOG) << "VIP action type had no VIP action body"; + return; + } + auto msg = + makeVipMessage(channel, now, payload.event, oAction.value()); + runInGuiThread([channel, msg] { + channel->addMessage(msg, MessageContext::Original); + }); + } + break; + + case Action::Unvip: { + const auto &oAction = payload.event.unvip; + + if (!oAction.has_value()) + { + qCWarning(LOG) << "UnVIP action type had no UnVIP action body"; + return; + } + auto msg = + makeUnvipMessage(channel, now, payload.event, oAction.value()); + runInGuiThread([channel, msg] { + channel->addMessage(msg, MessageContext::Original); + }); + } + break; + + case Action::Ban: + case Action::Timeout: + case Action::Unban: + case Action::Untimeout: + case Action::Clear: + case Action::Emoteonly: + case Action::Emoteonlyoff: + case Action::Followers: + case Action::Followersoff: + case Action::Uniquechat: + case Action::Uniquechatoff: + case Action::Slow: + case Action::Slowoff: + case Action::Subscribers: + case Action::Subscribersoff: + case Action::Unraid: + case Action::DeleteMessage: + case Action::Raid: + case Action::AddBlockedTerm: + case Action::AddPermittedTerm: + case Action::RemoveBlockedTerm: + case Action::RemovePermittedTerm: + case Action::Mod: + case Action::Unmod: + case Action::ApproveUnbanRequest: + case Action::DenyUnbanRequest: + case Action::Warn: + case Action::SharedChatBan: + case Action::SharedChatTimeout: + case Action::SharedChatUnban: + case Action::SharedChatUntimeout: + case Action::SharedChatDelete: + break; + } +} + +QString Connection::getSessionID() const +{ + return this->sessionID; +} + +bool Connection::isSubscribedTo(const SubscriptionRequest &request) const +{ + return this->subscriptions.contains(request); +} + +void Connection::markRequestSubscribed(const SubscriptionRequest &request) +{ + this->subscriptions.emplace(request); +} + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/Connection.hpp b/src/providers/twitch/eventsub/Connection.hpp new file mode 100644 index 00000000..2627b52b --- /dev/null +++ b/src/providers/twitch/eventsub/Connection.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "providers/twitch/eventsub/SubscriptionRequest.hpp" +#include "twitch-eventsub-ws/listener.hpp" +#include "twitch-eventsub-ws/session.hpp" + +#include +#include + +#include + +namespace chatterino::eventsub { + +class Connection final : public lib::Listener +{ +public: + void onSessionWelcome( + lib::messages::Metadata metadata, + lib::payload::session_welcome::Payload payload) override; + + void onNotification(lib::messages::Metadata metadata, + const boost::json::value &jv) override; + + void onChannelBan(lib::messages::Metadata metadata, + lib::payload::channel_ban::v1::Payload payload) override; + + void onStreamOnline( + lib::messages::Metadata metadata, + lib::payload::stream_online::v1::Payload payload) override; + + void onStreamOffline( + lib::messages::Metadata metadata, + lib::payload::stream_offline::v1::Payload payload) override; + + void onChannelChatNotification( + lib::messages::Metadata metadata, + lib::payload::channel_chat_notification::v1::Payload payload) override; + + void onChannelUpdate( + lib::messages::Metadata metadata, + lib::payload::channel_update::v1::Payload payload) override; + + void onChannelChatMessage( + lib::messages::Metadata metadata, + lib::payload::channel_chat_message::v1::Payload payload) override; + + void onChannelModerate( + lib::messages::Metadata metadata, + lib::payload::channel_moderate::v2::Payload payload) override; + + QString getSessionID() const; + + bool isSubscribedTo(const SubscriptionRequest &request) const; + void markRequestSubscribed(const SubscriptionRequest &request); + // TODO: Add an "markRequestUnsubscribed" method + +private: + QString sessionID; + + std::unordered_set subscriptions; +}; + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/Controller.cpp b/src/providers/twitch/eventsub/Controller.cpp new file mode 100644 index 00000000..8f53f878 --- /dev/null +++ b/src/providers/twitch/eventsub/Controller.cpp @@ -0,0 +1,318 @@ +#include "providers/twitch/eventsub/Controller.hpp" + +#include "common/QLogging.hpp" +#include "common/Version.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/eventsub/Connection.hpp" +#include "util/RenameThread.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +/// Enable LOCAL_EVENTSUB when you want to debug eventsub with a local instance of the Twitch CLI +/// twitch event websocket start-server --ssl --port 3012 +constexpr bool LOCAL_EVENTSUB = false; + +std::tuple getEventSubHost() +{ + if constexpr (LOCAL_EVENTSUB) + { + return {"localhost", "3012", "/ws"}; + } + + return {"eventsub.wss.twitch.tv", "443", "/ws"}; +} + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +const auto &LOG = chatterinoTwitchEventSub; + +} // namespace + +namespace chatterino::eventsub { + +Controller::Controller() + : userAgent(QStringLiteral("chatterino/%1 (%2)") + .arg(Version::instance().version(), + Version::instance().commitHash()) + .toUtf8() + .toStdString()) + , ioContext(1) + , work(boost::asio::make_work_guard(this->ioContext)) +{ + std::tie(this->eventSubHost, this->eventSubPort, this->eventSubPath) = + getEventSubHost(); + this->thread = std::make_unique([this] { + this->ioContext.run(); + }); + renameThread(*this->thread, "C2EventSub"); + + this->threadGuard = std::make_unique(this->thread->get_id()); +} + +Controller::~Controller() +{ + qCInfo(LOG) << "Controller dtor start"; + this->queuedSubscriptions.clear(); + + for (const auto &weakConnection : this->connections) + { + auto connection = weakConnection.lock(); + if (!connection) + { + continue; + } + + connection->close(); + } + + this->work.reset(); + + if (this->thread->joinable()) + { + this->thread->join(); + } + else + { + qCWarning(LOG) << "Thread not joinable"; + } + + qCInfo(LOG) << "Controller dtor end"; +} + +void Controller::removeRef(const SubscriptionRequest &request) +{ + std::lock_guard lock(this->subscriptionsMutex); + + assert(this->activeSubscriptions.contains(request)); + + auto &xd = this->activeSubscriptions[request]; + xd.refCount--; + qCInfo(LOG) << "Remove ref for" << request << xd.refCount; + // todo use actual atomic things here to be smart + if (xd.refCount <= 0) + { + qCInfo(LOG) << "TODO: Unsubscribe from" << request; + } +} + +SubscriptionHandle Controller::subscribe(const SubscriptionRequest &request) +{ + bool needToSubscribe = false; + + { + std::lock_guard lock(this->subscriptionsMutex); + + auto &xd = this->activeSubscriptions[request]; + if (xd.refCount == 0) + { + needToSubscribe = true; + } + xd.refCount++; + qCInfo(LOG) << "Add ref for" << request << xd.refCount; + } + + auto handle = std::make_unique(request); + + if (needToSubscribe) + { + boost::asio::post(this->ioContext, [this, request] { + this->subscribe(request, false); + }); + } + + return handle; +} + +void Controller::subscribe(const SubscriptionRequest &request, bool isQueued) +{ + qCInfo(LOG) << "Subscribe request for" << request.subscriptionType; + boost::asio::post(this->ioContext, [this, request, isQueued] { + // 1. Flush dead connections (maybe this should not be done here) + // TODO: implement + + if (isQueued) + { + qCInfo(LOG) << "Removing subscription from queued list"; + this->queuedSubscriptions.erase(request); + } + + if (this->queuedSubscriptions.contains(request)) + { + qCWarning(LOG) << "We already have a queued subscription for this, " + "let's chill :)"; + return; + } + + uint32_t openButNotReadyConnections = 0; + + // 2. Check if any currently open connection can handle this subscription + for (const auto &weakConnection : this->connections) + { + auto connection = weakConnection.lock(); + if (!connection) + { + // TODO: remove it here? + continue; + } + + auto *listener = + dynamic_cast(connection->getListener()); + + if (listener == nullptr) + { + // something really goofy is going on + qCWarning(LOG) << "listener was not the correct type"; + continue; + } + + if (listener->getSessionID().isEmpty()) + { + // This connection is open but it's not ready (i.e. no welcome has been posted yet) + ++openButNotReadyConnections; + continue; + } + + // TODO: Check if this listener has room for another subscription + + // TODO: Don't hardcode the subscription version + getHelix()->createEventSubSubscription( + request, listener->getSessionID(), + [this, request, connection, + weakConnection{weakConnection}](const auto &res) { + qCInfo(LOG) << "success" << res; + this->markRequestSubscribed(request, weakConnection); + }, + [this, request](const auto &error, const auto &errorString) { + using Error = HelixCreateEventSubSubscriptionError; + switch (error) + { + case Error::BadRequest: + qCWarning(LOG) << "BadRequest" << errorString; + break; + + case Error::Unauthorized: + qCWarning(LOG) << "Unauthorized" << errorString; + break; + + case Error::Forbidden: + qCWarning(LOG) << "Forbidden" << errorString; + break; + + case Error::Conflict: + // This session ID is already subscribed to this request, some logic of ours is wrong + qCWarning(LOG) << "Conflict" << errorString; + break; + + case Error::Ratelimited: + qCWarning(LOG) << "Ratelimited" << errorString; + break; + + case Error::Forwarded: + default: + qCWarning(LOG) + << "Unhandled error, retrying subscription" + << errorString; + boost::asio::post(this->ioContext, [this, request] { + this->queueSubscription( + request, boost::posix_time::seconds(2)); + }); + break; + } + }); + return; + } + + if (openButNotReadyConnections == 0) + { + // No connection was available to handle this subscription request, create a new connection + this->createConnection(); + this->queueSubscription(request, boost::posix_time::millisec(500)); + } + else + { + if (openButNotReadyConnections > 1) + { + qCWarning(LOG) << "We have" << openButNotReadyConnections + << "open but not ready connections, hmmm"; + } + + // At least one connection is open, but it has not gotten the welcome message yet + this->queueSubscription(request, boost::posix_time::millisec(250)); + } + }); +} + +void Controller::createConnection() +{ + try + { + boost::asio::ssl::context sslContext{ + boost::asio::ssl::context::tlsv12_client}; + + if constexpr (!LOCAL_EVENTSUB) + { + sslContext.set_verify_mode( + boost::asio::ssl::verify_peer | + boost::asio::ssl::verify_fail_if_no_peer_cert); + sslContext.set_default_verify_paths(); + + boost::certify::enable_native_https_server_verification(sslContext); + } + + auto connection = std::make_shared( + this->ioContext, sslContext, std::make_unique()); + + this->registerConnection(connection); + + connection->run(this->eventSubHost, this->eventSubPort, + this->eventSubPath, this->userAgent); + } + catch (std::exception &e) + { + qCWarning(LOG) << "Error in EventSub run thread" << e.what(); + } +} + +void Controller::registerConnection(std::weak_ptr &&connection) +{ + this->threadGuard->guard(); + + this->connections.emplace_back(std::move(connection)); +} + +void Controller::queueSubscription(const SubscriptionRequest &request, + boost::posix_time::time_duration delay) +{ + this->threadGuard->guard(); + + auto resubTimer = + std::make_unique(this->ioContext); + resubTimer->expires_from_now(delay); + resubTimer->async_wait([this, request](const auto &ec) { + if (!ec) + { + // The timer passed naturally + this->subscribe(request, true); + } + }); + + this->queuedSubscriptions.emplace(request, std::move(resubTimer)); +} + +void Controller::markRequestSubscribed(const SubscriptionRequest &request, + std::weak_ptr connection) +{ + std::lock_guard lock(this->subscriptionsMutex); + + this->activeSubscriptions[request].connection = std::move(connection); +} + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/Controller.hpp b/src/providers/twitch/eventsub/Controller.hpp new file mode 100644 index 00000000..a218575b --- /dev/null +++ b/src/providers/twitch/eventsub/Controller.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include "providers/twitch/eventsub/SubscriptionHandle.hpp" +#include "providers/twitch/eventsub/SubscriptionRequest.hpp" +#include "twitch-eventsub-ws/session.hpp" +#include "util/ThreadGuard.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace chatterino::eventsub { + +class IController +{ +public: + virtual ~IController() = default; + + virtual void removeRef(const SubscriptionRequest &request) = 0; + + /// Subscribe will make a request to each open connection and ask them to + /// add this subscription. + /// + /// If this subscription already exists, this call is a no-op. + /// + /// If no open connection has room for this subscription, this function will + /// create a new connection and queue up the subscription to run again after X seconds. + /// + /// TODO: Return a SubscriptionHandle that handles unsubscriptions + /// Dupe subscriptions should return shared subscription handles + /// So no more owners of the subscription handle means we send an unsubscribe request + [[nodiscard]] virtual SubscriptionHandle subscribe( + const SubscriptionRequest &request) = 0; +}; + +class Controller : public IController +{ +public: + Controller(); + ~Controller() override; + + void removeRef(const SubscriptionRequest &request) override; + + [[nodiscard]] SubscriptionHandle subscribe( + const SubscriptionRequest &request) override; + +private: + void subscribe(const SubscriptionRequest &request, bool isQueued); + + void createConnection(); + void registerConnection(std::weak_ptr &&connection); + + void queueSubscription(const SubscriptionRequest &request, + boost::posix_time::time_duration delay); + + void markRequestSubscribed(const SubscriptionRequest &request, + std::weak_ptr connection); + + const std::string userAgent; + + std::string eventSubHost; + std::string eventSubPort; + std::string eventSubPath; + + std::unique_ptr thread; + std::unique_ptr threadGuard; + boost::asio::io_context ioContext; + boost::asio::executor_work_guard + work; + + std::vector> connections; + + struct XD { + int32_t refCount = 0; + std::weak_ptr connection; + }; + + std::mutex subscriptionsMutex; + std::unordered_map activeSubscriptions; + + std::unordered_map> + queuedSubscriptions; +}; + +class DummyController : public IController +{ +public: + ~DummyController() override = default; + + void removeRef(const SubscriptionRequest &request) override + { + (void)request; + }; + + [[nodiscard]] SubscriptionHandle subscribe( + const SubscriptionRequest &request) override + { + (void)request; + return {}; + }; +}; + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/MessageBuilder.cpp b/src/providers/twitch/eventsub/MessageBuilder.cpp new file mode 100644 index 00000000..a6fd398e --- /dev/null +++ b/src/providers/twitch/eventsub/MessageBuilder.cpp @@ -0,0 +1,81 @@ +#include "providers/twitch/eventsub/MessageBuilder.hpp" + +#include "messages/MessageBuilder.hpp" + +namespace chatterino::eventsub { + +MessagePtr makeVipMessage( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_moderate::v2::Event &event, + const lib::payload::channel_moderate::v2::Vip &action) +{ + MessageBuilder builder; + + QString text; + + builder.emplace(); + builder->flags.set(MessageFlag::System); + builder->flags.set(MessageFlag::Timeout); + builder->loginName = event.moderatorUserLogin.qt(); + + builder.emplace( + event.moderatorUserName.qt(), event.moderatorUserLogin.qt(), + MessageColor::System, + channel->getUserColor(event.moderatorUserLogin.qt())); + text.append(event.moderatorUserLogin.qt() + " "); + + builder.emplaceSystemTextAndUpdate("has added", text); + + builder.emplace( + action.userName.qt(), action.userLogin.qt(), MessageColor::System, + channel->getUserColor(action.userLogin.qt())); + text.append(action.userLogin.qt() + " "); + + builder.emplaceSystemTextAndUpdate("as a VIP of this channel.", text); + + builder.message().messageText = text; + builder.message().searchText = text; + + builder.message().serverReceivedTime = time; + + return builder.release(); +} + +MessagePtr makeUnvipMessage( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_moderate::v2::Event &event, + const lib::payload::channel_moderate::v2::Unvip &action) +{ + MessageBuilder builder; + + QString text; + + builder.emplace(); + builder->flags.set(MessageFlag::System); + builder->flags.set(MessageFlag::Timeout); + builder->loginName = event.moderatorUserLogin.qt(); + + builder.emplace( + event.moderatorUserName.qt(), event.moderatorUserLogin.qt(), + MessageColor::System, + channel->getUserColor(event.moderatorUserLogin.qt())); + text.append(event.moderatorUserLogin.qt() + " "); + + builder.emplaceSystemTextAndUpdate("has removed", text); + + builder.emplace( + action.userName.qt(), action.userLogin.qt(), MessageColor::System, + channel->getUserColor(action.userLogin.qt())); + text.append(action.userLogin.qt() + " "); + + builder.emplaceSystemTextAndUpdate("as a VIP of this channel.", text); + + builder.message().messageText = text; + builder.message().searchText = text; + + builder.message().serverReceivedTime = time; + + return builder.release(); +} + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/MessageBuilder.hpp b/src/providers/twitch/eventsub/MessageBuilder.hpp new file mode 100644 index 00000000..e122b936 --- /dev/null +++ b/src/providers/twitch/eventsub/MessageBuilder.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "messages/Message.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "twitch-eventsub-ws/payloads/channel-moderate-v2.hpp" + +#include + +namespace chatterino::eventsub { + +/// has added as a VIP of this channel. +MessagePtr makeVipMessage( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_moderate::v2::Event &event, + const lib::payload::channel_moderate::v2::Vip &action); + +/// has removed as a VIP of this channel. +MessagePtr makeUnvipMessage( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_moderate::v2::Event &event, + const lib::payload::channel_moderate::v2::Unvip &action); + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/SubscriptionHandle.cpp b/src/providers/twitch/eventsub/SubscriptionHandle.cpp new file mode 100644 index 00000000..fb5b3bf2 --- /dev/null +++ b/src/providers/twitch/eventsub/SubscriptionHandle.cpp @@ -0,0 +1,25 @@ +#include "providers/twitch/eventsub/SubscriptionHandle.hpp" + +#include "Application.hpp" +#include "providers/twitch/eventsub/Controller.hpp" + +namespace chatterino::eventsub { + +RawSubscriptionHandle::RawSubscriptionHandle(SubscriptionRequest request_) + : request(std::move(request_)) +{ + // getApp()->getEventSub()->addRef(request); +} + +RawSubscriptionHandle::~RawSubscriptionHandle() +{ + auto *app = tryGetApp(); + if (app == nullptr) + { + // We're shutting down, assume the unsubscription has been taken care of + return; + } + app->getEventSub()->removeRef(request); +} + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/SubscriptionHandle.hpp b/src/providers/twitch/eventsub/SubscriptionHandle.hpp new file mode 100644 index 00000000..1676543f --- /dev/null +++ b/src/providers/twitch/eventsub/SubscriptionHandle.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "providers/twitch/eventsub/SubscriptionRequest.hpp" + +#include + +namespace chatterino::eventsub { + +struct RawSubscriptionHandle { + const SubscriptionRequest request; + + RawSubscriptionHandle(SubscriptionRequest request_); + + ~RawSubscriptionHandle(); +}; + +using SubscriptionHandle = std::unique_ptr; + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/SubscriptionRequest.cpp b/src/providers/twitch/eventsub/SubscriptionRequest.cpp new file mode 100644 index 00000000..81216d94 --- /dev/null +++ b/src/providers/twitch/eventsub/SubscriptionRequest.cpp @@ -0,0 +1,37 @@ +#include "providers/twitch/eventsub/SubscriptionRequest.hpp" + +#include + +namespace chatterino::eventsub { + +QDebug &operator<<(QDebug &dbg, const SubscriptionRequest &v) +{ + dbg << "eventsub::SubscriptionRequest{ type:" << v.subscriptionType + << "version:" << v.subscriptionVersion; + if (!v.conditions.empty()) + { + dbg << "conditions:["; + for (const auto &[conditionKey, conditionValue] : v.conditions) + { + dbg << conditionKey << "=" << conditionValue << ','; + } + dbg << ']'; + } + dbg << '}'; + return dbg; +} + +bool operator==(const SubscriptionRequest &lhs, const SubscriptionRequest &rhs) +{ + return std::tie(lhs.subscriptionType, lhs.subscriptionVersion, + lhs.conditions) == std::tie(rhs.subscriptionType, + rhs.subscriptionVersion, + rhs.conditions); +} + +bool operator!=(const SubscriptionRequest &lhs, const SubscriptionRequest &rhs) +{ + return !(lhs == rhs); +} + +} // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/SubscriptionRequest.hpp b/src/providers/twitch/eventsub/SubscriptionRequest.hpp new file mode 100644 index 00000000..dde8090d --- /dev/null +++ b/src/providers/twitch/eventsub/SubscriptionRequest.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace chatterino::eventsub { + +struct SubscriptionRequest { + /// e.g. "channel.ban" + /// can be made into an enum later + QString subscriptionType; + + // e.g. "1" + // maybe this should be part of the enum later + QString subscriptionVersion; + + /// Optional list of conditions for the subscription + std::vector> conditions; + + friend QDebug &operator<<(QDebug &dbg, const SubscriptionRequest &v); +}; + +bool operator==(const SubscriptionRequest &lhs, const SubscriptionRequest &rhs); +bool operator!=(const SubscriptionRequest &lhs, const SubscriptionRequest &rhs); + +} // namespace chatterino::eventsub + +namespace std { + +template <> +struct hash { + size_t operator()(const chatterino::eventsub::SubscriptionRequest &v) const + { + size_t seed = 0; + + boost::hash_combine(seed, qHash(v.subscriptionType)); + boost::hash_combine(seed, qHash(v.subscriptionVersion)); + + for (const auto &[conditionKey, conditionValue] : v.conditions) + { + boost::hash_combine(seed, qHash(conditionKey)); + boost::hash_combine(seed, qHash(conditionValue)); + } + + return seed; + } +}; + +} // namespace std diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index e996a832..7783bd5a 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -638,6 +638,10 @@ public: "/sound/backend", SoundBackend::Miniaudio, }; + BoolSetting enableExperimentalEventSub = { + "/eventsub/enableExperimental", + false, + }; private: ChatterinoSetting> highlightedMessagesSetting = diff --git a/src/util/ThreadGuard.hpp b/src/util/ThreadGuard.hpp index 00748033..41de8146 100644 --- a/src/util/ThreadGuard.hpp +++ b/src/util/ThreadGuard.hpp @@ -14,6 +14,15 @@ struct ThreadGuard { mutable std::optional threadID; #endif + ThreadGuard() = default; + + explicit ThreadGuard(std::thread::id threadID_) +#ifndef NDEBUG + : threadID(threadID_) +#endif + { + } + inline void guard() const { #ifndef NDEBUG diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 36226677..c1df7a67 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -129,6 +129,9 @@ AboutPage::AboutPage() addLicense(form.getElement(), "expected-lite", "https://github.com/martinmoene/expected-lite", ":/licenses/expected-lite.txt"); + addLicense(form.getElement(), "Howard Hinnant's date.h", + "https://github.com/HowardHinnant/date", + ":/licenses/howard-hinnant-date.txt"); } // Attributions diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 1c92fa9b..a257ccec 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -1273,6 +1273,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) "with sound playback on your system") ->addTo(layout); + layout.addCheckbox( + "Enable experimental Twitch EventSub support (requires restart)", + s.enableExperimentalEventSub); + layout.addStretch(); // invisible element for width diff --git a/vcpkg.json b/vcpkg.json index 5ab85f87..6375a049 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -5,9 +5,11 @@ "builtin-baseline": "01f602195983451bc83e72f4214af2cbc495aa94", "dependencies": [ "boost-asio", + "boost-beast", "boost-circular-buffer", "boost-foreach", "boost-interprocess", + "boost-json", "boost-signals2", "boost-variant", {